Точки зчленування#1945

Точки зчленування (Розбір)

Точки зчленування шукаються за допомогою пошуку в глибину. Для кожної вершини обчислюються дві мітки:

Вершина , яка не є коренем дерева обходу, є точкою зчленування, якщо у неї є син у дереві обходу в глибину, для якого виконується нерівність up[to] \ge d[v]. Ця нерівність означає, що з піддерева з коренем у по зворотних ребрах не можна піднятися вище вершини . Тому при видаленні це піддерево виявиться відрізаним від решти графа.

Корінь дерева обходу є точкою зчленування тоді і тільки тоді, коли у нього в дереві обходу не менше двох синів.

Приклад

Граф з прикладу має такий вигляд:

Жирними лініями виділені ребра дерева обходу в глибину. Поряд з кожною вершиною вказані мітки d[v] / up[v]. Точки зчленування виділені кольором.

Реалізація алгоритму

Граф зберігається у вигляді списку суміжності . Масив позначає відвідані вершини, у масивах і зберігаються однойменні мітки. Змінна містить поточний час обходу в глибину. Знайдені точки зчленування збираються у множині : вона виключає повтори і зберігає вершини у порядку зростання.

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) може бути одного з трьох видів:

  1. ребро дерева, що переглядається у зворотному напрямку, до батька (to = p). Таке ребро пропускаємо;

  2. зворотне ребро (вершина вже відвідана і to \ne p);

  3. ребро дерева, що веде у ще не відвідану вершину .

  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) може бути одного з трьох видів:

  1. ребро дерева, що переглядається у зворотному напрямку, до батька (to = p). Таке ребро пропускаємо;

  2. зворотне ребро (вершина вже відвідана і to \ne p);

  3. 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)