EduBrick

Вещественный поиск

Почему «пока разность больше эпсилон» — плохое условие остановки, и что писать вместо него.

3 мин

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

Так делать не надо

while (high - low > 1e-9) {   // цикл может не закончиться никогда
    double mid = (low + high) / 2;
    ...
}

У чисел с плавающей точкой точность относительная, а не абсолютная. Рядом с нулём соседние представимые числа отличаются на ничтожную величину, а рядом с 101010^{10} — примерно на 21062 \cdot 10^{-6}.

Значит, если границы порядка 101010^{10}, разность high - low никогда не станет меньше 10910^{-9}: она упрётся в шаг сетки представимых чисел и перестанет убывать. Цикл будет крутиться, пока не кончится время.

Коварство в том, что при малых значениях всё работает. Примеры из условия обычно малы.

Так надо

for (int step = 0; step < 200; step++) {
    double mid = (low + high) / 2;
    if (check(mid)) high = mid;
    else low = mid;
}

Фиксированное число шагов. Каждый делит промежуток пополам, сто шагов уменьшают его в 21002^{100} раз — несопоставимо меньше того, что вообще различает double. Двести шагов стоят двести вычислений check, то есть практически нисколько.

Такой цикл нельзя зациклить, он не зависит от масштаба чисел, и в нём нечего настраивать.

Сколько шагов брать

Промежуток длины RR после kk шагов имеет длину R/2kR / 2^k. Если нужна точность 10610^{-6} при R=1010R = 10^{10}, требуется log2(1016)54\log_2(10^{16}) \approx 54 шага.

Брать с запасом дёшево: 100 шагов покрывают любой разумный случай, 200 — вообще любой. Единственная причина считать точно — если check очень дорогая.

Точность ответа и точность вывода

Если условие обещает проверку с точностью 10610^{-6}, печатать надо больше знаков, чем требуется: девять после точки достаточно всегда.

printf("%.9f\n", answer);
cout << fixed << setprecision(9) << answer << "\n";

В Python — print(f"{answer:.9f}"). Обычный print для float даёт представление, которого проверяющей программе может не хватить.

Про верхнюю границу

Её стоит выводить из условия, а не брать «побольше». В задаче «найти xx, при котором x2+x=Cx^2 + \sqrt{x} = C» при C1010C \le 10^{10} ответ не превосходит 10510^5: иначе уже x2x^2 вылезет за CC. Взять границей сам CC формально можно, но это лишние шаги и лишние проблемы с точностью на больших числах.

Когда вещественного поиска можно избежать

Часто задача с вещественным ответом сводится к целочисленной. «Максимальная длина куска» при целых длинах — это целое число сантиметров; «минимальное время» при целых скоростях — целое число секунд.

Целочисленный поиск проще: у него честное условие остановки, нет накопления погрешности и не надо думать про формат вывода. Если ответ можно сделать целым — сделайте.