Блог пользователя 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
  • Проголосовать: не нравится

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

Мы с Milanin временами любим поспорить на тему отличия жадности и динамики. Он предпочитает большинство жадностей считать динамикой, я нет.

Что у них общего? И у того и у другого подхода решение задачи получается в результате множества этапов на каждом из которых мы получаем оптимальный ответ для какого-то состояния исходя из некоторых уже полученных оптимальных ответов для других состояний.

Что различного? В случае с динамикой мы фиксируем текущее состояние, для которого уже знаем оптимальный ответ, и рассматриваем ВСЕ способы достичь следующее. Необходимым условием является то, что следующее состояние должно быть достижимо только из уже посчитанных. Возможно на этом этапе используется жадность для того, что бы отсечь некоторые заведомо не оптимальные переходы.

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

Для примера рассмотрим кратчайший путь во взвешенном ориентированном графе. А именно вот в этом:

Из вершины 1 в вершину 3.

Эту задачу можно решать как дейкстрой так и динамикой. При этом дейкстра будет жадностью. Так как на каждом шаге у нас есть множество всевозможных переходов и мы не задумываясь ни о чём выбираем самый дешёвый (с учётом расстояния до вершины из которой он исходит). Мы совершенно не представляем куда можем попасть и у нас нет никаких ограничений, кроме существования перехода. Когда мы этот переход нашли, остальные нас уже не волнуют.

Если же решать задачу динамикой, то, считая ответ для вершины 3, нам необходимо уже знать ответ для вершины 2, и нам обязательно рассмотреть все способы в текущую вершину войти. В то время как жадность позволяет узнать ответ для 3, не зная ещё ответ для 2. Хотя и имея некоторые представления о нём. Для динамики обычно заранее известен порядок рассмотра вершин. Для ориентированного графа — одна из топологических сортировок. В то время как для жадности порядок рассмотра определяется самой жадностью. По крайней мере на него нельзя заранее наложить ограничения не диктуемые свойствами самой жадности.

Жадность позволяет вообще пропустить некоторые промежуточные состояния. Иногда это очень эффективно. Динамика рассматривает все возможные состояния и все возможные переходы между ними (возможно с некоторыми жадными отсечениями). Обычно это позволяет найти ответ для более широкого класса задач.

Буду рад комментариям на эту тему.

Полный текст и комментарии »

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

Автор vitar, 13 лет назад, По-русски
http://codeforces.com/blog/entry/1492

Полный текст и комментарии »

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