Топологічне сортування#1948

Топологічне сортування (Розбір)

Топологічне сортування орієнтованого графа — це така впорядкованість його вершин, що для кожного ребра u \to v вершина з'являється в послідовності раніше за вершину . Топологічне сортування існує тоді і тільки тоді, коли граф не містить орієнтованих циклів (тобто є ациклічним).

Розв'яжемо задачу за допомогою пошуку в глибину. Кожній вершині присвоюється колір:

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

Виявлення циклу. Якщо під час обходу ми зустрічаємо ребро, що веде до сірої вершини, граф містить цикл, і топологічне сортування неможливе — виводимо .

Складність. Кожна вершина та кожне ребро обробляються рівно один раз, тому час виконання становить O(n + m).

Приклад

Граф, наведений у прикладі, має такий вигляд:

Поряд з кожною вершиною ми розміщуємо мітки d[v] / f[v], де — час, коли вершина починає оброблятися, а — час, коли її обробка завершується. Топологічно відсортовані вершини розташовані в порядку спадання значень .

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

Оскільки кількість вершин у графі велика, зберігатимемо граф у вигляді списку суміжності . Для відстеження стану вершин використовуємо масив :

Під час виконання пошуку в глибину будемо додавати вершини до масиву у порядку завершення їх обробки. Після завершення роботи алгоритму масив міститиме вершини в оберненому порядку топологічного сортування.

vector<vector<int> > g;
vector<int> used, top;

Функція dfs виконує пошук в глибину, починаючи з вершини .

void dfs(int v)
{

Входимо у вершину та фарбуємо її в сірий колір.

  used[v] = 1;

Переберемо всі вершини , досяжні з вершини .

  for (int to : g[v])
  {

Якщо орієнтоване ребро (v, to) веде до сірої вершини, це вказує на наявність циклу в графі. У цьому випадку встановлюємо flag = 1.

    if (used[to] == 1) flag = 1;

Якщо вершину ще не відвідано, рекурсивно запускаємо пошук в глибину з неї.

    if (used[to] == 0) dfs(to);
  }

Завершуємо обробку вершини . Фарбуємо її в чорний колір і додаємо в кінець масиву .

  used[v] = 2;
  top.push_back(v);
}

Основна частина програми. Зчитуємо вхідні дані та будуємо список суміжності графа.

scanf("%d %d",&n,&m);
g.resize(n+1); used.resize(n+1);

for(i = 0; i < m; i++)
{
  scanf("%d %d",&a,&b);
  g[a].push_back(b);
}

Виконуємо пошук в глибину на орієнтованому графі.

flag = 0;
for(i = 1; i <= n; i++)
  if (used[i] == 0) dfs(i);

Якщо під час пошуку в глибину виявлено цикл (flag = 1), виводимо .

if (flag == 1) printf("-1");
else

Інакше виводимо вершини графа в оберненому порядку їх додавання до масиву .

  for(i = top.size() - 1; i >= 0; i--)
    printf("%d ",top[i]);

printf("\n");

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

Зберігаємо вхідний граф у списку суміжності .

Вхідні степені вершин зберігаємо в масиві InDegree.

Результат топологічного сортування зберігається в масиві .

vector<vector<int> > g;
vector<int> InDegree, top;
deque<int> q;

Зчитуємо кількість вершин та кількість ребер .

scanf("%d %d",&n,&m);
g.resize(n+1);
InDegree.resize(n+1);

Зчитуємо ребер графа.

for(i = 0; i < m; i++)
{
  scanf("%d %d",&a,&b);
  g[a].push_back(b);

Для кожного ребра збільшуємо InDegree[b] на .

  InDegree[b]++;
}

Усі вершини з нульовим вхідним степенем додаються до черги .

for(i = 1; i < InDegree.size(); i++)
  if (!InDegree[i]) q.push_back(i);

Продовжуємо виконання алгоритму, поки черга не стане порожньою.

while(!q.empty())
{

Вилучаємо вершину з черги та додаємо її в кінець списку топологічного порядку.

  v = q.front(); q.pop_front();
  top.push_back(v);

Видаляємо ребра (v, to) з графа. Для кожного такого ребра зменшуємо вхідний степінь вершини .

  for (int to : g[v])
  {
    InDegree[to]--;

Якщо вхідний степінь вершини стає нульовим, додаємо її до черги. Потім вона буде розміщена в списку топологічного порядку.

    if (!InDegree[to]) q.push_back(to);
  }
}

Якщо не всі вершин додано до масиву , тоді граф містить цикл, і топологічне сортування неможливе.

if (top.size() < n) 
  printf("-1\n");
else
{

Інакше виводимо вершини графа в топологічному порядку.

  for (int x : top) printf("%d ", x);
  printf("\n");
}

Реалізація мовою Java — пошук в глибину

import java.util.*;
import java.io.*;

public class Main
{
  static ArrayList<ArrayList<Integer>> g;
  static ArrayList<Integer> top = new ArrayList<Integer>();
  static int used[];
  static int n, m, flag = 0;
    
  static void dfs(int v)
  {
    used[v] = 1;
    for(int i = 0; i < g.get(v).size(); i++)
    {
      int to = g.get(v).get(i);
      if (used[to] == 1) flag = 1;
      if (used[to] == 0) dfs(to);
    }
    used[v] = 2;
    top.add(v);
  }
    
  public static void main(String[] args)
  {
    FastScanner con = new FastScanner(System.in);
    n = con.nextInt();
    m = con.nextInt();
    g = new ArrayList<ArrayList<Integer>>();
    used = new int[n+1];
      
    for (int i = 0; i <= n; i++)
      g.add(new ArrayList<Integer>());

    for (int i = 0; i < m; i++)
    {
      int a = con.nextInt();
      int b = con.nextInt();     
      g.get(a).add(b);
    }
      
    for(int i = 1; i <= n; i++)
      if (used[i] == 0) dfs(i);
      
    if (flag == 1) System.out.println("-1");
    else
      for(int i = top.size() - 1; i >= 0; i--)
        System.out.print(top.get(i) + " ");
    System.out.println();
  }
}	

class FastScanner
{
  BufferedReader br;
  StringTokenizer st;
 
  public FastScanner(InputStream inputStream)
  {
    br = new BufferedReader(new InputStreamReader(inputStream));
    st = new StringTokenizer("");
  }
 
  public String next()
  {
    while (!st.hasMoreTokens())
    {
      try
      {
        st = new StringTokenizer(br.readLine());
      } catch (Exception e)
      {
        return null;
      }
    }
    return st.nextToken();
  }
 
  public int nextInt()
  {
    return Integer.parseInt(next());
  }
}

Реалізація мовою Java — Кана

import java.util.*;
import java.io.*;

public class Main
{
  static ArrayList<ArrayList<Integer>> g;
  static ArrayList<Integer> top;
  static int InDegree[];
  static int n, m, flag = 0;
  
  public static void main(String[] args)
  {
    FastScanner con = new FastScanner(System.in);
    n = con.nextInt();
    m = con.nextInt();
    g = new ArrayList<ArrayList<Integer>>();
    InDegree = new int[n+1];
      
    for (int i = 0; i <= n; i++)
      g.add(new ArrayList<Integer>());

    for (int i = 0; i < m; i++)
    {
      int a = con.nextInt();
      int b = con.nextInt();     
      g.get(a).add(b);
      InDegree[b]++;
    }
      
    Queue<Integer> q = new LinkedList<Integer>();
    for(int i = 1; i <= n; i++)
      if (InDegree[i] == 0) q.add(i);

    top = new ArrayList<Integer>();
    while (!q.isEmpty())
    {
      int v = q.poll();
      top.add(v);
      for (int to : g.get(v))
      {
        InDegree[to]--;
        if (InDegree[to] == 0) q.add(to);
      }
    }

    if (top.size() < n)
    	System.out.println("-1");
      else
      {
        for (int x : top) System.out.print(x + " ");
        System.out.println();
      }
  }
}	

class FastScanner
{
  BufferedReader br;
  StringTokenizer st;
 
  public FastScanner(InputStream inputStream)
  {
    br = new BufferedReader(new InputStreamReader(inputStream));
    st = new StringTokenizer("");
  }
 
  public String next()
  {
    while (!st.hasMoreTokens())
    {
      try
      {
        st = new StringTokenizer(br.readLine());
      } catch (Exception e)
      {
        return null;
      }
    }
    return st.nextToken();
  }
 
  public int nextInt()
  {
    return Integer.parseInt(next());
  }
}

Реалізація мовою Python — пошук в глибину

Збільшуємо глибину рекурсії.

import sys
sys.setrecursionlimit(10000)

Функція dfs виконує пошук в глибину, починаючи з вершини .

def dfs(v):

Входимо у вершину та фарбуємо її в сірий колір.

  used[v] = 1

Переберемо всі вершини , досяжні з вершини .

  for to in g[v]:

Якщо орієнтоване ребро (v, to) веде до сірої вершини, це вказує на наявність циклу в графі. У цьому випадку встановлюємо flag = 1.

    if used[to] == 1:
      global flag
      flag = 1

Якщо вершину ще не відвідано, рекурсивно запускаємо пошук в глибину з неї.

    if used[to] == 0:
      dfs(to)

Завершуємо обробку вершини . Фарбуємо її в чорний колір і додаємо в кінець масиву .

  used[v] = 2
  top.append(v)

Основна частина програми. Зчитуємо кількість вершин та кількість ребер .

n, m = map(int, input().split())

Зберігаємо вхідний граф у списку суміжності . Оголошуємо список .

g = [[] for _ in range(n + 1)]
used = [0] * (n + 1)

Зчитуємо список ребер і будуємо список суміжності графа.

for _ in range(m):
  a, b = map(int, input().split())
  g[a].append(b)

Виконуємо пошук в глибину на орієнтованому графі.

flag = 0
top = []
for i in range(1, n + 1):
  if used[i] == 0: dfs(i)

Якщо під час пошуку в глибину виявлено цикл (flag = 1), виводимо .

if flag == 1:
  print("-1")
else:

Інакше виводимо вершини графа в оберненому порядку їх додавання до масиву .

  for i in range(len(top) - 1, -1, -1):
    print(top[i], end=" ")
  print()

Реалізація мовою Python — Кана

Оголошуємо чергу .

from collections import deque
q = deque()

Зчитуємо кількість вершин та кількість ребер .

n, m = map(int, input().split())

Зберігаємо вхідний граф у списку суміжності .

Вхідні степені вершин зберігаємо в списку InDegree.

Результат топологічного сортування зберігається в списку .

g = [[] for _ in range(n + 1)]
InDegree = [0] * (n + 1)
top = []

Зчитуємо ребер графа.

for _ in range(m):
  a, b = map(int, input().split())
  g[a].append(b)

Для кожного ребра збільшуємо InDegree[b] на .

  InDegree[b] += 1

Усі вершини з нульовим вхідним степенем додаються до черги .

for i in range(1, len(InDegree)):
  if not InDegree[i]: q.append(i)

Продовжуємо виконання алгоритму, поки черга не стане порожньою.

while q:

Вилучаємо вершину з черги та додаємо її в кінець списку топологічного порядку.

  v = q.popleft()
  top.append(v)

Видаляємо ребра (v, to) з графа. Для кожного такого ребра зменшуємо вхідний степінь вершини .

  for to in g[v]:
    InDegree[to] -= 1

Якщо вхідний степінь вершини стає нульовим, додаємо її до черги. Потім вона буде розміщена в списку топологічного порядку.

    if not InDegree[to]: q.append(to)

Якщо не всі вершин додано до масиву , тоді граф містить цикл, і топологічне сортування неможливе.

if len(top) < n:
  print("-1")
else:

Інакше виводимо вершини графа в топологічному порядку.

  for i in top:
    print(i, end=" ")
  print()