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

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

Друзья, будьте бдительны!

Алгоритм Левита взятый с http://e-maxx.ru/algo/levit_algorithm работает в худшем случае за экспоненциальное время.

Для пояснения привожу код, который генерирует ацикличный граф без отрицательных ребер из 91 вершин и 121 ребра (а также перемешивает все ребра в случайном порядке), на котором код по ссылке выше делает более 1 000 000 добавлений в очередь.

const int M = 30;
const int W = (int)8e8;

void gen()
{
  for (int i = 0; i < M; i++) 
    g[i].push_back(make_pair(i + 1, 0));
  g[0].push_back(make_pair(M, W));
  n = M;

  for (int i = 0; i < M; i++) 
  {
    g[n].push_back(make_pair(n + 2, W >> i));
    g[n].push_back(make_pair(n + 1, 0));
    g[n + 1].push_back(make_pair(n + 2, 0));
    n += 2;
  }
  n++;
  for (int i = 0; i < n; i++) 
    random_shuffle(g[i].begin(), g[i].end());
}

Какие выводы отсюда я хочу сделать? Не добавляйте в начало очереди, вся проблема в этом! Люди, добавляющие в начало очереди, рискуют однажды доиграться и попасться на хорошие тесты.

Если же вы хотите, чтобы ваш Форд-Беллман работал быстро — пишите обычного Форд-Беллмана с очередью, т.е. добавляйте вершины всегда в конец очереди и следите за тем, чтобы каждая вершина хранилась в очереди не более чем в одном экземпляре. Почитать можно здесь: http://wcipeg.com/wiki/Shortest_Path_Faster_Algorithm

P.S. В правильном "Левите" (см., например, книжку И.В.Романовского, Дискретный Анализ) использовалось 2 очереди. Описанный там алгоритм и правда работает за O(VE). Но от "Форд-Беллмана с очередью" не отличается (улучшение не более чем в 2 раза).

UPD:

Только что услышал такой способ искать кратчайший путь в графе с отрицательными ребрами:

Берем Дейкстру с кучей и запускаем на графе с отрицательными ребрами (при этом всегда, когда расстояние до вершины улучшается, кладем ее в кучу) :-) Казалось бы, этот алгоритм просто просмотрит каждую вершину несколько раз. На самом деле, он тоже работает за экспоненциальное время. Идея теста: давайте превратим ребра веса W в цепочки из двух ребер — веса 2k и (W2k). Далее строим такой же тест, как показан выше.

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

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

Сергей, если не секрет, что надоумило Вас анализировать этот алгоритм? Просто ради интереса, или кто-то напоролся на хороший тест?

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

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

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

    Паша правду говорит, в поезде СПБ-Петрозаводск оказалось, что я не умею доказывать оценку O(VE) для алгоритмов, добавляющих в начало. Оставлять это просто так было нельзя :-)

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

А что если не держать в очереди двух одинаковых вершин?

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

    Если не класть в очередь вершину, которая уже в очереди, то эффект будет тот же.

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

Насколько реально построить двудольный граф в задаче о назначениях так, чтобы валился Левит (если мы решаем эту задачу через min-cost max-flow)?

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

    Это очень просто. Возьмите таблицу умножения.

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

А как доказывается сложность левита при добавлении в конец?

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

    Классический алгоритм Форд-Беллмана: будем V раз пытаться релаксировать все ребра.

    Алгоритм Форд-Беллмана с очередью (также называемый "Левит с добавлением в конец"): будем на каждой из V итераций обрабатывать только ребра из тех вершин, расстояние которых улучшилось на предыдущей итерации (это как раз те вершины, что лежат в очереди).

    Вроде бы очевидно, что второй алгоритм работает всегда не хуже, чем первый =)

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

      Так было бы, если бы мы использовали две очереди (текущей итерации ФБ и новой), и добавляли с контролем дублирования во вторую. Здесь же мы в определенном случае релаксируем ребра "не вовремя", что может быть плохо (как в случае с добавлением в начало).

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

        Ну у а как bfs работает Вы же понимаете? Там тоже в одной и той же очереди лежат вершины, до которых расстояние d, а потом вершины, до которых расстояние d+1.

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

          Не-не, cooler содержательный вопрос задаёт. В отличие от bfs, в нашем случае вершина, расстояние до которой мы только что обновили, может оказаться далеко не в конце очереди (из-за того, что мы не кладём в очередь уже лежащую там вершину).

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

            Ну...)))) Ну и что?) Если класть вершины всегда, памяти O(E), но зато работает так же, как и bfs. А теперь заметим, что если не класть, то хуже не станет. Кажется, все. Я не прав?

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

        Ну всё равно этот ФБ быстро работает. Рассмотрим весь процесс и разобьём его на шаги: каждый шаг — это полный проход по очереди с предыдущего шага. Тогда утвержнение: на i-ом шагу для всех вершин, до которых есть кратчайший путь из  ≤ i рёбер, расстояние найдено правильно. Доказательство несложное — база индукции очевидна, переход — если есть кратчайший путь из i рёбер вида ... - u - v, то на i - 1'ом шаге расстояние до u точно было правильным, а значит, до окончания i-ого шага u с правильным расстоянием хоть раз была извлечена из очереди (и из неё были проведены все возможные релаксации). А значит, расстояние до v посчитано правильно.

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

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

          Вроде, разобрался, спасибо!

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

Пользуясь случаем, напишу про самый быстрый из известных мне вариантов Форда-Беллмана (насколько я помню, автор этого варианта — Tarjan).

Пусть мы хотим использовать алгоритм Форда-Беллмана не только для поиска кратчайших путей, но и для проверки того, есть ли в графе отрицательный цикл. Довольно естественная идея — искать циклы в метках from (которые обычно поддерживаются для восстановления кратчайших путей). В "нормальном" состоянии эти метки образуют лес, и если в этом лесе внезапно появился цикл — то это и будет отрицательный цикл в графе. Так что поддерживая лес меток from, можно быстро проверять, есть ли в графе отрицательные циклы.

Так вот, оказывается, если поддерживать этот лес в процессе алгоритма, можно его сильно ускорить следующим образом. Предположим, мы сделали релаксацию . У вершины v есть поддерево в нашем лесе, некоторые вершины {wi} которого могут лежать в очереди. Заметим, что поскольку мы только что прорелаксировали расстояние до v, все оценки расстояний до {wi} на данный момент неправильные, поэтому из них бесполезно делать релаксации, пока мы не обновим расстояния до них. Это наблюдение и даёт оптимизацию: после релаксации выкинем все вершины поддерева v из очереди и удалим из леса все рёбра поддерева.

В итоге получается следующий рецепт: будем выполнять обычный алгоритм Форда-Беллмана с очередью, поддерживая лес меток from. При релаксации в какую-то вершину убиваем её поддерево в лесе, удаляя вершины этого поддерева из очереди.

Как ни странно, эта оптимизация в среднем даёт очень сильное ускорение — на больших графах "олимпиадного" размера (порядка 106 вершин) количество операций в десятки раз меньше по сравнению с версией без оптимизации.

Реализация алгоритма (автор — ilyaraz).

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

    Класс! Я такого почему-то не знал =(

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

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

      Не, никаких нижних оценок не знаю. Из верхних — есть потокообразный адъ за (C — ограничение на вес, веса целые), но этот алгоритм сложно закодить и он проигрывает ФБ с эвристикой Tarjan'а.

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

Удачных челленджей и взломов =)

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

Надо признать, что мне писали об этом на почту, но я всё не находил времени сесть разобраться... Да и во всяких научных статьях конкретно того "Левита", что у меня, я не находил, поэтому оставалась надежда, что он всё же полиномиальный.

Эх, жалко, такой ведь забавный алгоритм был :)

P.S. Да и всё равно не отнять у него скорости работы на случайных тестах или некоторых графах особого вида (например, граф переходов по клеткам клетчатого поля) — обычное дело, когда на тестах жюри он отрабатывает быстрее Дейкстры :)

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

Вот :) Меня поймать, слава Богу, никто не успел

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

Видимо следует "быть бдительным" и по отношению к статье "Поток минимальной стоимости" (http://e-maxx.ru/algo/min_cost_flow) так как "приведена реализация алгоритма min-cost-flow, базирующаяся на алгоритме Левита.".

Хотя, если честно, последние год — полтора во всех задачах, где нужен был min-cost-max-flow, я писал этот алгоритм и он прекрасно работал и получас АС.

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

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

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

Еще на тему.