Ir para o conteúdo principal

Grafos Direcionados: Quando a Direção Importa

·4604 palavras·22 minutos·
Autor
Francisco Bustamante
Químico, cientista de dados e programador Python.
Tabela de conteúdos
A Matemática das Conexões - Este artigo faz parte de uma série de artigos.
Parte 10: Esse Artigo

Até agora, tratamos grafos onde as relações são simétricas: se \(A\) está conectado a \(B\), então \(B\) também está conectado a \(A\). Mas muitas relações do mundo real são assimétricas.

Uma rua de mão única vai de \(A\) para \(B\), mas não de \(B\) para \(A\). Um hyperlink leva de uma página a outra, mas a outra página não necessariamente aponta de volta. Uma tarefa \(A\) precisa ser concluída antes de \(B\) começar — não o contrário.

Para modelar essas situações, precisamos dos grafos direcionados.

Por que Dígrafos Importam?
#

Sempre que uma relação entre dois objetos não é automaticamente recíproca, um grafo comum não basta — é preciso registrar também o sentido dela. Alguns exemplos:

  • Navegação e mapas: ruas de mão única e rotas aéreas nem sempre podem ser percorridas nos dois sentidos
  • Web e mecanismos de busca: hyperlinks apontam de uma página para outra, e o PageRank do Google — já visto em Grafos: Uma Linguagem para Modelar Conexões — é construído sobre essa estrutura
  • Redes sociais: seguir alguém no Instagram ou no X não implica ser seguido de volta
  • Compiladores e gerenciadores de pacotes: dependências entre módulos só podem ser resolvidas em uma ordem específica
  • Escalonamento de tarefas: um projeto com etapas que precisam acontecer em sequência

Para tornar a ideia concreta antes da definição formal, considere o seguinte cenário.

Exemplo: um quarteirão com mão única

Enunciado: Considere um quarteirão de Copacabana, no Rio de Janeiro, limitado por quatro vias — a Avenida Atlântica, a Rua Figueiredo Magalhães, a Avenida Nossa Senhora de Copacabana e a Rua Santa Clara — com a Rua Domingos Ferreira cortando o quarteirão ao meio. Modele esse quarteirão primeiro como um grafo \(G\) comum e depois como um dígrafo \(D\), registrando o sentido do trânsito de cada rua.

Solução: Cada esquina (cruzamento de ruas) é um vértice, identificado pelas duas ruas que se cruzam ali: \(AF\) (Atlântica × Figueiredo), \(AS\) (Atlântica × Santa Clara), \(DF\) (Domingos Ferreira × Figueiredo), \(DS\) (Domingos Ferreira × Santa Clara), \(NF\) (N. S. Copacabana × Figueiredo) e \(NS\) (N. S. Copacabana × Santa Clara). Cada trecho de rua entre duas esquinas vizinhas é uma aresta.

Sem considerar o sentido do trânsito, isso já é um grafo \(G\) comum, com arestas

$$(AF,AS), (AF,DF), (DF,DS), (DF,NF), (DS,AS), (NS,DS), (NF,NS)$$

Mas as ruas têm sentido de tráfego: a Av. Atlântica tem mão dupla; a Rua Figueiredo Magalhães é de mão única no sentido \(AF \to DF \to NF\); a Rua Santa Clara tem mão única no sentido oposto, \(NS \to DS\) e \(DS \to AS\); a Rua Domingos Ferreira flui de \(DF\) para \(DS\); e a Av. Nossa Senhora de Copacabana flui de \(NS\) para \(NF\). Registrando cada sentido como um arco, obtemos o dígrafo

$$D: \{(AF,AS), (AS,AF), (AF,DF), (DF,NF), (NS,DS), (DS,AS), (DF,DS), (NS,NF)\}$$

— o mesmo quarteirão, agora com a informação de para onde o trânsito pode seguir em cada rua. A figura abaixo mostra as duas versões lado a lado:

Grafo G do quarteirão (esquinas sem direção) e o mesmo quarteirão como dígrafo D, com o sentido do trânsito de cada rua
O quarteirão como grafo G e como dígrafo D

O Que É um Dígrafo?
#

Dígrafo (Grafo Direcionado)

Um dígrafo \(D = (V, E)\) é um par onde:

  • \(V\) é um conjunto finito não vazio de vértices
  • \(E\) é um conjunto de arcos: pares ordenados de vértices distintos

Para um arco \((v, w) \in E(D)\):

  • \(v\) é a cauda (ponto de partida)
  • \(w\) é a cabeça (ponto de chegada)
  • Dizemos que o arco é divergente de \(v\) e convergente a \(w\)
  • Na representação geométrica, desenhamos uma seta de \(v\) para \(w\)
Diferença fundamental

Em um grafo (não direcionado), a aresta \(\{u,v\} = \{v,u\}\) — não há distinção.

Em um dígrafo, o arco \((u,v) \neq (v,u)\) — são arcos diferentes! Ambos podem existir simultaneamente.

Um arco genérico v para w com cauda e cabeça marcadas, e os dois arcos opostos (v,w) e (w,v) desenhados como curvas distintas
Cauda e cabeça de um arco; (v,w) e (w,v) são arcos diferentes

Exemplo concreto

Enunciado: Para o dígrafo \(D\) com \(V = \{a, b, c, d, e\}\) e \(E = \{(a,b),(b,c),(a,e),(b,e),(e,b),(e,c),(d,c),(d,e)\}\), descreva as relações de adjacência de cada vértice.

Solução:

Dígrafo D com vértices a, b, c, d, e, incluindo o par de arcos opostos entre b e e
Dígrafo D — V={a,b,c,d,e}

  • \(a\): aponta para \(b\) e \(e\) (mas ninguém aponta para \(a\))
  • \(b\) e \(e\) têm arcos em ambas as direções entre si: \((b,e)\) e \((e,b)\)
  • \(c\): recebe arcos de \(b\), \(e\) e \(d\), mas não aponta para ninguém

Grafo Subjacente e Orientação
#

Grafo Subjacente

O grafo subjacente de um dígrafo \(D\) é obtido removendo as direções dos arcos (transformando-os em arestas não direcionadas) e eliminando arestas paralelas resultantes.

Exemplo: construindo o grafo subjacente

Enunciado: Construa o grafo subjacente do dígrafo \(D\) do exemplo anterior (\(V=\{a,b,c,d,e\}\), arcos \(\{(a,b),(b,c),(a,e),(b,e),(e,b),(e,c),(d,c),(d,e)\}\)).

Solução: O grafo subjacente é construído em duas etapas:

  1. Remover as direções: cada arco vira uma aresta comum. Como \((b,e)\) e \((e,b)\) eram dois arcos distintos entre os mesmos vértices, viram duas arestas paralelas entre \(b\) e \(e\).
  2. Remover as arestas paralelas: as duas arestas entre \(b\) e \(e\) colapsam em uma só.

O resultado é o grafo subjacente \(G\), com arestas \((a,b), (a,e), (b,c), (b,e), (e,c), (c,d), (d,e)\) — 7 arestas, uma a menos que os 8 arcos originais de \(D\). A figura abaixo mostra as duas etapas:

Três painéis: o dígrafo D, o mesmo grafo sem direção com arestas paralelas entre b e e, e o grafo subjacente final com as paralelas colapsadas
Construindo o grafo subjacente de D em duas etapas

Orientação

Uma orientação de um grafo \(G\) é um dígrafo \(\vec{G}\) obtido especificando uma direção para cada aresta de \(G\). A orientação de um grafo não é única — o mesmo \(G\) admite várias orientações diferentes.

Exemplo: duas orientações diferentes do mesmo grafo

Enunciado: Encontre duas orientações diferentes do grafo subjacente \(G\) do exemplo anterior.

Solução: Duas possibilidades:

  • \(\vec{G}\): \((a,b), (a,e), (b,c), (e,b), (c,e), (c,d), (e,d)\)
  • \((\vec{G})’\): a orientação oposta de cada aresta — \((b,a), (e,a), (c,b), (b,e), (e,c), (d,c), (d,e)\)

As duas são dígrafos válidos sobre o mesmo grafo subjacente \(G\), mas com arcos apontando em sentidos diferentes — por isso a orientação de um grafo não é única, como mostra a figura:

O grafo subjacente G e duas orientações diferentes dele, vec G e (vec G) linha, com arcos em sentidos opostos
G admite mais de uma orientação

Graus de Entrada e Saída
#

Em dígrafos, cada vértice tem dois graus:

Grau de Entrada e Saída

Para um vértice \(v\) em um dígrafo \(D\):

  • Grau de entrada: \(d^-(v) = |\{(u,v) \in E(D)\}|\) — número de arcos que chegam a \(v\)
  • Grau de saída: \(d^+(v) = |\{(v,u) \in E(D)\}|\) — número de arcos que partem de \(v\)

Vértices especiais:

  • Fonte: \(d^-(v) = 0\) — nenhum arco chega a \(v\)
  • Sumidouro: \(d^+(v) = 0\) — nenhum arco parte de \(v\)

Fórmula Análoga ao Aperto de Mãos
#

Teorema

$$\sum_{v \in V} d^-(v) = \sum_{v \in V} d^+(v) = |E(D)|$$

Prova: Cada arco \((u,v)\) contribui 1 para \(d^+(u)\) e 1 para \(d^-(v)\). Somando sobre todos os arcos, cada soma conta cada arco exatamente uma vez. Portanto, \(\sum d^-(v) = \sum d^+(v) = |E(D)|\), que é o que queríamos demonstrar. \(\blacksquare\)

Exemplo: graus no dígrafo acima

Enunciado: Calcule o grau de entrada e o grau de saída de cada vértice do dígrafo \(D\) do exemplo acima (\(V = \{a,b,c,d,e\}\)), e identifique fontes e sumidouros.

Solução:

Vértice \(d^+(v)\) \(d^-(v)\)
\(a\) 2 0
\(b\) 2 2
\(c\) 0 3
\(d\) 2 0
\(e\) 2 3

\(a\) e \(d\) são fontes; \(c\) é sumidouro.

Soma dos graus de saída: \(2+2+0+2+2 = 8 = |E|\). ✓

Conferindo com código

Contar arcos à mão funciona para dígrafos pequenos, mas o networkx já traz o grau de entrada e de saída prontos como métodos do próprio dígrafo: D.in_degree(v) e D.out_degree(v). O código abaixo percorre cada vértice, calcula os dois graus e marca quem é fonte (\(d^-=0\)) ou sumidouro (\(d^+=0\)):

import networkx as nx

D = nx.DiGraph([("a", "b"), ("b", "c"), ("a", "e"), ("b", "e"),
                ("e", "b"), ("e", "c"), ("d", "c"), ("d", "e")])

for v in sorted(D.nodes()):
    d_mais, d_menos = D.out_degree(v), D.in_degree(v)
    papel = " (fonte)" if d_menos == 0 else " (sumidouro)" if d_mais == 0 else ""
    print(f"{v}: d+={d_mais}, d-={d_menos}{papel}")
a: d+=2, d-=0 (fonte)
b: d+=2, d-=2
c: d+=0, d-=3 (sumidouro)
d: d+=2, d-=0 (fonte)
e: d+=2, d-=3

O resultado bate exatamente com a tabela calculada à mão.

Caminhos e Ciclos em Dígrafos
#

As definições de passeio, trajeto, caminho e ciclo, já vistas em Caminhos, Ciclos e Conexidade, se mantêm, mas agora com uma restrição importante: deve-se seguir as direções dos arcos.

Passeio Direcionado

Um passeio direcionado em um dígrafo \(D\) é uma sequência de vértices \(P = v_0, v_1, \ldots, v_k\) tal que \((v_i, v_{i+1}) \in E(D)\) para \(0 \leq i \leq k-1\) — ou seja, cada passo da sequência segue um arco de \(D\) na direção indicada. Vértices e arcos podem se repetir.

Trajeto Direcionado

Um trajeto direcionado é um passeio direcionado em que nenhum arco se repete (mas vértices podem se repetir).

Caminho Direcionado

Um caminho direcionado é um passeio direcionado em que nenhum vértice se repete (e portanto nenhum arco também se repete).

Exemplo: passeio, trajeto e caminho no mesmo dígrafo

Enunciado: Considere o dígrafo com \(V = \{a,b,c,d,e,f,g,h\}\) e arcos \(E = \{(a,b),(b,c),(b,e),(c,e),(c,g),(e,a),(e,d),(d,a),(d,c),(d,f),(f,d),(d,h)\}\), mostrado na figura abaixo. Classifique as sequências \(P_1 = a,b,c,e,a,b,c,g\), \(P_2 = a,b,e,d,f,d,c,g\) e \(P_3 = a,b,c,g\).

Dígrafo com vértices a a h, com o ciclo de comprimento 2 entre d e f destacado em laranja
Dígrafo usado nos exemplos de passeio, trajeto, caminho e alcançabilidade

Solução:

  • \(P_1\) é um passeio direcionado de \(a\) a \(g\): os arcos \((a,b)\) e \((b,c)\) são percorridos duas vezes — logo não é um trajeto.
  • \(P_2\) é um trajeto direcionado de \(a\) a \(g\): nenhum arco se repete, mas o vértice \(d\) aparece duas vezes — logo não é um caminho.
  • \(P_3\) é um caminho direcionado de \(a\) a \(g\): nenhum vértice se repete.

Além disso, \(C_1 = a,b,c,e,d,a\) é um ciclo direcionado, e \(C_2 = d,f,d\) é um ciclo direcionado de comprimento 2 — possível porque \((d,f)\) e \((f,d)\) são ambos arcos de \(D\).

note

O ciclo \(C_2 = d,f,d\) do exemplo acima ilustra um fato exclusivo de dígrafos: se \((u,v)\) e \((v,u)\) são ambos arcos, então \(u \to v \to u\) é um ciclo de comprimento 2. Em grafos simples (não direcionados), isso não acontece, pois a mesma aresta não pode ser percorrida duas vezes em um ciclo.

Alcançabilidade: Dizemos que \(v\) alcança \(u\) se existe um caminho direcionado de \(v\) a \(u\).

Exemplo: alcançabilidade

Enunciado: No dígrafo da figura anterior, quais vértices \(a\) alcança? Existe algum vértice que não alcança \(a\)?

Solução: \(a\) alcança \(g\) (via \(a,b,c,g\)), alcança \(f\) (via \(a,b,e,d,f\)) e alcança \(h\) (via \(a,b,e,d,h\)) — na verdade, \(a\) alcança todos os outros vértices.

Isso não vale para todo vértice: \(g\) não tem nenhum arco saindo dele, então \(g\) não alcança ninguém — em particular, \(g\) não alcança \(a\).

Conexidade em Dígrafos
#

A noção de conexidade, vista em Caminhos, Ciclos e Conexidade para grafos comuns, é mais rica em dígrafos, pois as direções importam:

Dígrafo Fortemente Conexo

\(D\) é fortemente conexo se para todo par \(v, w \in V\) existe um caminho dirigido de \(v\) para \(w\) e de \(w\) para \(v\).

Dígrafo Unilateralmente Conexo

\(D\) é unilateralmente conexo se para todo par \(v, w \in V\) existe caminho dirigido de \(v\) para \(w\) ou de \(w\) para \(v\) (pelo menos um dos dois).

Dígrafo Fracamente Conexo

\(D\) é fracamente conexo se o grafo subjacente é conexo (ignorando as direções).

Dígrafo Desconexo

\(D\) é desconexo se o grafo subjacente é desconexo.

Hierarquia de Conexidade
#

$$\text{Fortemente conexo} \implies \text{Unilateralmente conexo} \implies \text{Fracamente conexo}$$

As implicações são estritas: cada nível é genuinamente mais fraco, como mostram os exemplos a seguir.

Exemplo: fortemente conexo

Enunciado: Verifique que o dígrafo \(D\) com \(V=\{a,b,c,d,e\}\) e arcos \(E = \{(a,b),(b,c),(c,d),(d,a),(a,e),(c,e),(e,b),(e,d)\}\), mostrado abaixo, é fortemente conexo.

Dígrafo fortemente conexo: quadrado a,b,c,d com vértice central e, arcos formando ciclos que cobrem todos os pares
Qualquer vértice alcança qualquer outro

Solução: Basta mostrar que, partindo de qualquer vértice, chega-se a todos os outros. Por exemplo, partindo de \(a\): chega-se a \(e\) diretamente, a \(b\) via \(a,b\), a \(c\) via \(a,b,c\) e a \(d\) via \(a,e,d\) (ou \(a,b,c,d\)). Repetindo essa verificação a partir de cada vértice, confirma-se que \(D\) é fortemente conexo.

Exemplo: os três níveis de conexidade lado a lado

Enunciado: Classifique os três dígrafos abaixo, todos com \(V = \{a,b,c,d,e\}\), quanto ao nível de conexidade:

  • \(D_1\): arcos \((a,b), (d,a), (b,c), (d,c), (c,e)\).
  • \(D_2\): arcos \((a,b), (a,d), (c,b), (c,d), (c,e)\).
  • \(D_3\): arcos \((a,b), (d,a), (b,c), (d,c)\), com \(e\) isolado.

Três dígrafos lado a lado: D1 unilateralmente conexo, D2 fracamente conexo, D3 desconexo
D1, D2 e D3 — três níveis de conexidade

Solução:

  • Em \(D_1\), \(d\) é uma fonte que alcança todos os demais (\(d \to a \to b \to c \to e\)), e \(e\) é um sumidouro alcançado por todos os demais. Como \(a\), \(b\) e \(c\) formam uma cadeia direta \(a \to b \to c\), todo par de vértices tem caminho em pelo menos um sentido — \(D_1\) é unilateralmente conexo. Mas como nada chega a \(d\) e \(e\) não alcança ninguém, \(D_1\) não é fortemente conexo.
  • Em \(D_2\), \(a\) só alcança \(b\) e \(d\), enquanto \(c\) alcança \(b\), \(d\) e \(e\) — nenhum dos dois alcança o outro. Esse único par já basta para \(D_2\) não ser unilateralmente conexo. Mas o grafo subjacente é conexo (todos os vértices se ligam através de \(a\) e \(c\)), então \(D_2\) é fracamente conexo.
  • Em \(D_3\), o vértice \(e\) está isolado — nenhum arco o liga aos demais, nem mesmo sem considerar direção. Como o grafo subjacente já é desconexo, \(D_3\) é desconexo.
Conferindo com código

Verificar conexidade à mão exige checar caminhos entre vários pares de vértices — cansativo mesmo em dígrafos pequenos. O networkx tem uma função pronta para cada um dos três níveis: nx.is_strongly_connected, nx.is_semiconnected (esse é o nome técnico para “unilateralmente conexo”) e nx.is_weakly_connected:

import networkx as nx

D1 = nx.DiGraph([("a", "b"), ("d", "a"), ("b", "c"), ("d", "c"), ("c", "e")])
D2 = nx.DiGraph([("a", "b"), ("a", "d"), ("c", "b"), ("c", "d"), ("c", "e")])
D3 = nx.DiGraph([("a", "b"), ("d", "a"), ("b", "c"), ("d", "c")])
D3.add_node("e")  # e isolado

for nome, D in [("D1", D1), ("D2", D2), ("D3", D3)]:
    print(f"{nome}: forte={nx.is_strongly_connected(D)}, "
          f"unilateral={nx.is_semiconnected(D)}, "
          f"fraco={nx.is_weakly_connected(D)}")
D1: forte=False, unilateral=True, fraco=True
D2: forte=False, unilateral=False, fraco=True
D3: forte=False, unilateral=False, fraco=False

Repare que fraco=True para \(D_1\) também — faz sentido, já que unilateralmente conexo implica fracamente conexo (a hierarquia vista antes). Só \(D_3\) tem fraco=False, exatamente porque \(e\) está isolado.

Componentes fortemente conexos

Um dígrafo que não é fortemente conexo se divide em componentes fortemente conexos: os maiores conjuntos de vértices em que cada um alcança todos os outros. No dígrafo dos exemplos de passeio, eles são \(\{a,b,c,d,e,f\}\), \(\{g\}\) e \(\{h\}\). Encontrá-los é um problema clássico, resolvido em tempo linear, \(O(n + m)\), por dois algoritmos baseados em busca em profundidade: o de Robert Tarjan, publicado em 1972, e o de S. Rao Kosaraju, descrito em 1978 e publicado por Micha Sharir em 1981 (veja o verbete Strongly connected component). No networkx, a função é nx.strongly_connected_components:

import networkx as nx

D = nx.DiGraph([
    ("a", "b"), ("b", "c"), ("b", "e"), ("c", "e"), ("c", "g"), ("e", "a"),
    ("e", "d"), ("d", "a"), ("d", "c"), ("d", "f"), ("f", "d"), ("d", "h"),
])
print(sorted(sorted(c) for c in nx.strongly_connected_components(D)))
# Saída: [['a', 'b', 'c', 'd', 'e', 'f'], ['g'], ['h']]

Representação Matricial
#

Matriz de Adjacência de Dígrafo
#

Matriz de Adjacência

Para \(D\) com \(V = \{v_1, \ldots, v_n\}\), a matriz de adjacência \(A = (a_{ij})\) é \(n \times n\):

$$a_{ij} = \begin{cases} 1 & \text{se } (v_i, v_j) \in E(D) \\ 0 & \text{caso contrário} \end{cases}$$
warning

A matriz de adjacência de um dígrafo não é simétrica em geral — ao contrário de grafos não orientados.

Relação com graus:

  • Soma da linha \(i\): fornece \(d^+(v_i)\) (grau de saída)
  • Soma da coluna \(j\): fornece \(d^-(v_j)\) (grau de entrada)
Exemplo: matriz de adjacência

Enunciado: Construa a matriz de adjacência do dígrafo \(D\) com \(V = \{v_1,v_2,v_3,v_4\}\) e arcos \((v_1,v_2), (v_1,v_4), (v_3,v_2), (v_3,v_4), (v_4,v_2), (v_2,v_4)\), e identifique fontes e sumidouros a partir dela.

Dígrafo D com vértices v1, v2, v3, v4
Dígrafo D — V={v1,v2,v3,v4}

Solução:

$$A = \begin{array}{c|cccc} & v_1 & v_2 & v_3 & v_4 \\ \hline v_1 & 0 & 1 & 0 & 1 \\ v_2 & 0 & 0 & 0 & 1 \\ v_3 & 0 & 1 & 0 & 1 \\ v_4 & 0 & 1 & 0 & 0 \end{array}$$
  • Linha de \(v_1\): soma = 2 = \(d^+(v_1)\)
  • Coluna de \(v_2\): soma = \(1+0+1+1 = 3 = d^-(v_2)\)
  • Colunas de \(v_1\) e \(v_3\) são totalmente nulas: \(v_1\) e \(v_3\) são fontes (nenhum arco chega a eles)
  • Nenhuma linha é totalmente nula: \(D\) não tem sumidouro

Matriz de Incidência de Dígrafo
#

Matriz de Incidência

Para \(D\) com \(n\) vértices e \(m\) arcos, a matriz de incidência \(B = (b_{ij})\) é \(n \times m\):

$$b_{ij} = \begin{cases} +1 & \text{se o arco } e_j \text{ diverge de } v_i \text{ (cauda)} \\ -1 & \text{se o arco } e_j \text{ converge a } v_i \text{ (cabeça)} \\ 0 & \text{caso contrário} \end{cases}$$

Cada coluna tem exatamente um \(+1\) (a cauda do arco) e um \(-1\) (a cabeça do arco).

Exemplo: matriz de incidência

Enunciado: Construa a matriz de incidência do mesmo dígrafo \(v_1,v_2,v_3,v_4\) do exemplo anterior, agora com os arcos rotulados \(e_1=(v_1,v_2), e_2=(v_3,v_2), e_3=(v_3,v_4), e_4=(v_1,v_4), e_5=(v_4,v_2), e_6=(v_2,v_4)\):

O mesmo dígrafo v1..v4, agora com cada arco rotulado e1 a e6
Dígrafo D com arcos rotulados e1..e6

Solução:

$$B = \begin{array}{c|cccccc} & e_1 & e_2 & e_3 & e_4 & e_5 & e_6 \\ \hline v_1 & 1 & 0 & 0 & 1 & 0 & 0 \\ v_2 & -1 & -1 & 0 & 0 & -1 & 1 \\ v_3 & 0 & 1 & 1 & 0 & 0 & 0 \\ v_4 & 0 & 0 & -1 & -1 & 1 & -1 \end{array}$$

Cada coluna tem exatamente um \(+1\) e um \(-1\): por exemplo, a coluna de \(e_5\) tem \(+1\) em \(v_4\) e \(-1\) em \(v_2\), confirmando o arco \((v_4,v_2)\).

Conferindo com código

O networkx calcula as duas matrizes prontas: nx.to_numpy_array para a de adjacência e nx.incidence_matrix para a de incidência. Só um detalhe: a convenção de sinal do networkx é a oposta da nossa (\(-1\) na cauda e \(+1\) na cabeça, em vez de \(+1\) na cauda e \(-1\) na cabeça) — então basta multiplicar o resultado por \(-1\) para bater com a matriz \(B\) que calculamos à mão:

import networkx as nx

D = nx.DiGraph()
D.add_edges_from([("v1", "v2"), ("v1", "v4"), ("v3", "v2"),
                   ("v3", "v4"), ("v4", "v2"), ("v2", "v4")])
vertices = ["v1", "v2", "v3", "v4"]
arcos = [("v1", "v2"), ("v3", "v2"), ("v3", "v4"), ("v1", "v4"), ("v4", "v2"), ("v2", "v4")]  # e1..e6

A = nx.to_numpy_array(D, nodelist=vertices, dtype=int)
print("Matriz de adjacência:\n", A)

B = -nx.incidence_matrix(D, nodelist=vertices, edgelist=arcos, oriented=True).toarray().astype(int)
print("Matriz de incidência:\n", B)
Matriz de adjacência:
 [[0 1 0 1]
 [0 0 0 1]
 [0 1 0 1]
 [0 1 0 0]]
Matriz de incidência:
 [[ 1  0  0  1  0  0]
 [-1 -1  0  0 -1  1]
 [ 0  1  1  0  0  0]
 [ 0  0 -1 -1  1 -1]]

As duas batem, arco por arco, com as matrizes \(A\) e \(B\) calculadas nos exemplos acima.

Aplicações de Dígrafos
#

Com a base teórica estabelecida, vale destacar como os dígrafos aparecem na prática, muitas vezes como parte central de outras estruturas:

  • Fluxo em redes: água em tubulações com válvulas, corrente elétrica em circuitos — o sentido do fluxo é parte do modelo
  • World Wide Web: páginas são vértices e hyperlinks são arcos; o PageRank (detalhado em Grafos: Uma Linguagem para Modelar Conexões) usa os graus de entrada e saída desse dígrafo gigante para estimar a relevância de cada página
  • Grafos de chamada: em análise de software, quais funções chamam quais — um arco \((f,g)\) significa que \(f\) chama \(g\)
  • DAGs (grafos acíclicos direcionados): dígrafos sem nenhum ciclo direcionado, usados para modelar dependências
DAG — Grafo Acíclico Direcionado

Um DAG (Directed Acyclic Graph, ou grafo acíclico direcionado) é um dígrafo acíclico (sem ciclos direcionados). É fundamental em:

  • Escalonamento de tarefas com dependências (o arco \((A, B)\) significa “A antes de B”), como no Apache Airflow
  • Instalação de pacotes com dependências (npm, pip, apt)
  • Análise de dependências em Makefiles
  • Representação de genealogias

Ordenação Topológica
#

Num DAG de dependências, a pergunta prática é: em que ordem executar as tarefas, de modo que nenhuma rode antes das que ela precisa? A resposta é uma ordenação topológica.

Ordenação Topológica

Uma ordenação topológica de um dígrafo \(D\) é uma lista de todos os seus vértices em que, para todo arco \((u, v)\), \(u\) aparece antes de \(v\).

Teorema

Um dígrafo admite uma ordenação topológica se e somente se é um DAG.

Prova: (\(\Rightarrow\)) Suponha que \(D\) tenha uma ordenação topológica e também um ciclo direcionado. Entre os vértices do ciclo, seja \(v\) o que aparece primeiro na ordenação. O arco do ciclo que chega a \(v\) sai de um vértice que aparece depois de \(v\) — o que a ordenação proíbe. Logo \(D\) não tem ciclos.

(\(\Leftarrow\)) Primeiro, todo DAG tem uma fonte. Se nenhum vértice fosse fonte, poderíamos partir de qualquer vértice e andar sempre para trás, por um arco que chega ao vértice atual; como há finitos vértices, algum se repetiria, e o trecho entre as duas visitas seria um ciclo direcionado. Agora, por indução no número de vértices: coloque uma fonte \(s\) no início da lista; o dígrafo \(D - s\) continua sem ciclos e, pela hipótese de indução, tem uma ordenação topológica, que vem depois de \(s\). Nenhum arco chega a \(s\), então nenhum arco é violado.

Portanto, um dígrafo admite uma ordenação topológica se e somente se é um DAG, que é o que queríamos demonstrar. \(\blacksquare\)

A segunda metade da prova já é um algoritmo, descrito por Arthur B. Kahn em 1962: tire uma fonte, depois outra, e assim por diante. Para não procurar fontes do zero a cada passo, o algoritmo de Kahn guarda o grau de entrada de cada vértice e uma fila com as fontes da vez; ao retirar um vértice \(u\), diminui em 1 o grau de entrada de cada vértice para o qual \(u\) aponta, e quem chega a 0 entra na fila. Se a fila esvaziar antes de todos os vértices saírem, sobrou um ciclo. Cada vértice e cada arco são tratados uma vez: tempo \(O(n + m)\) (veja o verbete Topological sorting).

from collections import deque

import networkx as nx

tarefas = nx.DiGraph([
    ("extrair", "limpar"), ("limpar", "treinar"), ("treinar", "avaliar"),
    ("limpar", "graficos"), ("avaliar", "relatorio"), ("graficos", "relatorio"),
])


def ordem_topologica(D):
    """Algoritmo de Kahn: uma ordenação topológica de D, ou None se D tiver ciclo."""
    grau_entrada = {v: D.in_degree(v) for v in D}
    fila = deque(v for v in D if grau_entrada[v] == 0)  # as fontes
    ordem = []
    while fila:
        u = fila.popleft()
        ordem.append(u)
        for w in D.successors(u):
            grau_entrada[w] -= 1
            if grau_entrada[w] == 0:
                fila.append(w)
    return ordem if len(ordem) == D.number_of_nodes() else None


print(ordem_topologica(tarefas))
# Saída: ['extrair', 'limpar', 'treinar', 'graficos', 'avaliar', 'relatorio']
print(list(nx.topological_sort(tarefas)))
# Saída: ['extrair', 'limpar', 'treinar', 'graficos', 'avaliar', 'relatorio']

tarefas.add_edge("relatorio", "extrair")  # fecha um ciclo
print(ordem_topologica(tarefas))  # None

Um DAG costuma ter várias ordenações topológicas — aqui, graficos poderia vir antes de treinar —, e qualquer uma delas é uma ordem válida de execução. A figura abaixo mostra o DAG do exemplo e, embaixo, os mesmos vértices em fila, na ordem encontrada:

Em cima, o DAG de tarefas: extrair aponta para limpar; limpar aponta para treinar e para graficos; treinar aponta para avaliar; avaliar e graficos apontam para relatorio. Embaixo, as seis tarefas em fila na ordenação topológica extrair, limpar, treinar, graficos, avaliar, relatorio, numeradas de 1º a 6º: os arcos entre vizinhos de fila são retos, os que pulam vértices passam por cima, e todos apontam para a direita.
Numa ordenação topológica, todos os arcos apontam para a frente

Aplicação: Apache Airflow

O Apache Airflow é a ferramenta mais usada para orquestrar pipelines de dados, e modela cada pipeline literalmente como um DAG: cada tarefa é um vértice, e o operador >> declara um arco de dependência entre duas tarefas.

from datetime import datetime

from airflow import DAG
from airflow.operators.python import PythonOperator


def extrair_dados():
    print("extraindo os dados de vendas")


def transformar_dados():
    print("transformando os dados")


def carregar_dados():
    print("carregando os dados no banco")


with DAG("pipeline_vendas", start_date=datetime(2026, 1, 1), schedule="@daily") as dag:
    extrair = PythonOperator(task_id="extrair_dados", python_callable=extrair_dados)
    transformar = PythonOperator(task_id="transformar_dados", python_callable=transformar_dados)
    carregar = PythonOperator(task_id="carregar_dados", python_callable=carregar_dados)

    extrair >> transformar >> carregar  # arcos do DAG: extrair -> transformar -> carregar

Traduzindo para a linguagem deste artigo: cada PythonOperator é um vértice (uma tarefa que executa uma função Python); o bloco with DAG(...) declara o dígrafo em si, com um nome e uma frequência de execução; e a linha extrair >> transformar >> carregar declara os arcos \((extrair, transformar)\) e \((transformar, carregar)\), na ordem em que as tarefas precisam rodar. Na hora de executar, o Airflow segue uma ordenação topológica desse DAG.

Se alguém tentasse fechar o ciclo com carregar >> extrair, o Airflow recusaria o DAG na validação — a aciclicidade não é só uma curiosidade teórica, é uma regra que a ferramenta impõe.

Comparação: Grafo vs. Dígrafo
#

Característica Grafo Dígrafo
Arestas/Arcos Pares não ordenados \(\{u,v\}\) Pares ordenados \((u,v)\)
Grau \(d(v)\) (único) \(d^+(v)\) (saída) e \(d^-(v)\) (entrada)
Vértices especiais Fonte (\(d^- = 0\)), Sumidouro (\(d^+ = 0\))
Matriz de adjacência Simétrica Não necessariamente simétrica
Conexidade Conexo / Desconexo Forte / Unilateral / Fraco / Desconexo
Aperto de mãos \(\sum d(v) = 2|E|\) \(\sum d^+(v) = \sum d^-(v) = |E|\)

Tabela-Resumo
#

Conceito Expressão/Definição
Dígrafo \(D = (V,E)\), com \(E\) formado por pares ordenados (arcos)
Grau de entrada / saída \(d^-(v) = |\{(u,v) \in E(D)\}|\), \(d^+(v) = |\{(v,u) \in E(D)\}|\)
Fonte / Sumidouro \(d^-(v) = 0\) (fonte) / \(d^+(v) = 0\) (sumidouro)
Aperto de mãos para dígrafos \(\sum_{v} d^-(v) = \sum_{v} d^+(v) = |E(D)|\)
Grafo subjacente \(D\) sem direções, sem arestas paralelas
Orientação \(\vec{G}\): dígrafo obtido escolhendo uma direção para cada aresta de \(G\) (não é única)
Fortemente conexo Todo par \(v,w\) tem caminho de \(v\) a \(w\) e de \(w\) a \(v\)
Unilateralmente conexo Todo par \(v,w\) tem caminho de \(v\) a \(w\) ou de \(w\) a \(v\)
Fracamente conexo O grafo subjacente é conexo
Desconexo O grafo subjacente é desconexo
Matriz de adjacência \(a_{ij} = 1\) se \((v_i,v_j) \in E(D)\), \(0\) caso contrário
Matriz de incidência \(b_{ij} = +1\) (cauda), \(-1\) (cabeça), \(0\) caso contrário
Componentes fortemente conexos Maiores conjuntos de vértices que se alcançam mutuamente; Tarjan e Kosaraju em \(O(n+m)\)
Ordenação topológica Existe \(\iff\) \(D\) é DAG; algoritmo de Kahn em \(O(n+m)\)

Exercícios
#

Exercício 1: Fontes e sumidouros

Para o dígrafo \(D\) com vértices \(\{a,b,c,d\}\) e arcos \(\{(a,b),(a,c),(c,a),(c,b),(d,a),(d,c)\}\):

(a) Quem são as fontes? (b) Quem são os sumidouros? (c) \(a\) alcança todos os vértices? (d) \(D\) é fortemente conexo?

Solução:

(a) Fonte: \(d\) — nenhum arco chega a \(d\) (\(d^-(d) = 0\)).

(b) Sumidouro: \(b\) — nenhum arco parte de \(b\) (\(d^+(b) = 0\)).

(c) De \(a\): \(a \to b\) ✓, \(a \to c\) ✓, mas \(a\) não alcança \(d\). Falso.

(d) Não, pois \(d\) é fonte — nenhum vértice alcança \(d\). Logo \(D\) não é fortemente conexo.

Exercício 2: Conexidade

Classifique como fortemente, unilateralmente ou fracamente conexo:

(a) Cadeia linear \(a \to b \to c \to d \to e \to f\) (b) Ciclo \(a \to b \to c \to a\) com arco extra \(b \to d\)

Solução:

(a) Unilateralmente conexo: de \(a\) alcança-se todos os vértices. Para qualquer par \((v_i, v_j)\) com \(i < j\), existe caminho de \(v_i\) para \(v_j\). Mas \(f\) não alcança \(a\): não é fortemente conexo.

(b) O vértice \(d\) não tem nenhum arco saindo — é sumidouro, então não alcança ninguém. Como \(d\) não alcança nenhum outro vértice, o dígrafo não é fortemente conexo. Mas todo vértice alcança \(d\) (\(a \to b \to d\), \(b \to d\), \(c \to a \to b \to d\)), e entre \(a\), \(b\) e \(c\) o ciclo garante caminho nos dois sentidos — logo todo par tem caminho em pelo menos um sentido, e o dígrafo é unilateralmente conexo.

Resumo Final da Série
#

Com os dígrafos, completamos os fundamentos da teoria dos grafos. Recapitulando toda a jornada de “A Matemática das Conexões”:

Artigo Tema Central
Introdução História, Königsberg, aplicações
Definições \(G=(V,E)\), subgrafos, \(K_n\), cliques
Grau \(d(v)\), grafos regulares, Aperto de Mãos
Isomorfismo Bijeções que preservam estrutura, matrizes
Caminhos Passeio/trajeto/caminho/ciclo, conexidade, BFS e Dijkstra, bipartido
Árvores Acíclico+conexo, \(m=n-1\), centro, geradora
Eulerianos/Hamiltonianos Percursos especiais, Problema do Caixeiro-Viajante (TSP)
Planares Fórmula de Euler, \(K_5\) e \(K_{3,3}\), Kuratowski, coloração e Quatro Cores
Dígrafos Simetria quebrada, graus orientados, DAGs, ordenação topológica

Este é o último artigo da série “A Matemática das Conexões”. A teoria dos grafos é uma das áreas mais vivas da matemática contemporânea — conectando abstrações elegantes a problemas práticos que moldam a infraestrutura digital do mundo.

A Matemática das Conexões - Este artigo faz parte de uma série de artigos.
Parte 10: Esse Artigo

Relacionados