E031. Поиск мостов в режиме онлайн

e-maxx algorithm original: C/C++ #algorithm #connectivity #emaxx #graph #search #tree
题目文本会按所选界面语言从俄语翻译;代码保持不变。

Источник: e-maxx.ru/algo, страница PDF 96.

Пусть дан неориентированный 图. Мостом называется такое edge, удаление которого делает 图 несвязным (или, точнее, увеличивает number компонент связности). it is required find все мосты в заданном 图е. Неформально эта 题目 ставится следующим образом: it is required find на заданной карте дорог все "важные" дороги, т.е. такие дороги, что удаление любой из них приведёт к исчезновению пути между какой-то парой городов. Описываемый здесь 算法 является онлайновым, что означает, что 输入ной 图 не является известным заранее, а рёбра в него добавляются по одному, и после каждого такого добавления 算法 пересчитывает все мосты в текущем 图е. Иными словами, 算法 предназначен для эффективной работы на динамическом, изменяющемся 图е. Более строго, постановка задачи следующая. Изначально 图 пустой и состоит из

вершин. Затем

поступают запросы, каждый из которых — это пара вершин

, которые обозначают edge, добавляемое в

图. it is required после каждого запроса, т.е. после добавления каждого ребра, выводить текущее количество мостов в 图е. (При желании можно поддерживать и список всех рёбер-мостов, а также явно поддерживать

компоненты рёберной двусвязности.)

Описываемый ниже 算法 работает за время

, где

— number запросов. 算法 основан

на структуре данных "Disjoint set union".

Приведённая 实现 算法а, впрочем, работает за время

, поскольку использует

в одном месте упрощённую версию системы непересекающихся множеств без ранговой эвристики.

算法

Известно, что рёбра-мосты разбивают вершины 图а на компоненты, называемые компонентами рёберной двусвязности. Если каждую компоненту рёберной двусвязности сжать в одну вершину, и оставить только рёбра- мосты между этими компонентами, то получится ациклический 图, т.е. лес. Описываемый ниже 算法 поддерживает в явном виде этот лес компонент рёберной двусвязности.

Понятно, что изначально, когда 图 пустой, он содержит

компонент рёберной двусвязности, не связанных никак

между собой.

При добавлении очередного ребра

может возникнуть три ситуации:

● Оба конца

и

находятся в одной и той же компоненте рёберной двусвязности — тогда это edge не является мостом, и ничего не меняет в структуре леса, поэтому просто пропускаем это edge. Таким образом, в этом случае number мостов не меняется.

● Вершины

и

находятся в разных компонентах связности, т.е. соединяют два дерева. В этом случае edge становится новым мостом, а эти два дерева объединяются в одно (а все старые мосты остаются). Таким образом, в этом случае number мостов увеличивается на единицу.

● Вершины

и

находятся в одной компоненте связности, но в разных компонентах рёберной двусвязности. В этом случае это edge образует цикл вместе с некоторыми из старых мостов. Все эти мосты перестают быть мостами, а образовавшийся цикл надо объединить в новую компоненту рёберной двусвязности. Таким образом, в этом случае number мостов уменьшается на два или более. Следовательно, вся 题目 сводится к эффективной реализации всех этих операций над лесом компонент.

Структуры данных для хранения леса

Всё, что нам понадобится из структур данных, — это Disjoint set union. На самом деле, нам понадобится делать два экземпляра этой структуры: одна будет для поддержания компонент связности, другая — для поддержания компонент рёберной двусвязности. Кроме того, для хранения структуры деревьев в лесу компонент двусвязности для каждой вершины будем

хранить указатель

на её предка в дереве. Будем теперь последовательно разбирать каждую операцию, которую нам надо научиться реализовывать:

● Проверка, лежат ли две указанные вершины в одной компоненте

связности/двусвязности. Делается обычным запросом к структуре "Disjoint set union".

● Соединение двух деревьев в одно по некоторому ребру

. Поскольку могло получиться, что

ни vertex

, ни vertex

не являются корнями своих деревьев, то единственный способ соединить эти два дерева — переподвесить одно из них. На示例, можно переподвесить одно 树 за вершину

, и затем

присоединить это к другому дереву, сделав вершину

дочерней к

. Однако встаёт вопрос об эффективности операции переподвешивания: чтобы переподвесить 树 с корнем в

за вершину

, надо пройти по пути из

в

, перенаправляя указатели

в обратную сторону, а также меняя ссылки

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

Таким образом, стоимость операции переподвешивания есть

, где

— высота дерева. Можно оценить её

ещё выше, сказав, что это есть величина

, где

— number вершин в дереве. Применим теперь такой стандартный приём: скажем, что из двух деревьев переподвешивать будем то, в котором меньше вершин. Тогда интуитивно понятно, что худший случай — когда объединяются два дерева 示例но равного размера, но тогда в результате получается 树 вдвое большего размера, что не позволяет такой ситуации происходить много раз. Формально это можно записать в виде рекуррентного соотношения:

где через

мы обозначили number операций, необходимое для получения дерева из

вершин с помощью

операций переподвешивания и объединения деревьев. Это известное рекуррентное соотношение, и оно имеет

解法

. Таким образом, суммарное время, затрачиваемое на всех переподвешивания, составит

, если мы

всегда будем переподвешивать меньшее из двух 树. Нам придётся поддерживать размеры каждой компоненты связности, но структура данных "Disjoint set union" позволяет делать это без труда.

● Поиск цикла, образуемого добавлением нового ребра

в какое-то 树. Фактически это означает, что

нам надо find наименьшего общего предка (LCA) вершин

и

. Заметим, что потом мы сожмём все вершины обнаруженного цикла в одну вершину, поэтому нас устроит любой 算法а поиска LCA, работающий за время порядка его длины. Поскольку вся информация о структуре дерева, которая у нас есть, — это ссылки

на предков, то

единственно возможным представляется следующий 算法 поиска LCA: помечаем вершины

и

как

посещённые, затем переходим к их предкам

и

и помечаем их, потом к их предкам, и так далее, пока

не случится, что хотя бы одна из двух текущих вершин уже помечена. Это будет означать, что текущая vertex — и есть искомый LCA, и надо будет заново повторить путь до неё от вершины

и от вершины

— тем самым мы

найдём искомый цикл. Очевидно, что этот 算法 работает за время порядка длины искомого цикла, поскольку каждый из двух указателей не мог пройти расстояние, большее этой длины.

● Сжатие цикла, образуемого добавлением нового ребра

в какое-то 树. Нам it is required создать новую компоненту рёберной двусвязности, которая будет состоять из всех вершин обнаруженного цикла (понятно, что обнаруженный цикл сам мог состоять из каких-то компонент двусвязности, но это ничего не меняет). Кроме того, надо произвести сжатие таким образом, чтобы не нарушилась структура дерева, и

все указатели

и две системы непересекающихся множеств были корректными. Самый простой способ добиться этого — сжать все вершины найденного цикла в их LCA. В самом деле, vertex-LCA — это самая высокая из сжимаемых вершин, т.е. её

остаётся без изменений. Для

всех остальных сжимаемых вершин обновлять тоже ничего не надо, поскольку эти вершины просто перестают существовать — в системе непересекающихся множеств для компонент двусвязности все эти вершины будут просто указывать на вершину-LCA. Но тогда получится, что Disjoint set union для компонент двусвязности работает без эвристики объединения по рангу: если мы всегда присоединяем вершины цикла к их LCA, то этой эвристике нет места.

В этом случае в асимптотике возникнет

, поскольку без эвристики по рангу любая операция с

системой непересекающихся множеств работает именно за такое время.

Для достижения асимптотики

на один запрос необходимо объединять вершины цикла

согласно ранговой эвристике, а затем присвоить

нового лидера в

.

实现

Приведём здесь итоговую реализацию всего 算法а. В целях простоты Disjoint set union для компонент двусвязности написана без

ранговой эвристики, поэтому итоговая Asymptotic complexity составит

на запрос в среднем. (О том,

как достичь асимптотики

, написано выше в пункте "Сжатие цикла".)

Также в данной реализации не хранятся сами рёбра-мосты, а хранится только их количество — см. переменная

. Впрочем, при желании не составит никакого труда завести

из всех мостов.

Изначально следует вызвать функцию

, которая инициализирует две системы непересекающихся

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

предков

.

Основная функция — это

, которая обрабатывает запрос на добавление нового ребра.

Константе

следует задать значение, равное максимально возможному количеству вершин во 输入ном 图е. Более подробные пояснения к данной реализации см. ниже.

const int MAXN = ...;

int n, bridges, par[MAXN], bl[MAXN], comp[MAXN], size[MAXN];

void init() {

for (int i=0; i<n; ++i) {

bl[i] = comp[i] = i;

size[i] = 1;

par[i] = -1;

}

bridges = 0;

}

int get (int v) {
if (v==-1)  return -1;
return bl[v]==v ? v : bl[v]=get(bl[v]);

}

int get_comp (int v) {

v = get(v);

return comp[v]==v ? v : comp[v]=get_comp(comp[v]);

}

void make_root (int v) {

v = get(v);

int root = v,

child = -1;

while (v != -1) {
int p = get(par[v]);

par[v] = child;

comp[v] = root;

child=v; v=p;

}

size[root] = size[child];

}

int cu, u[MAXN];

void merge_path (int a, int b) {

++cu;

vector<int> va, vb;

int lca = -1;

for(;;) {

if (a != -1) {

a = get(a);

va.pb (a);

if (u[a] == cu) {

lca = a;

break;

}

u[a] = cu;

a = par[a];

}

if (b != -1) {

b = get(b);

vb.pb (b);

if (u[b] == cu) {

lca = b;

break;

}

u[b] = cu;

b = par[b];

} }

for (size_t i=0; i<va.size(); ++i) {

bl[va[i]] = lca;

if (va[i] == lca)  break;

--bridges;

}

for (size_t i=0; i<vb.size(); ++i) {

bl[vb[i]] = lca;

if (vb[i] == lca)  break;

--bridges;

} }

void add_edge (int a, int b) {

a = get(a); b = get(b);

if (a == b)  return;
int ca = get_comp(a),

cb = get_comp(b);

if (ca != cb) {

++bridges;

if (size[ca] > size[cb]) {

swap (a, b);

swap (ca, cb);

}

make_root (a);

par[a] = comp[a] = b;

size[cb] += size[a];

}

else

merge_path (a, b);

} Прокомментируем код более подробно. Disjoint set union для компонент двусвязности хранится в

数组е

, а функция, возвращающая лидера компоненты двусвязности — это

. Эту функцию

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

корректные данные (предок

, предок в системе непересекающихся множеств для компонент связности, и т.д.). Disjoint set union для компонент связности хранится в

数组е

, также есть дополнительный 数组

для хранения размеров компонент.

Функция

returns лидера компоненты связности (который на самом деле является корнем дерева).

Функция переподвешивания дерева

работает, как и было описано выше: она идёт

от вершины

по предкам до корня, каждый раз перенаправляя предка

в обратную сторону (вниз, по направлению

к вершине

). Также обновляется указатель

в системе непересекающихся множеств для компонент

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

,

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

Функция обнаружения и сжатия пути

, как и было описано выше, ищет

LCA вершин

и

, для чего поднимается от них параллельно вверх, пока какая-то vertex не встретится во второй раз. В целях эффективности пройденные вершины помечаются с помощью техники "numberвого used", что работает за

вместо Applications

. Пройденные пути сохраняются в векторах

и

, чтобы потом пройтись по ним

второй раз до LCA, получив тем самым все вершины цикла. Все вершины цикла сжимаются, путём присоединения их

к LCA (здесь возникает Asymptotic complexity

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

количество отнимается от

).

Наконец, функция обработки запросов

определяет компоненты связности, в

которых лежат вершины

и

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

и

лежат

в одном дереве, но в разных компонентах двусвязности, то вызывается функция

,

которая обнаружит цикл и сожмёт его в одну компоненту двусвязности.

C# 解法

自动草稿,提交前请检查
using System;
using System.Collections.Generic;
using System.Linq;

public static class AlgorithmDraft
{
    // Auto-generated C# draft from the original e-maxx C/C++ listing. Review before production use.
    const int MAXN = ...;
    int n, bridges, par[MAXN], bl[MAXN], comp[MAXN], size[MAXN];
    void init() {
            for (int i=0; i<n; ++i) {
                    bl[i] = comp[i] = i;
                    size[i] = 1;
                    par[i] = -1;
            }
            bridges = 0;
    }
    int get (int v) {
            if (v==-1)  return -1;
            return bl[v]==v ? v : bl[v]=get(bl[v]);
    }
    int get_comp (int v) {
            v = get(v);
            return comp[v]==v ? v : comp[v]=get_comp(comp[v]);
    }
    void make_root (int v) {
            v = get(v);
            int root = v,
                    child = -1;
            while (v != -1) {
                    int p = get(par[v]);
                    par[v] = child;
                    comp[v] = root;
                    child=v;  v=p;
            }
            size[root] = size[child];
    }
    int cu, u[MAXN];
    void merge_path (int a, int b) {
            ++cu;
            List<int> va, vb;
            int lca = -1;
            for(;;) {
                    if (a != -1) {
                            a = get(a);
                            va.pb (a);
                            if (u[a] == cu) {
                                    lca = a;
                                    break;
                            }
                            u[a] = cu;
                            a = par[a];
                    }
                    if (b != -1) {
                            b = get(b);
                            vb.pb (b);
                            if (u[b] == cu) {
                                    lca = b;
                                    break;
                            }
                            u[b] = cu;
                            b = par[b];
                    }
            }
            for (size_t i=0; i<va.size(); ++i) {
                    bl[va[i]] = lca;
                    if (va[i] == lca)  break;
                    --bridges;
            }
            for (size_t i=0; i<vb.size(); ++i) {
                    bl[vb[i]] = lca;
                    if (vb[i] == lca)  break;
                    --bridges;
            }
    }
    void add_edge (int a, int b) {
            a = get(a);   b = get(b);
            if (a == b)  return;
            int ca = get_comp(a),
                    cb = get_comp(b);
            if (ca != cb) {
                    ++bridges;
                    if (size[ca] > size[cb]) {
                            swap (a, b);
                            swap (ca, cb);
                    }
                    make_root (a);
                    par[a] = comp[a] = b;
                    size[cb] += size[a];
            }
            else
                    merge_path (a, b);
    }
}

C++ 解法

匹配/原始
const int MAXN = ...;
int n, bridges, par[MAXN], bl[MAXN], comp[MAXN], size[MAXN];
void init() {
        for (int i=0; i<n; ++i) {
                bl[i] = comp[i] = i;
                size[i] = 1;
                par[i] = -1;
        }
        bridges = 0;
}
int get (int v) {
        if (v==-1)  return -1;
        return bl[v]==v ? v : bl[v]=get(bl[v]);
}
int get_comp (int v) {
        v = get(v);
        return comp[v]==v ? v : comp[v]=get_comp(comp[v]);
}
void make_root (int v) {
        v = get(v);
        int root = v,
                child = -1;
        while (v != -1) {
                int p = get(par[v]);
                par[v] = child;
                comp[v] = root;
                child=v;  v=p;
        }
        size[root] = size[child];
}
int cu, u[MAXN];
void merge_path (int a, int b) {
        ++cu;
        vector<int> va, vb;
        int lca = -1;
        for(;;) {
                if (a != -1) {
                        a = get(a);
                        va.pb (a);
                        if (u[a] == cu) {
                                lca = a;
                                break;
                        }
                        u[a] = cu;
                        a = par[a];
                }
                if (b != -1) {
                        b = get(b);
                        vb.pb (b);
                        if (u[b] == cu) {
                                lca = b;
                                break;
                        }
                        u[b] = cu;
                        b = par[b];
                }
        }
        for (size_t i=0; i<va.size(); ++i) {
                bl[va[i]] = lca;
                if (va[i] == lca)  break;
                --bridges;
        }
        for (size_t i=0; i<vb.size(); ++i) {
                bl[vb[i]] = lca;
                if (vb[i] == lca)  break;
                --bridges;
        }
}
void add_edge (int a, int b) {
        a = get(a);   b = get(b);
        if (a == b)  return;
        int ca = get_comp(a),
                cb = get_comp(b);
        if (ca != cb) {
                ++bridges;
                if (size[ca] > size[cb]) {
                        swap (a, b);
                        swap (ca, cb);
                }
                make_root (a);
                par[a] = comp[a] = b;
                size[cb] += size[a];
        }
        else
                merge_path (a, b);
}

Java 解法

自动草稿,提交前请检查
import java.util.*;
import java.math.*;

public class AlgorithmDraft {
    // Auto-generated Java draft from the original e-maxx C/C++ listing. Review before production use.
    const int MAXN = ...;
    int n, bridges, par[MAXN], bl[MAXN], comp[MAXN], size[MAXN];
    void init() {
            for (int i=0; i<n; ++i) {
                    bl[i] = comp[i] = i;
                    size[i] = 1;
                    par[i] = -1;
            }
            bridges = 0;
    }
    int get (int v) {
            if (v==-1)  return -1;
            return bl[v]==v ? v : bl[v]=get(bl[v]);
    }
    int get_comp (int v) {
            v = get(v);
            return comp[v]==v ? v : comp[v]=get_comp(comp[v]);
    }
    void make_root (int v) {
            v = get(v);
            int root = v,
                    child = -1;
            while (v != -1) {
                    int p = get(par[v]);
                    par[v] = child;
                    comp[v] = root;
                    child=v;  v=p;
            }
            size[root] = size[child];
    }
    int cu, u[MAXN];
    void merge_path (int a, int b) {
            ++cu;
            ArrayList<Integer> va, vb;
            int lca = -1;
            for(;;) {
                    if (a != -1) {
                            a = get(a);
                            va.pb (a);
                            if (u[a] == cu) {
                                    lca = a;
                                    break;
                            }
                            u[a] = cu;
                            a = par[a];
                    }
                    if (b != -1) {
                            b = get(b);
                            vb.pb (b);
                            if (u[b] == cu) {
                                    lca = b;
                                    break;
                            }
                            u[b] = cu;
                            b = par[b];
                    }
            }
            for (size_t i=0; i<va.size(); ++i) {
                    bl[va[i]] = lca;
                    if (va[i] == lca)  break;
                    --bridges;
            }
            for (size_t i=0; i<vb.size(); ++i) {
                    bl[vb[i]] = lca;
                    if (vb[i] == lca)  break;
                    --bridges;
            }
    }
    void add_edge (int a, int b) {
            a = get(a);   b = get(b);
            if (a == b)  return;
            int ca = get_comp(a),
                    cb = get_comp(b);
            if (ca != cb) {
                    ++bridges;
                    if (size[ca] > size[cb]) {
                            swap (a, b);
                            swap (ca, cb);
                    }
                    make_root (a);
                    par[a] = comp[a] = b;
                    size[cb] += size[a];
            }
            else
                    merge_path (a, b);
    }
}

Материал разбит как 算法ическая 题目: изучить постановку, понять асимптотику и реализовать 算法 на выбранном языке.

Vacancies for this task

活跃职位 with overlapping task tags are 已显示.

所有职位
目前还没有活跃职位。