Algoritmo di ordinamento topologico: Python, C++ Esempio

โšก Riepilogo intelligente

L'ordinamento topologico ordina i nodi di un grafo aciclico diretto in modo che ogni nodo appaia prima di quelli a cui punta, utilizzando l'algoritmo di Kahn per selezionare ripetutamente i nodi con grado di entrata pari a zero.

  • ๐Ÿ“ Definizione: L'ordinamento topologico produce un ordine lineare dei vertici del DAG in cui ogni arco diretto (u, v) ha u prima di v.
  • ๐Ÿ” Algoritmo di Kahn: Seleziona ripetutamente un nodo con zero archi entranti, aggiungilo all'ordine e decrementa il grado di entrata dei suoi vicini.
  • ๐Ÿšซ Cicli bloccati: Un grafo contenente un ciclo non puรฒ essere ordinato topologicamente, poichรฉ nessun nodo raggiunge mai un grado di entrata pari a zero all'interno del ciclo.
  • ๐Ÿ’ป Code: C++ and Python Le implementazioni utilizzano una coda piรน un array di grado entrante per calcolare l'ordine in tempo O(V + E).
  • ๐Ÿ“Š Complessitร : La complessitร  temporale รจ O(V + E) e la complessitร  spaziale รจ O(V), dove V รจ il numero di vertici ed E รจ il numero di archi.
  • ๏ธ applicazioni: La pianificazione delle attivitร  e delle build, la risoluzione delle dipendenze dei pacchetti (apt, npm), il rilevamento dei deadlock e i prerequisiti dei corsi utilizzano tutti un ordine topologico.

Algoritmo di ordinamento topologico

Cos'รจ l'algoritmo di ordinamento topologico?

L'ordinamento topologico รจ noto anche come algoritmo di Kahn ed รจ un algoritmo di ordinamento popolare. Utilizzando un grafico diretto come input, l'ordinamento topologico ordina i nodi in modo che ciascuno appaia prima di quello a cui punta.

Questo algoritmo viene applicato a un DAG (grafo aciclico diretto) in modo che ogni nodo appaia nell'array ordinato prima di tutti gli altri nodi a cui punta. L'algoritmo segue ripetutamente alcune regole fino al completamento dell'ordinamento.

Per semplificare, guarda il seguente esempio:

Grafico diretto

Grafico diretto

Qui possiamo vedere che "A" non ha grado di entrata. Il grado di entrata indica l'arco che punta a un nodo. "B" e "C" hanno come prerequisito "A", mentre "E" ha come prerequisito i nodi "D" e "F". Alcuni nodi dipendono da altri nodi.

Ecco un'altra rappresentazione del grafico precedente:

Dipendenza di ciascun nodo

Dipendenza di ciascun nodo (ordinamento lineare)

Quindi, quando passiamo il DAG (grafico aciclico diretto) all'ordinamento topologico, ci darร  un array con ordinamento lineare, dove il primo elemento non ha dipendenza.

Algoritmo di ordinamento topologico

Ecco i passaggi per farlo:

Passo 1) Trova il nodo con zero bordi entranti, un nodo con zero gradi.

Passo 2) Memorizza quel nodo con grado di entrata pari a zero in una coda o in uno stack e rimuovi il nodo dal grafo.

Passo 3) Quindi elimina l'arco uscente da quel nodo. Questo decrementerร  il conteggio del grado entrante per il nodo successivo.

L'ordinamento topologico richiede che la struttura dati del grafo non presenti cicli. Un grafo sarร  considerato un DAG (grafo aciclico diretto) se soddisfa i seguenti requisiti:

  • Uno o piรน nodi con un valore di grado pari a zero.
  • Il grafico non contiene alcun ciclo.

Finchรฉ nel grafo sono presenti nodi e il grafo rimane un DAG, eseguiremo i tre passaggi sopra descritti. Altrimenti, l'algoritmo entrerร  in una dipendenza ciclica e l'algoritmo di Kahn non sarร  in grado di trovare un nodo con grado di entrata pari a zero.

Come funziona l'ordinamento topologico

Qui utilizzeremo l'algoritmo di Kahn per l'ordinamento topologico. Supponiamo di avere il seguente grafo:

Lavori di ordinamento topologico

Ecco i passaggi dell'algoritmo di Kahn:

Passo 1) Calcola il grado ingrado o il bordo entrante di tutti i nodi nel grafico.

Nota:

  • Ingrado indica i bordi diretti che puntano al nodo.
  • Per gradi esterni si intendono i bordi diretti che provengono da un nodo.

Ecco il grado di entrata e il grado di uscita del grafico sopra riportato:

Grado di ingresso e grado di uscita

Passo 2) Trova il nodo con grado di entrata pari a zero o con zero archi entranti. Un nodo con grado di entrata pari a zero significa che nessun arco entra in quel nodo. Il nodo "A" ha grado di entrata pari a zero, il che significa che non esiste alcun arco che punti al nodo "A". Pertanto, eseguiremo le seguenti azioni:

  • Rimuovi questo nodo e i suoi archi uscenti.
  • Posiziona il nodo nella coda per l'ordine.
  • Aggiorna il conteggio del grado di entrata del nodo vicino ad "A".

Lavori di ordinamento topologico

Passo 3) Dobbiamo trovare un nodo con un grado di entrata pari a zero. In questo esempio, "B" e "C" hanno un grado di entrata pari a zero. Possiamo scegliere uno qualsiasi dei due. Prendiamo "B" ed eliminiamolo dal grafo. Quindi aggiorniamo i valori di entrata degli altri nodi. Dopo aver eseguito queste operazioni, il nostro grafo e la coda avranno il seguente aspetto:

Lavori di ordinamento topologico

Passo 4) Il nodo "C" non ha archi entranti. Pertanto, rimuoveremo il nodo "C" dal grafo e lo inseriremo nella coda. Possiamo anche eliminare l'arco uscente da "C". Ora il nostro grafo apparirร  cosรฌ:

Lavori di ordinamento topologico

Passo 5) Possiamo notare che i nodi "D" e "F" hanno un grado di entrata pari a zero. Prenderemo un nodo e lo inseriremo nella coda. Iniziamo rimuovendo "D". Il conteggio del grado di entrata per il nodo "E" sarร  quindi 1. Ora non ci sarร  alcun nodo da D a E. Dobbiamo fare lo stesso per il nodo "F", e il risultato sarร  il seguente:

Lavori di ordinamento topologico

Passo 6) Il grado di entrata (archi entranti) e il grado di uscita (archi uscenti) del nodo "E" sono diventati zero. Pertanto, abbiamo soddisfatto tutti i prerequisiti per il nodo "E". A questo punto, posizioneremo "E" alla fine della coda. Non ci sono piรน nodi da aggiungere e l'algoritmo termina qui.

Lavori di ordinamento topologico

Soprannome Code per l'ordinamento topologico

Ecco lo pseudocodice per l'ordinamento topologico utilizzando l'algoritmo di Kahn.

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

L'ordinamento topologico puรฒ essere implementato anche utilizzando il DFS (Prima ricerca in profonditร ) metodo. Tuttavia, questo approccio รจ il metodo ricorsivo. L'algoritmo di Kahn รจ piรน efficiente dell'approccio DFS.

C++ Implementazione dell'ordinamento topologico

#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();
}

Uscita

0       1       2       3       5       4

Python Implementazione dell'ordinamento topologico

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()

Uscita

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

Grafici ciclici dell'algoritmo di ordinamento topologico

Un grafo contenente un ciclo non puรฒ essere ordinato topologicamente, poichรฉ il grafo ciclico presenta una dipendenza ciclica. Ad esempio, osserva questo grafo:

Grafici ciclici dell'algoritmo di ordinamento topologico

Questo grafo non รจ un DAG (grafo aciclico diretto) perchรฉ A, B e C formano un ciclo. Come si puรฒ notare, non esiste alcun nodo con grado di entrata pari a zero. Secondo l'algoritmo di Kahn, se analizziamo il grafo sopra riportato:

  • Trova un nodo con zero gradi (nessun arco entrante).
  • Rimuovi quel nodo dal grafo e aggiungilo alla coda. Tuttavia, nel grafo sopra, non c'รจ nessun nodo con grado di entrata pari a zero. Ogni nodo ha un valore di grado di entrata maggiore di 0.
  • Restituisce una coda vuota, poichรฉ non รจ stato possibile trovare alcun nodo con grado di entrata pari a zero.

Possiamo rilevare i cicli utilizzando l'ordinamento topologico con i seguenti passaggi:

Passo 1) Eseguire l'ordinamento topologico.

Passo 2) Calcolare il numero totale di elementi nell'elenco ordinato topologicamente.

Passo 3) Se il numero di elementi รจ uguale al numero totale di vertici, allora non c'รจ alcun ciclo.

Passo 4) Se non รจ uguale al numero di vertici, allora nella struttura dati del grafo data รจ presente almeno un ciclo.

Analisi della complessitร  dell'ordinamento topologico

Esistono due tipi di complessitร  negli algoritmi. Essi sono:

  1. Complessitร  temporale
  2. Complessitร  spaziale

Queste complessitร  sono rappresentate da una funzione che fornisce una complessitร  generale.

Complessitร  temporale: La complessitร  temporale per l'ordinamento topologico รจ sempre la stessa. Esistono scenari di complessitร  temporale peggiore, media e migliore. La complessitร  temporale per l'ordinamento topologico รจ O(E + V), dove E rappresenta il numero di archi nel grafo e V il numero di vertici nel grafo.

Cerchiamo di superare questa complessitร :

Passo 1) All'inizio calcoleremo tutti gli ingradi. Per fare ciรฒ, dobbiamo passare attraverso tutti gli spigoli e inizialmente assegneremo a zero tutti gli gradi del vertice V. Quindi, i passaggi incrementali che completeremo saranno O(V+E).

Passo 2) Troveremo il nodo con valore di grado zero. Dobbiamo cercare dal numero V del vertice. Quindi, i passaggi completati saranno O(V).

Passo 3) Per ogni nodo con zero gradi, rimuoveremo quel nodo e decrementeremo il grado. Sarร  necessario eseguire questa operazione per tutti i nodi O(E).

Passo 4) Infine, controlleremo se รจ presente o meno un ciclo. Controlleremo se il numero totale di elementi nell'array ordinato รจ uguale al numero totale di nodi. Ci vorrร  O (1).

Questi erano quindi i tempi di complessitร  individuali per ogni fase dell'ordinamento topologico. Possiamo affermare che la complessitร  temporale derivante dal calcolo precedente sarร  O(V + E); dove O indica la funzione di complessitร .

Complessitร  spaziale: Per eseguire l'algoritmo di ordinamento topologico, avevamo bisogno di spazi di tipo O(V). Ecco i passaggi in cui era necessario lo spazio per il programma:

  • Dovevamo calcolare tutti gli gradi dei nodi presenti nel Grafico. Poichรฉ il grafico ha un totale di nodi V, dobbiamo creare un array di dimensioni V. Quindi, lo spazio richiesto era O(V).
  • Per memorizzare il nodo con grado zero รจ stata utilizzata una struttura dati coda. Abbiamo rimosso i nodi con grado zero dal grafico originale e li abbiamo inseriti nella coda. Per questo, lo spazio richiesto era O(V).
  • L'array รจ denominato "order", che memorizza i nodi in ordine topologico. Ciรฒ richiedeva anche O(V) spazi.

Queste erano le singole complessitร  spaziali. Pertanto, dobbiamo massimizzare questi spazi in fase di esecuzione. La complessitร  spaziale รจ O(V), dove V indica il numero di vertici nel grafo.

Applicazione dell'ordinamento topologico

L'ordinamento topologico ha moltissime applicazioni. Eccone alcune:

  • Si usa quando un Operasistema di ting deve eseguire lโ€™allocazione delle risorse.
  • Individuazione di un ciclo nel grafo. Possiamo verificare se il grafo รจ un DAG o meno tramite l'ordinamento topologico.
  • Ordinamento delle frasi nelle app di completamento automatico.
  • Viene utilizzato per rilevare situazioni di stallo.
  • Diversi tipi di pianificazione o programmazione dei corsi utilizzano l'ordinamento topologico.
  • Risoluzione delle dipendenze. Ad esempio, se provi a installare un pacchetto, quel pacchetto potrebbe richiedere anche altri pacchetti. L'ordinamento topologico individua tutti i pacchetti necessari per installare il pacchetto corrente.
  • Linux utilizza l'ordinamento topologico in "apt" per verificare la dipendenza dei pacchetti.

DOMANDE FREQUENTI

L'ordinamento topologico produce un ordinamento lineare dei vertici di un DAG in modo che per ogni arco diretto da u a v, u appaia prima di v nell'ordinamento.

Qualsiasi ciclo intrappola ogni nodo al suo interno con un grado di entrata diverso da zero che non scende mai a zero, quindi l'algoritmo di Kahn non puรฒ scegliere un nodo successivo. Un ordine topologico valido richiede un grafo aciclico diretto.

L'algoritmo di Kahn utilizza una coda e contatori di grado entrante in modo iterativo. L'ordinamento topologico basato su DFS scorre ricorsivamente il grafo e inserisce i nodi completati in uno stack. Entrambi hanno una complessitร  temporale di O(V + E).

La complessitร  temporale รจ O(V + E) poichรฉ ogni vertice e arco viene elaborato una sola volta. La complessitร  spaziale รจ O(V) per l'array del grado di entrata, la coda e l'array dell'ordine di output.

Sรฌ. Quando due o piรน nodi hanno grado di entrata pari a zero nello stesso passaggio, uno qualsiasi dei due puรฒ essere scelto per primo. Ordini di scelta diversi producono ordinamenti topologici validi diversi dello stesso DAG.

I gestori di pacchetti come apt, npm e pip utilizzano l'ordine topologico per la risoluzione delle dipendenze. Anche i sistemi di compilazione, gli scheduler di attivitร  e i pianificatori dei prerequisiti dei corsi si basano su questo principio.

Framework di apprendimento automatico come TensorFlow e PyTorI grafi computazionali vengono ordinati topologicamente per pianificare i passaggi in avanti e all'indietro. Anche le reti bayesiane richiedono un ordine topologico sulle variabili.

Sรฌ. Gli strumenti AI Copilot come GitHub Copilot generano il boilerplate dell'algoritmo di Kahn in C++, Python, o JavaGli sviluppatori devono ancora verificare il rilevamento dei cicli e la corretta gestione delle code.

Riassumi questo post con: