Блог пользователя vitar

Автор vitar, 11 лет назад, По-русски

Когда имеешь дело с вещественными числами в первую очередь нужно подумать нельзя ли от них избавиться и перейти к целым. Причём умножение на какую-нибудь степень десятки по сути применимо только к числам из инпута, данным с фиксированной точностью. Если же даблы возникают в процессе вычислений, то стоит постараться преобразовать формулу, так, что бы они не возникали. Например не извлекать корни, не делить что-нибудь и т.д. Классический пример: у нас в процессе вычислений получились 2 числа: a/b и c/d. Нам надо сравнить их. Вместо (double)a / b < (double)c / d. Почти всегда стоит использовать форму a*d < c*b. Здесь погрешностей не будет. Но надо проследить, что бы a*d не переполнилось. И только если это произведение переполняет long long, то можно подумать о даблах... Но скорей всего это означает, что надо вывести другую формулу.

Эпсилон необходимо использовать ВСЕГДА, когда нас интересует равенство двух вещественных чисел. Короче, по сути вообще при любом сравнении. Исключение составляет разве что случай, когда равенство вообще невозможно. Например, когда нас интересует, что больше p^a или q^b, где p и q — простые. Мы напишем a*log(p) > b*log(q). Они точно не могут быть равны, значит эпсилон нам только помешает если значения близки. В остальных случаях — эпсилон обязателен.

Какой выбирать эпсилон? Большинство даже очень опытных команд всё-таки подгоняют эпсилон, когда не могут сдать задачу. По-этому точно разве что Бурундук скажет :) И то в половине случаев сам себя перемудрит :) Эпсилон должен быть меньше, чем точность требуемая в ответе. Но эпсилон должен быть больше чем лучшая точность, которую мы технически можем получить. Дабл вмещает 15 десятичных знаков. Допустим ответ по модулю не превосходит 10^8 и нам надо вывести его с 3 знаками после запятой. Тоесть эпсилон должен быть меньше 10^-3, но при этом больше 10^-7. Почему? Если он будет слишком большим, то мы не получим нужной точности, это очевидно. Если же он будет слишком маленьким, то он не учтётся в дабле. 10^9 + 10^-9 == 10^9. Потому что для того, что бы действительно произвести сложение и сохранить результат надо уже 18 десятичных знаков, а у нас в дабле их только 15, значит младшие (10^-9) отбросятся. В моём предыдущем примере у нас 8 знаков до запятой, соответственно после запятой остаётся только 7. Значит эпсилон не может быть меньше 10^-7. Тут, конечно, очень многое зависит от способов вычислений. Например если писать вещественный бинсёрч так: while (abs(l - r) > eps) {... то всё, что я писал полностью справедливо. Однако если писать его умней: while (d > eps) { c = l + d; d /= 2... то здесь можно взять вообще любое эпсилон. В качестве ещё одного совета — можно прикинуть погрешность и взять эпсилон где-нибудь на 2 разряда больше погрешности. Как прикинуть погрешность — придёт с опытом :)

самые худшие операции при работе с даблами — сложение и вычитание. Потому, что если мы складываем большое число с маленьким, то большое всегда останется, а младшие разряды маленького могут отброситься если не влезут в дабл. По-этому если, например, надо сложить много даблов, то лучше это делать в порядке их возрастания. И вообще стоит всячески избегать последовательных операций с даблами, которые ВСЕГДА приводят к накоплению ошибки. Например у нас есть цикл:

for (d1 = 0, d2 = 0, i = 0; i < 1000000; i ++) {
 d1 = d1 + i / 1000000.0; 
 d2 = i * (i + 1) / 2.0 / 1000000.0 
}

d2 будет гораздо точней d1.

Long double имеет смысл не использовать вообще никогда, так как на большинстве компиляторов он равен double. BidDecimal в Java — другое дело.

Даблы в качестве ключей для map тоже лучше не использовать. Обычно вместо них можно создать структуру из нескольких целочисленных полей. Это будет всегда правильно. Если уж использовать, то перегрузить оператор меньше для даблов: bool operator < (double a, dobule b) { return a < b — eps};

UPD: рекомендую обратить внимание на некоторые комментарии:

http://codeforces.com/blog/entry/6345#comment-117294

http://codeforces.com/blog/entry/6345#comment-117339 и комментарии к нему

http://codeforces.com/blog/entry/6345#comment-117542

  • Проголосовать: нравится
  • +105
  • Проголосовать: не нравится

»
11 лет назад, # |
Rev. 2   Проголосовать: нравится 0 Проголосовать: не нравится

Спасибо!
Long double имеет смысл не использовать вообще никогда, так как на большинстве компиляторов он равен double. — например, у меня бывало так, что замена double на long double в тернарном поиске приводила к AC. Или это возможно было связано с неправильной оценкой нужного количества итераций?
А данный компаратор к сожалению иногда дает сбои, особенно в sort.

  • »
    »
    11 лет назад, # ^ |
      Проголосовать: нравится +16 Проголосовать: не нравится

    Да, некоторые компиляторы таки поддерживают Long Double, но полагаться на это очень рискованно. Двоичный и троичный поиски я уже давно пишу без эпсилон вообще.

    for (it = 0; it < 500; it ++) {
      c = (l + r) / 2;
      ...
    }
    

    Такого количества итераций обычно хватает для любой задачи. Главное что бы в тл влезло. Хотя в теории и не имеет смысл делать больше 64 итераций для бинсёрча и где-нибудь 100 для троичного, на практике так надёжней.

    Да, компаратор для даблов всегда ненадёжная штука. Его стоит использовать только если пошаманить хочется. Иначе надо переходить к структурам из интов.

    • »
      »
      »
      11 лет назад, # ^ |
      Rev. 2   Проголосовать: нравится +33 Проголосовать: не нравится

      Во-первых, Visual C++ приравнивает long double к double, а g++, как правило, нет. Поэтому иногда имеет смысл посылать решение (геометрию особенно) на g++, а не на Visual C++, если вы используете long double.

      Во-вторых, я бы выразился так: long double имеет смысл использовать вообще всегда. Если очень лень писать два слова вместо одного, то можно сделать так:

      #define double long double
      • »
        »
        »
        »
        11 лет назад, # ^ |
          Проголосовать: нравится 0 Проголосовать: не нравится

        Категорически не согласен, так как считаю, что код должен максимально одинаково исполняться на всех компиляторах. А ты должен всегда понимать что он делает. Иначе будешь натыкаться на свои же грабли и не понимать как работает или не работает твой же код. Тут, например, дело ещё не только в точности, но и в размере. Выделишь массив даблов — и словишь мемори лимит. Или производительность упадёт.

        • »
          »
          »
          »
          »
          11 лет назад, # ^ |
            Проголосовать: нравится +11 Проголосовать: не нравится

          Мемори лимит — это другой вопрос, можно как угодно накосячить и его получить, дело не в этом.

          Код по определению не может всегда выполняться одинаково на всех компиляторах, потому что компиляторы разные. Поэтому можно использовать всякие фишки вроде того, что в g++ тип long double точнее, чем в Visual C++, но разумеется, надо это делать с головой.

          • »
            »
            »
            »
            »
            »
            11 лет назад, # ^ |
            Rev. 2   Проголосовать: нравится -12 Проголосовать: не нравится

            Код по определению не может всегда выполняться одинаково на всех компиляторах, потому что компиляторы разные

            мда? И почему всем обычно не нравится, когда сайты в разных браузерах по разному отображаются... ведь браузеры же разные...

            Существуют спецификации. Код удовлетворяющий им ОБЯЗАН совершенно определённо и одинаково работать на любом компиляторе, который эти спецификации поддерживает.

            • »
              »
              »
              »
              »
              »
              »
              11 лет назад, # ^ |
                Проголосовать: нравится +19 Проголосовать: не нравится

              И браузеры, и компиляторы иногда не следуют спецификациям :) может, мои слова не очень педагогичны, но это правда жизни, и ее тоже нужно знать. Повторюсь, фишки тоже нужно использовать с головой.

      • »
        »
        »
        »
        11 лет назад, # ^ |
          Проголосовать: нравится +11 Проголосовать: не нравится

        Только надо не забывать, что printf не умеет выводить long double, и надо при выводе все же к double приводить.

        • »
          »
          »
          »
          »
          11 лет назад, # ^ |
          Rev. 2   Проголосовать: нравится 0 Проголосовать: не нравится

          конечно, на это нужно либо потоки iostream, либо предварительно запомнить старый тип double, и перед выводом приводить к нему:

          typedef double old_double;
          #define double long double;
          • »
            »
            »
            »
            »
            »
            11 лет назад, # ^ |
              Проголосовать: нравится 0 Проголосовать: не нравится

            я обычно пишу так:
            typedef long double ld
            Везде пишу ld (кроме printf), а потом при необходимости просто long убираю.

      • »
        »
        »
        »
        11 лет назад, # ^ |
          Проголосовать: нравится +25 Проголосовать: не нравится

        В подтверждение: размеры double/long double в байтах:

        • MSVC++ 2012 для x86: 8/8
        • MinGW для x86: 8/12
        • GCC/Linux для x86: 8/12
        • GCC/Linux для x86-64: 8/16
        • »
          »
          »
          »
          »
          11 лет назад, # ^ |
            Проголосовать: нравится 0 Проголосовать: не нравится

          Для GCC/Linux для x86-64 только размер больше (для выравнивания по 64 бита) или там в самом деле больше задействовано бит на мантиссу и экспоненту? Наверное же второе, да?

          • »
            »
            »
            »
            »
            »
            11 лет назад, # ^ |
              Проголосовать: нравится +12 Проголосовать: не нравится

            std::numeric_limits<long double>::digits равен 64 как в 32-битном режиме, так и в 64-битном. Так что всё-таки первое.

            • »
              »
              »
              »
              »
              »
              »
              11 лет назад, # ^ |
                Проголосовать: нравится 0 Проголосовать: не нравится

              Да-да, когда писал "второе" имел ввиду первый вариант. Кстати, есть же еще какие-то __float128. Ими кто-нибудь умеет пользоваться?

              • »
                »
                »
                »
                »
                »
                »
                »
                11 лет назад, # ^ |
                  Проголосовать: нравится +5 Проголосовать: не нравится

                Ух ты, не знал про такое. Быстрый поиск даёт:

                В запуске на Codeforces базовые операции с типом работают, <quadmath.h> присутствует, однако компиляция валится с undefined reference при попытке воспользоваться cosq() и т.д. (наверное, нужен -lquadmath).

                Занятно, что этот тип поддерживается и на x86, в то время как __int128 не поддерживается.

              • »
                »
                »
                »
                »
                »
                »
                »
                11 лет назад, # ^ |
                  Проголосовать: нравится 0 Проголосовать: не нравится

                А что им уметь-то пользоваться? Берешь и пользуешься. Обычные действительные числа, с четырехкратной точностью.

                Описаны в википедии подробно: http://en.wikipedia.org/wiki/Floating_point http://en.wikipedia.org/wiki/Quad_precision

                Вот только реализованы они при помощи software emulation. Я как-то тестировал алгоритм флойда на g++ с long double и с __float128, использование последних чисел замедляло программу в 50 раз.

      • »
        »
        »
        »
        11 лет назад, # ^ |
          Проголосовать: нравится +5 Проголосовать: не нравится

        Хочется отметить, что long double работает примерно в 2 раза медленнее, чем double. Например, на FFT. Можно случайно поймать на этом TLE, будьте внимательнее.

    • »
      »
      »
      11 лет назад, # ^ |
        Проголосовать: нравится +9 Проголосовать: не нравится

      В тренировке есть задача С, которую получилось сдать только с long double на g++, так что не стоит быть таким категоричным к нему)

      • »
        »
        »
        »
        11 лет назад, # ^ |
          Проголосовать: нравится 0 Проголосовать: не нравится

        в этой задаче еще можно было взять производную от функции. Я так сделал, после того как мое решение в даблах не заходило(правда на Java).

      • »
        »
        »
        »
        11 лет назад, # ^ |
          Проголосовать: нравится 0 Проголосовать: не нравится

        Только что с double свалилась, а с long double зашла: 2893084.

    • »
      »
      »
      11 лет назад, # ^ |
        Проголосовать: нравится 0 Проголосовать: не нравится

      Делать больше 64 итераций смысл очень даже имеет: например, если бинпоиск в отрезке длины 1e40, понадобится около 133 итераций, чтобы уменьшить длину отрезка до единицы. Наверно, не может понадобиться больше примерно 2048 итераций.

      • »
        »
        »
        »
        11 лет назад, # ^ |
        Rev. 3   Проголосовать: нравится +18 Проголосовать: не нравится

        Получается, что одна итерация бинпоиска может отсекать намного меньше половины представимых действительных чисел в отрезке. Чтобы отсекать половину, можно попробовать выбирать в качестве средней точки не среднее арифметическое краев, а среднее между ними (по счету) представимое число. Вот тогда будет достаточно 64 итераций. Если оба края положительные, это просто сделать: достаточно взять среднее арифметическое этих чисел как целых (но наверно стандарт ничего по этому поводу не гарантирует):

        double a,b;
        ...
        long long cl=((unsigned long long&)a+(unsigned long long&)b)/2;
        double c=(double&)cl;
        

        Осторожно, я ни разу так не пробовал делать, может быть какой-нибудь подвох :)

        • »
          »
          »
          »
          »
          11 лет назад, # ^ |
            Проголосовать: нравится 0 Проголосовать: не нравится

          А не появится подвох когда, например, a==0.3; b==0.4
          Чему будет равно c?

        • »
          »
          »
          »
          »
          11 лет назад, # ^ |
          Rev. 6   Проголосовать: нравится 0 Проголосовать: не нравится

          затупил

          • »
            »
            »
            »
            »
            »
            11 лет назад, # ^ |
            Rev. 2   Проголосовать: нравится +5 Проголосовать: не нравится

            Битовые представления положительных чисел возрастают (выше упоминался пруфлинк). Это как раз из-за того, что биты степени идут раньше бит мантиссы. Для a=0.3,b=1000; => c=17.225000 вполне правдоподобно (ясно, что c всегда не больше среднего арифметического и отдаленно примерно равно среднему геометрическому).

»
11 лет назад, # |
  Проголосовать: нравится +13 Проголосовать: не нравится

Я бы сказал, что не стоит писать условие выхода в бинпоиске с вещественными числами. При неправильном eps это может привести к непонятному TL, который можно долго искать.

Гораздо лучше делать фиксированное число итераций (к тому же его легче подгонять под ограничение по времени):

double L = 0, R = 1e10;
for (int step = 0; step < 100; step++) { // 100 - безумно грубая оценка сверху
  double M = (L + R) / 2;
  // do stuff...
}
  • »
    »
    11 лет назад, # ^ |
      Проголосовать: нравится 0 Проголосовать: не нравится

    Абсолютно согласен. О чём и написал в предыдущем комментарии. Но лично мне проблемы с эпсилонами в бинсёрче помогли лучше понять как работают даблы, по-этому в целях обучения имеет смысл понатыкаться на эти грабли, пока не сможешь писать надёжный бинсёрч как эпсилонами так и итерациями.

»
11 лет назад, # |
Rev. 4   Проголосовать: нравится +13 Проголосовать: не нравится

Ничего не нашел про денормализованные числа, а знание о них иногда бывает полезно в СП.
Наверное хорошо было бы включить инфу об этом в пост.

»
11 лет назад, # |
  Проголосовать: нравится +8 Проголосовать: не нравится

Пока решал простой новогодний контест, обнаружил такую штуку. Одна задача никак не хотелась сдаваться из-за точности (в решении был уверен, да и протестил с брутфорсом). Долго пытался подобрать эпсилон, заменить double на long double, еще какие-то операции заменить, но постоянно получал ВА 17. Потом решил поделить все координаты на 1000, а в конце ответ домножить что-то соответствующее. Это сразу дало ВА 54. Потом заменил 1000 на 100 и получил АС. Выходит, иногда выгоднее решать задачу в маленьких числах для достижения большей точности промежуточных вычислений?

  • »
    »
    11 лет назад, # ^ |
      Проголосовать: нравится +11 Проголосовать: не нравится

    Думаю, что просто было не сильно много сложных тестов и вы по сути загнали рандом =)

  • »
    »
    11 лет назад, # ^ |
      Проголосовать: нравится 0 Проголосовать: не нравится

    Вот это, кстати, интересный эффект. Я на него наткнулся, если не путаю, когда пытался впихнуть численное решение заадчи Джип с тимуса. Если просто поделить инпут на 1000, посчитать и умножить, то получается гораздо точнее чем если считать втупую. И в той задаче понятно, что это не погрешность и не слабые тесты.

    Я не могу нормально объяснить как это работает. Может быть al13n может?

  • »
    »
    11 лет назад, # ^ |
      Проголосовать: нравится +8 Проголосовать: не нравится

    Может быть всё дело просто в сдвиге?

    0.5[10]=0.1[2]
    0.05[10]=0.0000(1100)[2]
    

    То есть возникает просто какой-то хороший сдвиг, который убирает периодичность и с ним округление, как в приведённом выше примере.

»
11 лет назад, # |
Rev. 3   Проголосовать: нравится +39 Проголосовать: не нравится

Эпсилон необходимо использовать ВСЕГДА Если уж использовать, то перегрузить оператор меньше для даблов: bool operator < (double a, dobule b) { return a < b — eps};

Вот так как раз лучше вообще никогда не делать. Если у тебя есть неточное сравнение, то порядок добавления ключей в мэп может очень сильно поменять набор ключей. Например, если eps = 1.0, а ключи: 1, 2, 3, 4. То если ты их добавишь в этом же порядке, то в ключах мэпа будут числа 1 и 3, а мэп будет "думать", что 1 == 2 и 3 == 4. Если добавить в таком порядке: 2, 3, 4, 1. То ключами уже будут 2 и 4. А мэп будет думать что 2 == 3 == 1. Такой мэп обычно приносит больше вреда чем пользы.

Если необходимо группировать вещественные числа по "близости", то перегрузка сравнения с eps очень вредная вещь. В каждой конкретной задаче надо хорошо подумать насколько числа могут быть близки, чтобы правильно их сгруппировать.

последовательных операций с даблами, которые ВСЕГДА приводят к накоплению ошибки

Не всегда, а только когда мантиссы в типе не хватает чтобы сохранить мантиссу результата. Из этого следует, что числа одинаковых порядков будут складываться точнее чем разных.

В твоем примере числа (i / 1000000.0) не могут точно храниться в double. Поэтому "теряется" точность. Если ты будешь в знаменателе использовать 1048576.0, то получишь АБСОЛЮТНО одинаковый результат, что опровергает твое утверждение о всесущем накоплении ошибок.

В работе с вещественными числами с плавающей точкой нет "вечных" правил типа "всегда сравнивай с EPS" или "всегда сравнивай строго". Поможет только одно правило: "Всегда надо думать, чем плохо сравнивать строго и чем плохо сравнивать с погрешностью".

»
5 лет назад, # |
  Проголосовать: нравится -21 Проголосовать: не нравится

Сколько знаков хранит double? Разве не 6 всего?