Точки зчленування шукаються за допомогою пошуку в глибину. Для кожної вершини обчислюються дві мітки:
— час входу у вершину при обході в глибину;
— мінімальний час входу серед вершин, до яких можна потрапити з піддерева з коренем у , пройшовши по одному зворотному ребру.
Вершина , яка не є коренем дерева обходу, є точкою зчленування, якщо у неї є син у дереві обходу в глибину, для якого виконується нерівність up[to] \ge d[v]. Ця нерівність означає, що з піддерева з коренем у по зворотних ребрах не можна піднятися вище вершини . Тому при видаленні це піддерево виявиться відрізаним від решти графа.
Корінь дерева обходу є точкою зчленування тоді і тільки тоді, коли у нього в дереві обходу не менше двох синів.
Приклад
Граф з прикладу має такий вигляд:
Жирними лініями виділені ребра дерева обходу в глибину. Поряд з кожною вершиною вказані мітки d[v] / up[v]. Точки зчленування виділені кольором.
Вершина 1 — корінь дерева обходу. Вона є точкою зчленування, оскільки у неї в дереві обходу два сини: вершини і .
Вершина 2 — точка зчленування, оскільки для ребра дерева виконується up[6] \ge d[2]~(2 \ge 2).
Вершина 3 — точка зчленування, оскільки для ребра дерева виконується up[8] \ge d[3]~(3 \ge 3).
Реалізація алгоритму
Граф зберігається у вигляді списку суміжності . Масив позначає відвідані вершини, у масивах і зберігаються однойменні мітки. Змінна містить поточний час обходу в глибину. Знайдені точки зчленування збираються у множині : вона виключає повтори і зберігає вершини у порядку зростання.
vector<vector<int>> g;
vector<int> d, up, used;
set<int> ArtP;Функція dfs виконує пошук в глибину з вершини і попутно знаходить точки зчленування. Параметр — батько вершини у дереві обходу. Для кореня дерева p = -1, це значення за замовчуванням.
void dfs(int v, int p = -1)
{При вході у вершину позначаємо її як відвідану. Мітки і отримують поточне значення таймера, після чого таймер збільшується.
used[v] = true;
d[v] = up[v] = timer++;Змінна children рахує синів вершини у дереві обходу; це число знадобиться, якщо виявиться коренем.
int children = 0;Перебираємо вершини , суміжні з . Ребро (v, to) може бути одного з трьох видів:
ребро дерева, що переглядається у зворотному напрямку, до батька (to = p). Таке ребро пропускаємо;
зворотне ребро (вершина вже відвідана і to \ne p);
ребро дерева, що веде у ще не відвідану вершину .
for (int to : g[v])
{
if (to == p) continue;Якщо вершина вже відвідана, то (v, to) — зворотне ребро. Перераховуємо значення .
if (used[to])
up[v] = min(up[v], d[to]); // back edge
else
{Інакше (v, to) — ребро дерева. Запускаємо пошук в глибину з вершини , її батьком стає . Після повернення з рекурсії перераховуємо значення .
dfs(to, v);
up[v] = min(up[v], up[to]);Якщо up[to] \ge d[v] і вершина не є коренем (p \ne -1), то — точка зчленування: з піддерева з коренем у не можна піднятися вище , тому видалення відріже це піддерево від решти графа.
if (up[to] >= d[v] && p != -1) // некоренева вершина
ArtP.insert(v);Вершина — черговий син у дереві обходу, збільшуємо лічильник children.
children++;
}
}Корінь дерева обходу (p = -1) є точкою зчленування тоді і тільки тоді, коли у нього більше одного сина.
if (p == -1 && children > 1) // корінь DFS-дерева
ArtP.insert(v);
}Основна частина програми. Читаємо вхідні дані.
scanf("%d %d", &n, &m);
g.resize(n + 1);
for (int i = 0; i < m; i++)
{
scanf("%d %d", &a, &b);Граф неорієнтований, тому кожне ребро додаємо до списків суміжності обох його кінців.
g[a].push_back(b);
g[b].push_back(a);
}Виділяємо пам'ять під масиви , і .
d.resize(n + 1);
up.resize(n + 1);
used.resize(n + 1);Ініціалізуємо таймер і запускаємо пошук в глибину з кожної ще не відвіданої вершини: граф може бути незв'язним.
timer = 1;
for (int i = 1; i <= n; i++)
if (!used[i]) dfs(i);Виводимо кількість точок зчленування, а потім самі вершини. Множина впорядкована, тому вершини виводяться у порядку зростання.
printf("%d\n", ArtP.size());
for (int v : ArtP)
printf("%d\n", v);Java реалізація
import java.util.*;
public class Main
{
static ArrayList<Integer>[] g;
static int used[], d[], up[];
static int time;
static TreeSet<Integer> ArtPoints = new TreeSet<Integer>();
static void dfs (int v, int p)
{
int i, to, children;
used[v] = 1;
d[v] = up[v] = time++;
children = 0;
for (i = 0; i < g[v].size(); i++)
{
to = g[v].get(i);
if (to == p) continue;
if (used[to] == 1)
up[v] = Math.min(up[v], d[to]);
else
{
dfs (to, v);
up[v] = Math.min(up[v], up[to]);
if ((up[to] >= d[v]) && (p != -1)) ArtPoints.add(v);
children++;
}
}
if ((p == -1) && (children > 1)) ArtPoints.add(v);
}
@SuppressWarnings("unchecked")
public static void main(String[] args)
{
Scanner con = new Scanner(System.in);
int n = con.nextInt();
int m = con.nextInt();
g = new ArrayList[n+1]; // unchecked
used = new int[n+1];
d = new int[n+1];
up = new int[n+1];
for (int i = 0; i <= n; i++)
g[i] = new ArrayList<Integer>();
for (int i = 0; i < m; i++)
{
int a = con.nextInt();
int b = con.nextInt();
g[a].add(b);
g[b].add(a);
}
time = 1;
for(int i = 1; i <= n; i++)
if (used[i] == 0) dfs(i,-1);
System.out.println(ArtPoints.size());
for(int i : ArtPoints)
System.out.println(i);
con.close();
}
}Python реалізація
Збільшимо максимальну глибину рекурсії.
import sys
sys.setrecursionlimit(10**6)Граф зберігається у вигляді списку суміжності . Список позначає відвідані вершини, у списках і зберігаються однойменні мітки. Знайдені точки зчленування збираються у множині ArtPoints.
g = []
used = []
d = []
up = []
ArtPoints = set()Функція dfs виконує пошук в глибину з вершини і попутно знаходить точки зчленування. Параметр — батько вершини у дереві обходу; для кореня p = -1. Змінна children рахує синів вершини у дереві обходу.
def dfs(v, p = -1):
global t
children = 0При вході у вершину позначаємо її як відвідану. Мітки і отримують поточне значення , після чого збільшується.
used[v] = 1
d[v] = up[v] = t
t += 1Перебираємо вершини , суміжні з . Ребро (v, to) може бути одного з трьох видів:
ребро дерева, що переглядається у зворотному напрямку, до батька (to = p). Таке ребро пропускаємо;
зворотне ребро (вершина вже відвідана і to \ne p);
3. ребро дерева, що веде у ще не відвідану вершину .
for to in g[v]:
if to == p: continueЯкщо вершина вже відвідана, то (v, to) — зворотне ребро. Перераховуємо значення .
if used[to]:
up[v] = min(up[v], d[to])
else:Інакше (v, to) — ребро дерева. Запускаємо пошук в глибину з вершини , її батьком стає . Після повернення з рекурсії перераховуємо значення .
dfs(to, v)
up[v] = min(up[v], up[to])Якщо up[to] \ge d[v] і вершина не є коренем (p ≠ -1), то — точка зчленування.
if up[to] >= d[v] and p != -1:
ArtPoints.add(v)Вершина — черговий син у дереві обходу, збільшуємо лічильник children.
children += 1Корінь дерева обходу (p = -1) є точкою зчленування тоді і тільки тоді, коли у нього більше одного сина.
if p == -1 and children > 1:
ArtPoints.add(v)Основна частина програми. Створюємо списки і читаємо вхідні дані.
n, m = map(int, input().split())
g = [[] for _ in range(n + 1)]
used = [0] * (n + 1)
d = [0] * (n + 1)
up = [0] * (n + 1)Читаємо ребра неорієнтованого графа і заповнюємо списки суміжності.
for _ in range(m):
a, b = map(int, input().split())
g[a].append(b)
g[b].append(a)Запускаємо пошук в глибину з кожної ще не відвіданої вершини: граф може бути незв'язним.
t = 1
for i in range(1, n + 1):
if not used[i]: dfs(i)Виводимо кількість точок зчленування, а потім самі вершини у порядку зростання.
print(len(ArtPoints))
for x in sorted(ArtPoints):
print(x)