Este capítulo está intimamente relacionado com o anterior, pois encontrar uma numeração topológica é essencialmente o mesmo que decidir se um grafo é acíclico.
Uma numeração topológica (= ordenação topológica = topological order) dos vértices de um grafo é uma atribuição de números aos vértices — um número t para cada vértice — de tal modo que
vt > wt para cada arco (v, w).
(O campo t é um utility field.) Usualmente vértices diferentes recebem números diferentes, todos no intervalo 1 .. n, sendo n é o número de vértices do grafo. Mas não há mal em permitir que dois vértices não adjacentes recebam o mesmo número.
Se um grafo tem uma numeração topológica então ele é acíclico; isto é evidente. A recíproca é menos óbvia mas não menos verdadeira.
Para que serve uma numeração topológica? Se os vértices de seu grafo são tarefas e um arco (v, w) significa que a tarefa v deve ser executada antes da tarefa w, então uma numeração topológica dá a sequência em que as tarefas podem ser executadas (a primeira tarefa a ser executada é a tiver o maior número e assim por diante).
Eis outra aplicação: Suponha que você foi incumbido de encontrar um ciclo em um grafo. Depois de uma noite de trabalho frustrante, você está convencido de que o grafo não tem ciclo algum. Como você pode convencer o seu chefe, no dia seguinte, de que o grafo não tem ciclos? Resposta: basta exibir uma numeração topológica dos vértices. O seu chefe terá um pouco de trabalho braçal para verificar que a numeração é de fato topológica, mas depois disso estará convencido de que não há ciclos. (A coisa também pode ser entendida ao contrário. Se o seu chefe quer uma numeração topológica e você precisa convencê-lo de que tal numeração não existe, basta exibir um ciclo.)
Segue o esboço de um algoritmo simples de numeração topológica. No início da execução do algoritmo, faça k = 0 e vt = −1 para todo vértice v. Para descrever o algoritmo, diremos que um vértice v é branco se vt ≡ −1 e preto em caso contrário. Diremos também que o grau de saída branco de um vértice v é o número de arcos da forma (v, w) em que w é branco. O algoritmo pode ser descrito assim:
enquanto o grafo tem sorvedouros brancos
escolha um vértice v cujo grau de saída branco é 0
faça vt = k
faça k = k+1
se todos os vértices são pretos
então t é uma numeração topológica
senão o grafo tem um ciclo
Uma implementação descuidada do algoritmo pode ser muito ineficiente. Uma implementação inteligente é bastante eficiente. (Mas o algoritmo da próxima seção é melhor.) O algoritmo tem um defeito: ele não devolve um ciclo explicitamente se tal existir. (Entretanto, veja o exercício do grafo sem sorvedouros no capítulo anterior.)
O algoritmo a seguir é muito eficiente
e faz um serviço completo.
A função num_topológica
que implementa o algoritmo
é um aperfeiçoamento da função
temciclo.
Ela recebe um grafo g e devolve 0
se g admite uma numeração topológica e 1 em caso contrário.
No primeiro caso,
a função grava a numeração topológica no
utility field
t
de cada vértice
(os números são
1, … ,
gn).
No segundo caso, ela usa o mesmo
utility field t
para marcar com CINZA os vértices de um
6
(que contém um ciclo).
#define BRANCO −1
#define CINZA 0
#define t u.I
Não há PRETO
explícito,
mas você pode imaginar que são PRETOs
todos os vértices que têm t ≥ 1.
int num_topológica (Graph *g) {
Vertex *v0 = gvertices, *vn = gvertices + gn;
for (v = v0; v < vn; v++)
vt = BRANCO;
for (y = v0; y < vn; y++) {
if (yt ≥ 1) continue;
yt = CINZA;
if (dfs (y) ≡ 1) return 1;
}
return 0;
}
(É claro que falta declarar as variáveis v e y.) A função dfs, que é o coração do algoritmo, é quase igual à do capítulo anterior; ela faz uma busca em profundidade a partir do vértice y e marca com CINZA todos os vértices encontrados. A variável estática k é inicializada apenas na primeira execução de dfs e incrementada a cada execução subsequente; ela administra a numeração topológica.
int dfs (Vertex *y) {
static int k = 1;
for (a = yarcs; a ≠ NULL; a = anext) {
z = atip;
if (zt ≥ 1) continue;
if (zt ≡ CINZA) return 1;
zt = CINZA;
if (dfs (z) ≡ 1) return 1;
}
yt = k++;
return 0;
}
for (v = v0; v < vn; v++)
vt = BRANCO;
v0t = CINZA;
if (dfs (v0) ≡ 1) return 1;
return 0;
board (8, 8, 0, 0, 1, 0, 1)
board (8, 8, 0, 0, 1, −1, 1)
econ (79, 2, 100, 0)
do SGB. Para saber se sua implementação é realmente eficiente, aplique-a aos grafos
binary (10, 5, 1)
partial_gates (prod (20,80), 20, 0, 0, NULL)
all_trees (10, 1)
(Você precisa incluir gb_econ.h para usar econ, gb_gates.h para usar partial_gates e gb_basic.h para usar qualquer uma das demais.)