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

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

Пост для тех, кому интересно научиться ещё быстрее искать паросочетание в двудольном графе.

Алгоритм Куна ищет паросочетание в двудольном графе за O(VE). Реализация, данная на emaxx, укладывается в такой шаблон:

forn(i, n) {
  fill(used, 0);
  dfs(i);
}

Я обычно пишу так:

fill(used, 0);
forn(i, n) 
  if (dfs(i))
    fill(used, 0);

То есть, обнуляю пометки только если нашёл дополняющую цепь. Теперь Кун работает за O(|ME), где |M| — размер максимального паросочетания.

Решение можно ускорить ещё как минимум в 2 раза, используя жадную инициализацию паросочетания. Идея: применяем Куна не к пустому паросочетанию, а к паросочетанию размера хотя бы |M| / 2. Кстати, |M| / 2 — оценка снизу, если перед жадной инициализацией сделать random_shuffle, жадность найдёт паросочетание чуть побольше, и Кун будет работать чуть быстрее.

Новое для меня

Оказалось, можно заставить Куна работать ещё в несколько раз быстрее...

fill(pair, -1);
for (int run = 1; run; ) {
  run = 0, fill(used, 0);
  forn(i, n)
    if (pair[i] == -1 && dfs(i))
      run = 1;
}

То есть, теперь я не обнуляю пометки даже если нашёл дополняющую цепь.

Я успел потестить на нескольких задачах, например, на задаче про доминошки. Эффект: новая идея без жадной инициализации в 3 раза быстрее старой идеи с жадной инициализацией. Можно скачать тесты и решения и поиграться самостоятельно. Думаете, в доминошках специфический граф? Потестил на произвольном двудольном графе, эффект "на макс тесте в 10 раз быстрее".

Мне эта модификация Куна чем-то напоминает Хопкрофта-Карпа, т.к. получается, что мы за O(E) находим не один путь, а несколько. Непонятно, что стало с асимптотикой алгоритма. Может быть, она тоже улучшилась?)

Спасибо за внимание.

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

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

"новая идея без жадной инициализации" — не совсем верно, ведь первая итерация внешнего цикла во внутреннем делает именно жадную инициализацию, сочетая каждую вершину с первым не тронутым (а значит — не сочтенным) соседом.

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

    Да, ты прав. Я имел в виду "без лишнего кода, который обычно появляется, когда мы её добавляем".

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

Если говорить про константные оптимизации, то как-то не комильфо каждый раз занулять массив used.

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

    Теоретически ты прав. А практически эта оптимизация в данном случае ничего не даст. Например, в последней версии: после того, как я обнулил used, я делаю forn(i, n) if (...), который работает сильно дольше.

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

Я примерно такими хаками занимался на РОИ 2010 в задаче 3 про самолеты) Пихал Куна вместо того, чтобы решать задачу)

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

Сдавать сюда удобнее.

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

Интересно, а я всю жизнь так искал паросочетание и, кстати, всего раз получил ТЛ когда паросочетанием задача-таки сдавалась с алгоритмом Хопкрофта-Карпа. Правда, этот ТЛ был на простом новогоднем контесте, так что не очень считается :) Я, кстати, пару раз пытался сравнить с Диницем и все разы оно (паросочетание Куном) быстрее работало.

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

Прохладная история про эту оптимизацию.

В ПТЗ как-то была задача (H здесь), которая, по-видимому, задумывалась авторами как простая. Условием была не очень сильно замаскированная просьба посчитать паросочетание в двудольном графе с такими ограничениями: в каждой доле не более 104 вершин, рёбер до 105, мультитест из не более чем 10 тестов, ограничений на сумму чего бы то ни было по всем тестам нет. Особую пикантность добавляло отсутствие тайм-лимита в условии, потому что "как на финале".

Реакцию участников на эту задачу можно оценить по монитору, обратив особенное внимание на зелёный плюсик в первые полчаса (КНУ сдали ровно то, что написано в исходном посте). andrewzta картинно возмущался на всю аудиторию, что мол как же так — задача про паросочетание, ищу Хопкрофт-Карпом, а оно не заходит! (И ещё он обещал обучить этому алгоритму, кажется, итмо 2 или 3.)

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

    задумывалась авторами как простая

    Насколько я помню (как один из авторов контеста =), задумывалась она, как "образовательная задача на Хопкрофта-Карпа"

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

      Авторское решение в этой задаче работает не быстрее парсоча, который ты привел в этом посте.

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

Я верно понимаю, что в последней из приведенных версий парсоча нет смысла в жадной инициализации? Я протестил на паре задач, к которым были тесты — нигде не улучшает больше, чем на погрешность.

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

    Да, ведь на первой итерации внешнего цикла у нас автоматически получится жадная инициализация.

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

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

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

        Возможно, это зависит от реализации ДФСа?..

        Но в моей — только длины 1. Мы не ищем чередующиеся цепи через вершины, ранее сочтенные на этом шагу, потому что они уже помечены "использованными" тогда, когда были сочтены.

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

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

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

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

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

      Хм, действительно, первый взгляд подвел, ничего автоматически не вытекает.

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

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

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

Еще есть эвристика в написании dfs'a. Вместо чего-то вроде

bool dfs(int v) {
    if (vis[v]) return false;
    vis[v] = true;
    for (int i = 0; i < (int)e[v].size(); i++) {
        if (mt[e[v][i]] == -1 || dfs(mt[e[v][i]])) {
            mt[e[v][i]] = v;
            return true;
        }
    }
    return false;
}

можно написать два одинаковых цикла: в первом поставить if (mt[e[v][i]] == -1), а во втором  –  if (dfs(mt[e[v][i]])). То есть, сначала проверить, нет ли ребра в вершину, не покрытую паросочетанием, и лишь если нет ни одной такой вершины, запускать dfs.

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

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

    Звучит здорово, ставлю лайк.

    Только по поводу "ускоряет в задаче sociology" есть вопрос.

    http://acm.math.spbu.ru/~sk1/problems/sociology_solutions.7z

    Содержит несколько решений Куном. В последнем sociology_sk_4.cpp реализована твоя идея.

    На большинстве тестов я вижу ускорение в 1.5-2 раза. Но на 32-м тесте решение без твоей идеи работает

    time = 3.06 cnt = 59 495 427

    А с ней

    time = 2.73 cnt = 92 825 806

    32-й — самый плохой для обоих решений тест.

    cnt — сколько суммарно вершин и рёбер мы перебрали за всё время работы dfs

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

      Она у нас залита в pcms, туда я и отправляю, тестов у меня нет.

      Без этой оптимизации на 32 тесте 0.703с, а макс  –  на 60, 0.984с.

      С ней  –  на 32 тесте 0.703с (макс), а на 60  –  0.671с.

      Код1, код2.

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

        А у меня на 60-м мгновенно.

        Я правильно понял, что разница в том, что у меня ещё и жадная инициализация, а у тебя она закомментирована?

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

          Нет, раскомментил жадную эвристику  –  время работы почти не изменилось в обоих случаях, без двух циклов так и работает ~1.0c на 60 тесте.

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

Кто нибудь знает кем был Кун? Или какое у него имя? Или откуда он?