Topologický algoritmus řazení: Python, C++ Příklad

⚡ Chytré shrnutí

Topologické řazení seřazuje uzly orientovaného acyklického grafu tak, aby se každý uzel objevil před těmi, na které ukazuje, a to pomocí Kahnova algoritmu k opakovanému výběru uzlů s nulovým stupněm vnitřního kolísání.

  • 📐 Definice: Topologické třídění vytváří lineární uspořádání vrcholů DAG, kde každá orientovaná hrana (u, v) má u před v.
  • 🔁 Kahnův algoritmus: Opakovaně vybírejte uzel s nulovými příchozími hranami, přidávejte ho do pořadí a snižujte stupeň vstupu jeho sousedů.
  • 🚫 Zablokované cykly: Graf obsahující cyklus nelze topologicky seřadit, protože žádný uzel uvnitř cyklu nikdy nedosáhne nulového stupně independence.
  • 💻 Code: C++ a Python Implementace používají frontu plus pole vnitřních stupňů k výpočtu pořadí v čase O(V + E).
  • 📊 Složitost: Časová složitost je O(V + E) a prostorová složitost je O(V), kde V je počet vrcholů a E je počet hran.
  • 🛠️ Aplikace: Plánování úloh a sestavení, řešení závislostí balíčků (apt, npm), detekce deadlocků a předpoklady kurzu – to vše využívá topologické pořadí.

Algoritmus topologického řazení

Co je topologický třídicí algoritmus?

Topologické třídění je také známé jako Kahnův algoritmus a je oblíbeným třídicím algoritmem. Pomocí orientovaného grafu jako vstupu topologické řazení seřadí uzly tak, aby se každý objevil před tím, na který ukazuje.

Tento algoritmus se aplikuje na DAG (Directed Acyclic Graph) tak, že každý uzel se v uspořádaném poli objeví před všemi ostatními uzly, na které ukazuje. Tento algoritmus opakovaně dodržuje určitá pravidla, dokud není řazení dokončeno.

Pro zjednodušení se podívejte na následující příklad:

Režírovaný graf

Režírovaný graf

Zde vidíme, že „A“ nemá vnitřní stupeň. Vnitřní stupeň znamená hranu, která ukazuje na uzel. „B“ a „C“ mají předpoklad „A“, zatímco „E“ má předpoklad uzlů „D“ a „F“. Některé z uzlů jsou závislé na jiných uzlech.

Zde je další znázornění výše uvedeného grafu:

Závislost každého uzlu

Závislost každého uzlu (lineární řazení)

Když tedy předáme DAG (Directed Acyclic Graph) topologickému řazení, dostaneme pole s lineárním uspořádáním, kde první prvek nemá žádnou závislost.

Algoritmus topologického řazení

Zde jsou kroky, jak to provést:

Krok 1) Najděte uzel s nulovými vstupními hranami, uzel s nulovými stupni.

Krok 2) Uložte tento uzel s nulovým stupněm vstupu do fronty nebo zásobníku a odeberte jej z grafu.

Krok 3) Pak odstraňte odchozí hranu z daného uzlu. Tím se sníží počet vstupních stupňů pro další uzel.

Topologické uspořádání vyžaduje, aby datová struktura grafu neobsahovala žádný cyklus. Graf bude považován za DAG, pokud splňuje tyto požadavky:

  • Jeden nebo více uzlů s nestupňovou hodnotou nula.
  • Graf neobsahuje žádný cyklus.

Dokud jsou v grafu uzly a graf je stále DAG, provedeme výše uvedené tři kroky. Jinak algoritmus spadne do cyklické závislosti a Kahnův algoritmus nebude schopen najít uzel s nulovým stupněm vnějšího gradientu.

Jak funguje topologické řazení

Zde pro topologické třídění použijeme „Kahnův algoritmus“. Řekněme, že máme následující graf:

Topologické řazení

Zde jsou kroky pro Kahnova algoritmu:

Krok 1) Vypočítejte nestupeň nebo vstupní hranu všech uzlů v grafu.

Poznámka:

  • Indegree znamená nasměrované hrany směřující k uzlu.
  • Outdegree znamená směrované hrany, které pocházejí z uzlu.

Zde je vnitřní a vnější stupeň výše uvedeného grafu:

Vnitřní a vnější stupeň

Krok 2) Najděte uzel s nulovým stupněm inputu nebo nulovou hranou směřující do něj. Uzel s nulovým stupněm inputu znamená, že k tomuto uzlu nepřicházejí žádné hrany. Uzel „A“ má nulový stupeň inputu, což znamená, že žádná hrana nesměřuje do uzlu „A“. Provedeme tedy následující akce:

  • Odstraňte tento uzel a jeho vnější hrany (výstupní hrany).
  • Umístěte uzel do fronty pro objednání.
  • Aktualizujte počet stupňů sousedního uzlu „A“.

Topologické řazení

Krok 3) Potřebujeme najít uzel s nulovým indegree. V tomto příkladu mají uzly „B“ a „C“ nulový indegree. Zde můžeme vzít kterýkoli z těchto dvou. Vezměme „B“ a smažme ho z grafu. Poté aktualizujme hodnoty indegree ostatních uzlů. Po provedení těchto operací bude náš graf a fronta vypadat následovně:

Topologické řazení

Krok 4) Uzel „C“ nemá žádnou vstupní hranu. Takže uzel „C“ z grafu odstraníme a vložíme ho do fronty. Můžeme také smazat hranu, která z „C“ vychází. Náš graf bude nyní vypadat takto:

Topologické řazení

Krok 5) Vidíme, že uzly „D“ a „F“ mají nulový vstupní stupeň. Vezmeme jeden uzel a vložíme ho do fronty. Nejprve odebereme „D“. Počet vstupních stupňů pro uzel „E“ bude 1. Nyní nebude žádný uzel z D do E. Totéž musíme udělat pro uzel „F“ a náš výsledek bude vypadat takto:

Topologické řazení

Krok 6) Vstupní stupeň (vstupní hrany) a výstupní stupeň (výstupní hrany) uzlu „E“ se stal nulovým. Splnili jsme tedy všechny předpoklady pro uzel „E“. Zde umístíme „E“ na konec fronty. Takže nám nezbývají žádné uzly a algoritmus zde končí.

Topologické řazení

Nepravý Code pro topologické třídění

Zde je pseudokód pro topologické třídění s využitím Kahnova algoritmu.

function TopologicalSort( Graph G ):
  for each node in G:
    calculate the indegree
  start = Node with 0 indegree
  G.remove(start)
  topological_list = [start]
  while node with 0 indegree present:
    topological_list.append(node)
    G.remove(node)
    // Update indegree of present nodes
  return topological_list

Topologické řazení lze také implementovat pomocí DFS (Hloubka první hledání) metoda. Tento přístup je však rekurzivní metodou. Kahnův algoritmus je efektivnější než přístup DFS.

C++ Implementace topologického třídění

#include<bits/stdc++.h>
using namespace std;
class graph{
  int vertices;
  list<int> *adjecentList;
public:
  graph(int vertices){
    this->vertices = vertices;
    adjecentList = new list<int>[vertices];
  }
  void createEdge(int u, int v){
    adjecentList[u].push_back(v);
  }
  void TopologicalSort(){
    // filling the vector with zero initially
    vector<int> indegree_count(vertices,0);

    for(int i=0;i<vertices;i++){
      list<int>::iterator itr;
      for(itr=adjecentList[i].begin(); itr!=adjecentList[i].end();itr++){
        indegree_count[*itr]++;
      }
    }
    queue<int> Q;
    for(int i=0; i<vertices;i++){
      if(indegree_count[i]==0){
        Q.push(i);
      }
    }
    int visited_node = 0;
    vector<int> order;
    while(!Q.empty()){
      int u = Q.front();
      Q.pop();
      order.push_back(u);

      list<int>::iterator itr;
      for(itr=adjecentList[u].begin(); itr!=adjecentList[u].end();itr++){
        if(--indegree_count[*itr]==0){
          Q.push(*itr);
        }
      }
      visited_node++;
    }
    if(visited_node!=vertices){
      cout<<"There's a cycle present in the Graph.\nGiven graph is not DAG"<<endl;
      return;
    }
    for(int i=0; i<order.size();i++){
      cout<<order[i]<<"\t";
    }
  }
};
int main(){
  graph G(6);
  G.createEdge(0,1);
  G.createEdge(0,2);
  G.createEdge(1,3);
  G.createEdge(1,5);
  G.createEdge(2,3);
  G.createEdge(2,5);
  G.createEdge(3,4);
  G.createEdge(5,4);
  G.TopologicalSort();
}

Výstup

0       1       2       3       5       4

Python Implementace topologického třídění

from collections import defaultdict
class graph:
    def __init__(self, vertices):
        self.adjacencyList = defaultdict(list)
        self.Vertices = vertices  # No. of vertices
    # function to add an edge to adjacencyList
    def createEdge(self, u, v):
        self.adjacencyList[u].append(v)
    # The function to do Topological Sort.
    def topologicalSort(self):
        total_indegree = [0]*(self.Vertices)
        for i in self.adjacencyList:
            for j in self.adjacencyList[i]:
                total_indegree[j] += 1
        queue = []
        for i in range(self.Vertices):
            if total_indegree[i] == 0:
                queue.append(i)
        visited_node = 0
        order = []
        while queue:
            u = queue.pop(0)
            order.append(u)
            for i in self.adjacencyList[u]:
                total_indegree[i] -= 1

                if total_indegree[i] == 0:
                    queue.append(i)
            visited_node += 1
        if visited_node != self.Vertices:
            print("There's a cycle present in the Graph.\nGiven graph is not DAG")
        else:
            print(order)
G = graph(6)
G.createEdge(0,1)
G.createEdge(0,2)
G.createEdge(1,3)
G.createEdge(1,5)
G.createEdge(2,3)
G.createEdge(2,5)
G.createEdge(3,4)
G.createEdge(5,4)
G.topologicalSort()

Výstup

[0, 1, 2, 3, 5, 4]

Cyklické grafy topologického algoritmu řazení

Graf obsahující cyklus nelze topologicky uspořádat, protože cyklický graf má závislost cyklickým způsobem. Podívejte se například na tento graf:

Cyklické grafy topologického algoritmu řazení

Tento graf není DAG (Directed Acyclic Graph), protože A, B a C vytvářejí cyklus. Pokud si všimnete, neexistuje žádný uzel s nulovou hodnotou vnitřního stupně. Podle Kahnova algoritmu, pokud analyzujeme výše uvedený graf:

  • Najděte uzel s nulovými stupni (žádné vstupní hrany).
  • Odeberte daný uzel z grafu a vložte ho do fronty. Ve výše uvedeném grafu však není žádný uzel s nulovou hodnotou stupňů. Každý uzel má hodnotu stupně větší než 0.
  • Vrátí prázdnou frontu, protože nenajde žádný uzel s nulovými stupni.

Cykly můžeme detekovat pomocí topologického uspořádání pomocí následujících kroků:

Krok 1) Proveďte topologické třídění.

Krok 2) Vypočítejte celkový počet prvků v topologicky seřazeném seznamu.

Krok 3) Pokud se počet prvků rovná celkovému počtu vrcholů, pak neexistuje žádný cyklus.

Krok 4) Pokud se nerovná počtu vrcholů, pak v dané datové struktuře grafu existuje alespoň jeden cyklus.

Analýza složitosti topologického řazení

V algoritmech existují dva typy složitosti. Jsou to:

  1. Časová složitost
  2. Složitost vesmíru

Tyto složitosti jsou reprezentovány funkcí, která poskytuje obecnou složitost.

Časová složitost: Časová složitost topologického třídění je veškerá stejná. Existují nejhorší, průměrný a nejlepší scénáře časové složitosti. Časová složitost topologického třídění je O(E + V), kde E znamená počet hran v grafu a V znamená počet vrcholů v grafu.

Pojďme se s touto složitostí vypořádat:

Krok 1) Na začátku spočítáme všechny stupně. Abychom to udělali, musíme projít všechny hrany a zpočátku přiřadíme všechny stupně V vrcholů nule. Takže postupné kroky, které dokončíme, budou O(V+E).

Krok 2) Najdeme uzel s nulovou indegren hodnotou. Musíme hledat z čísla V vrcholu. Dokončené kroky tedy budou O(V).

Krok 3) Pro každý uzel s nulovými stupni odstraníme tento uzel a snížíme stupeň. Provedení této operace pro všechny uzly bude trvat O(E).

Krok 4) Nakonec zkontrolujeme, zda existuje nějaký cyklus nebo ne. Zkontrolujeme, zda se celkový počet prvků v seřazeném poli rovná celkovému počtu uzlů. Zabere to O (1).

Takže to byly jednotlivé časové složitosti pro každý krok topologického třídění nebo topologického uspořádání. Můžeme říci, že časová složitost z výše uvedeného výpočtu bude O(V + E); zde O znamená funkci složitosti.

Prostorová složitost: Pro spuštění algoritmu topologického třídění jsme potřebovali O(V) prostorů. Zde jsou kroky, ve kterých jsme tento prostor pro program potřebovali:

  • Museli jsme vypočítat všechny stupně uzlů přítomných v grafu. Vzhledem k tomu, že graf má celkem V uzlů, musíme vytvořit pole o velikosti V. Požadovaný prostor byl tedy O(V).
  • K uložení uzlu s nulovým stupněm byla použita datová struktura Queue. Odstranili jsme uzly s nulovým stupněm z původního grafu a umístili je do fronty. K tomu byl potřebný prostor O(V).
  • Pole s názvem „order“ (pořadí) ukládá uzly v topologickém pořadí. To také vyžadovalo O(V) prostor.

Toto byly jednotlivé prostorové složitosti. Takže musíme tyto prostory maximalizovat za běhu programu. Prostorová složitost je zkratka pro O(V), kde V znamená číslo vrcholu v grafu.

Aplikace topologického řazení

Topologické třídění má obrovské využití. Zde je několik z nich:

  • Používá se, když Operating systému potřebuje provést alokaci zdrojů.
  • Nalezení cyklu v grafu. Topologickým tříděním můžeme ověřit, zda je graf DAG či nikoli.
  • Řazení vět v aplikacích automatického dokončování.
  • Používá se k detekci uváznutí.
  • Různé typy plánování nebo plánování kurzů používají topologické řazení.
  • Řešení závislostí. Pokud se například pokusíte nainstalovat balíček, může tento balíček také potřebovat další balíčky. Topologické uspořádání zjistí všechny potřebné balíčky k instalaci aktuálního balíčku.
  • Linux používá topologické řazení v „apt“ ke kontrole závislosti balíčků.

Nejčastější dotazy

Topologické třídění vytváří lineární uspořádání vrcholů DAG tak, že pro každou orientovanou hranu od u do v se u v uspořádání objevuje před v.

Libovolný cyklus zachytí každý uzel v něm s nenulovým stupněm vnitřního obvodu, který nikdy neklesne na nulu, takže Kahnův algoritmus nemůže vybrat další uzel. Platné topologické uspořádání vyžaduje orientovaný acyklický graf.

Kahnův algoritmus iterativně používá frontu a čítače vnitřních stupňů. Topologické řazení založené na DFS rekurzivně prochází grafem a hotové uzly vkládá do zásobníku. Oba běží za O(V + E).

Časová složitost je O(V + E), protože každý vrchol a hrana je zpracován jednou. Prostorová složitost je O(V) pro pole vnitřních stupňů, frontu a pole výstupního pořadí.

Ano. Pokud dva nebo více uzlů mají ve stejném kroku nulový independence, kterýkoli z nich může být vybrán jako první. Různá pořadí výběru vedou k různým platným topologickým uspořádáním stejného DAG.

Správci balíčků jako apt, npm a pip používají topologické uspořádání pro řešení závislostí. Na něm se spoléhají i systémy sestavení, plánovače úloh a plánovače předpokladů pro kurzy.

Frameworky strojového učení, jako jsou TensorFlow a PyTorch topologicky třídí výpočetní grafy pro plánování průchodů vpřed a vzad. Bayesovské sítě také vyžadují topologické uspořádání proměnných.

Ano. Nástroje AI Copilot, jako je GitHub Copilot, generují šablonu Kahnova algoritmu v C++, Pythonnebo JavaVývojáři stále potřebují ověřit detekci cyklů a správné zpracování front.

Shrňte tento příspěvek takto: