Ir para o conteúdo principal

Caminhos, Ciclos e Conexidade

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 6: Esse Artigo

Como saber se dois pontos de uma rede estão conectados, e por qual caminho mais curto? Até agora estudamos grafos como objetos estáticos — conjuntos de vértices e arestas. Mas a pergunta mais comum que fazemos sobre um grafo não é sobre sua estrutura em si: é sobre navegar por ele, partindo de um vértice e percorrendo arestas para chegar a outros.

Isso nos leva aos conceitos de passeios, trajetos, caminhos e ciclos — e à fundamental noção de conexidade.

Por que Caminhos e Ciclos Importam?
#

Navegar em um grafo — e saber se essa navegação é sequer possível — é a base de problemas centrais em ciência da computação e matemática aplicada:

  • Roteamento de redes: protocolos de roteamento (como os usados na internet) precisam encontrar caminhos entre roteadores, e o conceito de distância mais curta define diretamente o custo de uma rota.
  • Sistemas distribuídos: antes de rodar qualquer algoritmo distribuído, é comum verificar se a rede de máquinas é conexa — se não for, o sistema se divide em partições que não conseguem se comunicar.
  • Alocação de recursos: problemas de emparelhamento (como designar operários a máquinas, ou candidatos a vagas) são modelados como grafos bipartidos, e resolvê-los depende de entender a estrutura desses grafos.
  • Verificação de circuitos e protocolos: detectar ciclos em um grafo de dependências é o que evita, por exemplo, deadlocks em sistemas concorrentes ou referências circulares em builds de software.

Passeios, Trajetos e Caminhos
#

Existem diferentes maneiras de percorrer um grafo, e as distinções entre elas importam:

Passeio (Walk)

Um passeio em \(G\) é uma sequência de vértices

$$P = v_0, v_1, \ldots, v_k$$

tal que \((v_i, v_{i+1}) \in E(G)\) para \(0 \leq i \leq k-1\) — ou seja, entre cada par de vértices consecutivos da sequência existe uma aresta de \(G\).

O inteiro \(k\) (o número de arestas percorridas) é o comprimento do passeio, \(v_0\) é o seu início, \(v_k\) é o seu término, e dizemos que \(P\) é um passeio de \(v_0\) a \(v_k\).

Em um passeio, vértices e arestas podem se repetir.

Trajeto (Trail)

Um trajeto é um passeio em que nenhuma aresta se repete (mas vértices podem se repetir).

Caminho (Path)

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

A hierarquia é: todo caminho é um trajeto, e todo trajeto é um passeio. O inverso não é necessariamente verdadeiro.

Para fixar essas ideias, vamos usar o mesmo grafo \(G\) em vários exemplos ao longo deste artigo — a ideia é observar como os mesmos oito vértices sustentam conceitos cada vez mais específicos, de passeio até centro do grafo. Partimos do grafo \(G\) a seguir:

Grafo G de 8 vértices em formato de diamante à esquerda ligado a um quadrado com diagonal à direita
Grafo G — referência para os exemplos desta seção

Exemplo: passeio, trajeto e caminho no mesmo grafo

Enunciado: No grafo \(G\) acima, classifique as sequências \(P_1 = a, b, c, d, c, b, g\), \(P_2 = a, b, c, f, b, g\), \(P_3 = a, h, g\) e \(P_4 = g, h, a, b, f, e, c, d\) como passeio, trajeto ou caminho.

Solução:

  • \(P_1\) é um passeio de \(a\) a \(g\): os vértices \(b\) e \(c\) e as arestas \(\{b,c\}\) e \(\{c,d\}\) aparecem duas vezes.
  • \(P_2\) é um trajeto de \(a\) a \(g\): nenhuma aresta se repete, mas o vértice \(b\) aparece duas vezes — logo \(P_2\) não é um caminho.
  • \(P_3\) é um caminho entre \(a\) e \(g\): nenhum vértice se repete.
  • \(P_4\) também é um caminho, agora entre \(g\) e \(d\), passando por todos os demais vértices de \(G\) sem repetir nenhum.

A figura abaixo destaca \(P_1\) sobre o grafo \(G\), evidenciando onde o vértice \(c\) e a aresta \(\{c,d\}\) se repetem:

Grafo G com o passeio P1 destacado, mostrando o vértice c e a aresta {c,d} percorridos duas vezes
P1 = a,b,c,d,c,b,g — um passeio que repete vértice e aresta

Passeios e Trajetos Fechados
#

Um passeio (ou trajeto, ou caminho) é fechado se o vértice inicial coincide com o vértice final, isto é, \(v_0 = v_k\).

Trajeto Fechado

Um trajeto fechado é um passeio fechado em que todas as arestas são distintas.

Ciclo

Um ciclo é um passeio fechado em que, além disso, todos os vértices são distintos — com exceção do inicial e do final, que coincidem por definição. Em outras palavras, se \(v_0 = v_k\) e \(v_0, v_1, \ldots, v_{k-1}\) é um caminho, então \(P\) é um ciclo.

O comprimento de um ciclo é dado pelo seu número de arestas (equivalente ao seu número de vértices distintos).

Ciclo Par e Ciclo Ímpar

Um ciclo com um número ímpar de arestas é chamado de ciclo ímpar; caso contrário, é chamado de ciclo par.

Ciclos de comprimento 3 são triângulos.

O grafo \(G\) tem exemplos dos dois tipos: um ciclo de comprimento 4 (\(a,b,g,h,a\)) e um triângulo, que é um ciclo de comprimento 3 (\(a,b,h,a\)):

Grafo G com um ciclo par de comprimento 4 destacado em um painel, e um ciclo ímpar (triângulo) de comprimento 3 destacado em outro painel
Ciclo par a,b,g,h,a vs. ciclo ímpar a,b,h,a

Exemplo: passeio fechado, trajeto fechado e ciclos

Enunciado: Ainda no grafo \(G\), classifique \(P_5 = a, b, c, d, c, e, f, b, g, h, a\), \(P_6 = a, b, c, e, f, b, g, h, a\), \(P_7 = a, b, g, h, a\) e \(P_8 = a, b, h, a\).

Solução:

  • \(P_5\) é um passeio fechado, mas não um trajeto fechado: a aresta \(\{c,d\}\) é percorrida duas vezes.
  • \(P_6\) é um trajeto fechado — nenhuma aresta se repete — mas não é um ciclo, pois o vértice \(b\) é visitado duas vezes.
  • \(P_7\) é um ciclo par, de comprimento 4.
  • \(P_8\) é um ciclo ímpar, de comprimento 3 — um triângulo.
Aplicação: detectando dependências circulares

Ciclos não são só um conceito abstrato — eles aparecem (e causam dor de cabeça) sempre que um sistema de software modela dependências entre componentes como um grafo dirigido: se o módulo \(A\) depende de \(B\), desenhamos uma aresta \(A \to B\). Um projeto só compila (ou importa, ou instala) se esse grafo de dependências não tiver ciclos — caso contrário, “o que carregar primeiro” vira uma pergunta sem resposta.

import networkx as nx

deps = nx.DiGraph()
deps.add_edges_from([
    ("app", "auth"),
    ("app", "database"),
    ("auth", "database"),
    ("database", "config"),
    ("config", "utils"),
    ("utils", "auth"),  # fecha o ciclo auth -> database -> config -> utils -> auth
])

try:
    ciclo = nx.find_cycle(deps)
    print("Dependência circular encontrada:")
    for origem, destino in ciclo:
        print(f"  {origem} -> {destino}")
except nx.NetworkXNoCycle:
    print("Nenhum ciclo — grafo de dependências é um DAG, build seguro.")
Dependência circular encontrada:
  auth -> database
  database -> config
  config -> utils
  utils -> auth

O grafo abaixo mostra as dependências declaradas, com o ciclo encontrado destacado em laranja:

Grafo dirigido de dependências entre os módulos app, auth, database, config e utils, com o ciclo auth-database-config-utils-auth destacado em laranja
Grafo de dependências com o ciclo detectado por nx.find_cycle

O ciclo não é óbvio de enxergar módulo por módulo: auth importa database, que importa config, que importa utils, que por sua vez importa auth de volta. É exatamente esse tipo de dependência circular indireta que ferramentas como pip, npm ou sistemas de build detectam automaticamente com busca de ciclos em um grafo, em vez de depender de alguém perceber o problema manualmente.

O que cada ferramenta faz ao encontrar um ciclo varia: cargo (Rust) e a maioria dos build systems compilados recusam o build direto, já que compilar exige uma ordem estrita entre dependências; o make clássico detecta, avisa e quebra o ciclo arbitrariamente para seguir em frente; e linguagens interpretadas como Python e Node.js toleram o import circular em tempo de execução, entregando um módulo parcialmente inicializado no ponto em que o ciclo fecha — uma fonte clássica de bugs sutis. Por trás de todas essas estratégias está o mesmo fato de teoria dos grafos: uma ordem válida de build, instalação ou import é uma ordenação topológica, e ordenação topológica só existe se o grafo dirigido for acíclico — por isso a primeira coisa que essas ferramentas fazem é checar se ordenar é sequer possível.

Conexidade
#

Até aqui falamos em “existe um caminho entre \(v\) e \(w\)” de forma solta. Vamos formalizar essa ideia par a par antes de aplicá-la ao grafo inteiro.

Vértices Conexos

Dois vértices \(v\) e \(w\) são conexos em \(G\) quando existe algum caminho entre \(v\) e \(w\) em \(G\).

Considere agora o grafo \(H\) abaixo, formado por um agrupamento de seis vértices \(\{p,q,r,s,t,u\}\) e, separadamente, um triângulo \(\{x,y,z\}\):

Grafo H com um agrupamento de seis vértices à esquerda e um triângulo à direita, sem nenhuma aresta entre as duas partes
Grafo H — dois agrupamentos sem arestas entre eles

Exemplo: verificando conexidade par a par

Enunciado: No grafo \(H\), \(p\) e \(t\) são conexos? E \(p\) e \(x\)?

Solução:

  • \(p\) e \(t\) são conexos: o caminho \(P = p, r, s, t\) liga os dois.
  • \(p\) e \(x\) não são conexos: não existe nenhum caminho entre eles, já que nenhuma aresta liga o agrupamento \(\{p,\ldots,u\}\) ao triângulo \(\{x,y,z\}\).
Grafo Conexo

Um grafo \(G\) é conexo quando todo par de vértices distintos de \(G\) é conexo, isto é, quando existe um caminho entre qualquer par de vértices \(u, v \in V(G)\). Caso contrário, \(G\) é desconexo.

O grafo \(H\) do exemplo acima é desconexo — mas cada uma de suas duas partes, isoladamente, é conexa. Isso nos leva à definição de componente conexo.

Componente Conexo

Seja \(G = (V, E)\) um grafo e \((V_1, V_2, \ldots, V_k)\) uma partição de \(V\) (isto é, \(V_1 \cup V_2 \cup \cdots \cup V_k = V\) e \(V_i \cap V_j = \emptyset\) para \(i \neq j\)). O subgrafo induzido \(G[V_i]\) é um componente conexo de \(G\) se:

  1. \(G[V_i]\) é conexo, e
  2. para todo vértice \(v \in V - V_i\), o subgrafo \(G[V_i \cup \{v\}]\) não é conexo.

A condição 2 é o que força cada componente a ser o maior subgrafo conexo possível: se qualquer vértice de fora for acrescentado a \(V_i\), o resultado deixa de ser conexo. É nesse sentido que os componentes conexos são os “pedaços conexos maximais” de \(G\).

Denotamos por \(\omega(G)\) o número de componentes conexos de \(G\). Um grafo é conexo se e somente se \(\omega(G) = 1\).

Cuidado com a notação

\(\omega(G)\) para o número de componentes é comum em textos em português, mas não é universal: muitos livros em inglês escrevem \(c(G)\) ou \(k(G)\) e reservam \(\omega(G)\) para o número de clique, o tamanho da maior clique de \(G\). Vale conferir a convenção ao consultar outra fonte — neste artigo, \(c(G)\) é o centro do grafo, definido mais adiante.

Por que isso importa

Muitos algoritmos sobre grafos assumem, como pré-condição, que o grafo de entrada é conexo. Quando isso não acontece, uma etapa comum é “pré-processar” o grafo para separá-lo em seus componentes conexos e resolver o problema em cada um deles isoladamente.

Voltando ao grafo \(H\), destacamos abaixo seus dois componentes conexos:

Grafo H com o agrupamento {p,q,r,s,t,u} em uma cor e o triângulo {x,y,z} em outra cor, evidenciando os dois componentes conexos
Os dois componentes conexos de H

Exemplo: contando componentes conexos

Enunciado: Quantos componentes conexos tem o grafo \(H\)? E um grafo \(G’\) com \(V(G’) = \{1, \ldots, 10\}\) particionado em \(V_1 = \{1,2,3,4\}\) (com arestas \(\{1,2\},\{1,4\},\{2,3\},\{2,4\},\{3,4\}\)), \(V_2 = \{5,6\}\) (com a aresta \(\{5,6\}\)), \(V_3 = \{7\}\) (vértice isolado) e \(V_4 = \{8,9,10\}\) (um triângulo)?

Solução: No grafo \(H\), o subgrafo induzido por \(\{p,q,r,s,t,u\}\) é conexo, e o subgrafo induzido por \(\{x,y,z\}\) também é conexo — e nenhum vértice pode ser movido de um para o outro sem quebrar a conexidade. Logo esses são os dois componentes conexos de \(H\), e \(\omega(H) = 2\).

Um grafo pode ter vários componentes, inclusive vértices isolados. Cada \(G’[V_i]\) é conexo e maximal, logo \(\omega(G’) = 4\).

A figura abaixo mostra \(G’\), com cada componente conexo em uma cor:

Grafo numérico de 10 vértices em quatro grupos coloridos: um diamante com diagonal, uma aresta simples, um vértice isolado e um triângulo
Um grafo com quatro componentes conexos, incluindo um vértice isolado

E o grafo \(G\) que vimos na seção anterior (vértices \(\{a,\ldots,h\}\))? Todo par de vértices de \(G\) é conexo — por exemplo, \(P_4\) já mostrou um caminho entre \(g\) e \(d\) passando por todos os demais vértices. Logo \(G\) é conexo e \(\omega(G) = 1\).

Distância, Excentricidade e Diâmetro
#

Com o conceito de conexidade em mãos, podemos medir “distâncias” dentro de um grafo:

Distância

A distância \(d(v, w)\) entre dois vértices \(v\) e \(w\) é o comprimento do menor caminho (caminho mais curto) entre eles — esse caminho mais curto também é chamado de geodésica. Se \(v\) e \(w\) não são conexos (isto é, estão em componentes conexos distintos de \(G\)), não existe caminho entre eles, e definimos \(d(v,w) = \infty\).

As definições a seguir — excentricidade, diâmetro e centro — pressupõem que \(G\) é um grafo conexo (caso contrário, algum \(e(v)\) poderia ser infinito).

Excentricidade

Seja \(G\) um grafo conexo. A excentricidade \(e(v)\) de um vértice \(v\) é a maior distância de \(v\) a qualquer outro vértice:

$$e(v) = \max_{w \in V} d(v, w)$$
Diâmetro

O diâmetro de \(G\) é a maior excentricidade:

$$\text{diam}(G) = \max_{v \in V} e(v) = \max_{v,w \in V} d(v,w)$$
Centro

O centro de \(G\) é o conjunto dos vértices com menor excentricidade:

$$c(G) = \{v \in V : e(v) = \min_{u \in V} e(u)\}$$

Vamos calcular essas três medidas no grafo \(G\) (vértices \(\{a,\ldots,h\}\)) que já usamos nas seções anteriores.

Exemplo: excentricidade, diâmetro e centro de \(G\)

Enunciado: Calcule a excentricidade de cada vértice de \(G\), o diâmetro de \(G\) e seu centro.

Solução: Partindo de \(a\), as distâncias até os demais vértices são: \(d(a,b)=1\), \(d(a,h)=1\), \(d(a,g)=2\), \(d(a,c)=2\), \(d(a,f)=2\), \(d(a,e)=3\), \(d(a,d)=3\). A maior delas é 3, logo \(e(a) = 3\).

Repetindo o cálculo para todos os vértices de \(G\), obtemos:

$$e(a) = e(d) = e(e) = e(g) = e(h) = 3 \qquad e(b) = e(c) = e(f) = 2$$

Como a maior excentricidade é 3, temos \(\text{diam}(G) = 3\). E como \(b\), \(c\) e \(f\) são os vértices de menor excentricidade (2), o centro é \(c(G) = \{b, c, f\}\) — exatamente os três vértices que ficam entre o “lado esquerdo” (\(a,g,h\)) e o “lado direito” (\(d,e\)) de \(G\), o que faz sentido intuitivamente: vértices por onde passam os caminhos entre as duas partes do grafo tendem a estar mais perto de todo mundo.

A figura a seguir mostra a excentricidade de cada vértice e destaca o centro:

Grafo G com a excentricidade anotada acima de cada vértice e os vértices b, c, f do centro destacados em verde
Excentricidades de G e seu centro {b,c,f}

Exemplo: o caso do caminho \(P_5\)

Enunciado: Calcule a excentricidade de cada vértice, o diâmetro e o centro do caminho \(P_5 = v_1 - v_2 - v_3 - v_4 - v_5\).

Solução: Em um grafo caminho, a excentricidade cresce de forma bem previsível a partir das pontas:

  • \(e(v_1) = e(v_5) = 4\) (as pontas precisam cruzar o caminho inteiro)
  • \(e(v_2) = e(v_4) = 3\)
  • \(e(v_3) = 2\) (o vértice central)

Diâmetro = 4. Centro = \(\{v_3\}\). Em qualquer \(P_n\), o centro é sempre o(s) vértice(s) mais próximo(s) do meio da sequência.

Conferindo com código

Tudo que calculamos à mão para o grafo \(G\) — excentricidades, diâmetro e centro — sai em poucas linhas com o networkx:

import networkx as nx

G = nx.Graph()
G.add_edges_from([
    ("a","b"), ("a","h"), ("b","h"), ("b","g"), ("g","h"),
    ("b","c"), ("b","f"), ("c","f"), ("c","e"), ("e","f"), ("c","d"),
])

print("Excentricidades:", nx.eccentricity(G))
print("Diâmetro:", nx.diameter(G))
print("Centro:", nx.center(G))
Excentricidades: {'a': 3, 'b': 2, 'h': 3, 'g': 3, 'c': 2, 'f': 2, 'e': 3, 'd': 3}
Diâmetro: 3
Centro: ['b', 'c', 'f']

Os valores batem exatamente com o cálculo manual. A vantagem de usar uma biblioteca não é só evitar erro de conta: o mesmo código funciona sem nenhuma mudança para um grafo com 8 ou com 8 milhões de vértices — o que fizemos manualmente para \(G\) só é viável à mão porque o grafo é pequeno.

Calculando distâncias: a busca em largura
#

Nos exemplos acima, lemos as distâncias no desenho de \(G\), e o networkx fez a mesma conta em uma linha. Mas como se calcula a distância entre dois vértices de um grafo com milhões deles? Quando as arestas não têm pesos, a resposta é a busca em largura (BFS, do inglês breadth-first search).

A ideia é explorar o grafo em camadas a partir de uma origem \(v\): primeiro o próprio \(v\), à distância 0; depois todos os seus vizinhos, à distância 1; depois os vizinhos desses vizinhos que ainda não apareceram, à distância 2; e assim por diante. Uma fila garante essa ordem: cada vértice descoberto entra no fim da fila, e os vértices são processados na ordem em que entraram.

Busca em Largura (BFS)

Dado um vértice de origem \(v\):

  1. Marque \(d(v, v) = 0\) e coloque \(v\) na fila.
  2. Enquanto a fila não estiver vazia, retire o primeiro vértice \(u\) da fila. Para cada vizinho \(w\) de \(u\) que ainda não tem distância marcada, marque \(d(v, w) = d(v, u) + 1\) e coloque \(w\) no fim da fila.

Ao final, cada vértice alcançável a partir de \(v\) tem sua distância marcada; os que ficaram sem marca estão em outro componente conexo, a distância \(\infty\).

Por que a distância marcada é a menor possível? Os vértices saem da fila em ordem crescente de distância — todos os da camada \(k\) antes de qualquer um da camada \(k+1\) —, então a primeira vez que um vértice \(w\) aparece é a partir de um vértice da camada mais próxima possível da origem. E o custo é baixo: cada vértice entra na fila uma única vez, e cada aresta é examinada duas vezes, uma a partir de cada extremo, o que dá tempo \(O(n + m)\).

Aplicada ao grafo \(G\) a partir de \(a\), a BFS reproduz as distâncias que calculamos à mão:

from collections import deque

import networkx as nx

G = nx.Graph()
G.add_edges_from([
    ("a", "b"), ("a", "h"), ("b", "h"), ("b", "g"), ("g", "h"),
    ("b", "c"), ("b", "f"), ("c", "f"), ("c", "e"), ("e", "f"), ("c", "d"),
])


def distancias(G, origem):
    """Distância de origem a cada vértice alcançável, pela busca em largura."""
    dist = {origem: 0}
    fila = deque([origem])
    while fila:
        v = fila.popleft()
        for w in G.neighbors(v):
            if w not in dist:  # primeira vez que w aparece
                dist[w] = dist[v] + 1
                fila.append(w)
    return dist


d_a = distancias(G, "a")
print(d_a)
# Saída: {'a': 0, 'b': 1, 'h': 1, 'g': 2, 'c': 2, 'f': 2, 'e': 3, 'd': 3}
print(max(d_a.values()))  # 3, a excentricidade de a

A figura abaixo desenha \(G\) nessas camadas. As arestas grossas são as que a busca usou para descobrir cada vértice; as tracejadas foram examinadas e descartadas, porque o vértice do outro lado já tinha distância marcada:

O grafo G redesenhado em quatro colunas, uma por distância a partir de a: d = 0 com a; d = 1 com b e h; d = 2 com c, f e g; d = 3 com d e e. Sete arestas grossas em roxo, a-b, a-h, b-c, b-f, b-g, c-d e c-e, formam a árvore da busca em largura; as outras quatro, b-h, c-f, f-e e h-g, aparecem tracejadas em cinza.
Busca em largura a partir de a: camadas por distância e a árvore da busca

A maior distância encontrada é a excentricidade de \(a\). Rodando a BFS a partir de cada vértice, obtemos todas as excentricidades — e, com elas, o diâmetro e o centro — em tempo \(O(n(n + m))\). A mesma busca volta mais adiante, neste artigo, para testar se um grafo é bipartido, e no artigo sobre árvores, onde produz uma árvore geradora. Ela foi descrita por Konrad Zuse em 1945, numa tese que só seria publicada em 1972, e reinventada por Edward F. Moore em 1959 para achar a saída de labirintos (veja o verbete Breadth-first search).

Grafos ponderados: o algoritmo de Dijkstra

Num mapa de ruas, as arestas têm comprimentos diferentes, e o caminho com menos arestas nem sempre é o mais curto. Num grafo ponderado, cada aresta tem um peso — distância, tempo, custo —, e o comprimento de um caminho passa a ser a soma dos pesos das suas arestas. A BFS, que só conta arestas, deixa de servir.

O algoritmo de Dijkstra, concebido por Edsger W. Dijkstra em 1956 e publicado em 1959, adapta a mesma ideia: troca a fila comum por uma fila de prioridade e, a cada passo, fixa a distância do vértice ainda não fixado que está mais perto da origem. O algoritmo exige pesos não negativos e, com a fila de prioridade feita num heap, roda em tempo \(O((n + m) \log n)\). Aplicativos de rota usam variações dele, com muitas otimizações para grafos do tamanho de um país.

No grafo abaixo, o caminho com menos arestas de \(A\) a \(B\) é a aresta direta, de peso 4; o mais curto passa por \(C\) e \(D\) e tem comprimento 3:

import networkx as nx

G = nx.Graph()
G.add_weighted_edges_from([("A", "B", 4), ("A", "C", 1), ("C", "D", 1), ("D", "B", 1)])

print(nx.shortest_path(G, "A", "B"))         # ['A', 'B'] (sem pesos: BFS)
print(nx.dijkstra_path(G, "A", "B"))         # ['A', 'C', 'D', 'B']
print(nx.dijkstra_path_length(G, "A", "B"))  # 3

Grafos Especiais
#

Alguns grafos aparecem com tanta frequência em aplicações e exemplos que vale a pena nomeá-los e reconhecê-los de imediato.

Grafo Completo \(K_n\) e Grafo Trivial

O grafo completo \(K_n\) tem \(n\) vértices, com todo par de vértices ligado por uma aresta. Em particular, \(K_1\) — o grafo completo de um único vértice, sem arestas — é chamado de grafo trivial.

Grafo Nulo \(N_n\)

O grafo nulo \(N_n\) tem \(n\) vértices e nenhuma aresta — o extremo oposto do grafo completo.

Os dois casos extremos, lado a lado:

Painel com K1, o grafo trivial de um único vértice sem arestas, e N4, um grafo nulo de quatro vértices sem nenhuma aresta
Grafo trivial K1 e grafo nulo N4

Ciclos e Caminhos
#

Ciclo \(C_n\) e Caminho \(P_n\)
  • \(C_n\): ciclo com \(n\) vértices (\(n \geq 3\)), 2-regular
  • \(P_n\): caminho com \(n\) vértices (\(n-1\) arestas), com dois vértices de grau 1 e \(n-2\) de grau 2

Os primeiros ciclos da família \(C_n\):

Família de grafos ciclo C3, C4, C5 e C6, cada um formando um polígono regular
Grafos ciclo C3 a C6

E os primeiros caminhos da família \(P_n\):

Família de grafos caminho P2, P3, P4 e P5, cada um formando uma linha de vértices conectados em sequência
Grafos caminho P2 a P5

Grafos Bipartidos
#

Uma classe importante de grafos é aquela em que os vértices podem ser divididos em dois grupos, com arestas apenas entre os grupos.

Grafo Bipartido

Um grafo \(G = (V, E)\) é bipartido se \(V\) pode ser particionado em dois subconjuntos \(V_1\) e \(V_2\) (com \(V_1 \cup V_2 = V\) e \(V_1 \cap V_2 = \emptyset\)) tais que toda aresta de \(G\) tem um extremo em \(V_1\) e o outro em \(V_2\). Dizemos que \((V_1, V_2)\) é uma bipartição de \(V\).

\(V_1\) e \(V_2\) são independentes

Como toda aresta liga \(V_1\) a \(V_2\), não pode existir aresta entre dois vértices de \(V_1\), nem entre dois vértices de \(V_2\). Ou seja, \(V_1\) e \(V_2\) são conjuntos independentes em \(G\) — essa observação vai ser útil para entender por que ciclos em grafos bipartidos são sempre pares, mais adiante.

Uma forma útil de visualizar um grafo bipartido é desenhar \(V_1\) e \(V_2\) como duas colunas separadas, deixando explícito que toda aresta cruza de uma coluna para a outra:

Diagrama de bipartição com vértices v1,v2,v3 em uma coluna azul à esquerda e w1,w2,w3,w4 em uma coluna laranja à direita, com arestas cruzando apenas entre as colunas
Bipartição genérica: arestas só entre V1 e V2

Exemplo: alocação de operários a máquinas

Enunciado: Como modelar, como um grafo bipartido, o problema de saber quais operários de uma fábrica sabem operar quais máquinas?

Solução: Sejam os operários \(x_1, x_2, \ldots, x_r\) formando \(V_1\) e as máquinas \(y_1, y_2, \ldots, y_s\) formando \(V_2\). Existe uma aresta entre o operário \(x_i\) e a máquina \(y_j\) sempre que \(x_i\) é capaz de operar \(y_j\). Esse grafo é bipartido por construção: nunca há aresta entre dois operários ou entre duas máquinas, só entre um operário e uma máquina. É esse tipo de estrutura que problemas de emparelhamento (matching) exploram para decidir, por exemplo, quem deve operar o quê.

Diagrama bipartido com operários x1 a x4 em uma coluna azul à esquerda e máquinas y1 a y3 em uma coluna laranja à direita, com arestas indicando quem sabe operar o quê
Grafo bipartido de alocação: operários x máquinas

Grafo Bipartido Completo \(K_{p,q}\)

O grafo bipartido completo \(K_{p,q}\) tem \(|V_1| = p\), \(|V_2| = q\), e toda aresta possível entre \(V_1\) e \(V_2\). Portanto, \(|E(K_{p,q})| = p \cdot q\).

Um exemplo pequeno é \(K_{2,3}\), com todas as \(2 \times 3 = 6\) arestas possíveis entre os dois lados:

Grafo bipartido completo K2,3 com dois vértices v1,v2 à esquerda e três vértices w1,w2,w3 à direita, com todas as 6 arestas possíveis entre eles
K2,3 — grafo bipartido completo

Exemplos de grafos bipartidos:

  • Caminhos \(P_n\) (para \(n \geq 2\)): vértices de posição par em \(V_1\), ímpares em \(V_2\)
  • Ciclos pares \(C_{2k}\): vértices alternados em \(V_1\) e \(V_2\)

A figura a seguir mostra \(C_4\) desenhado de duas formas — como polígono e como bipartição — e traz \(C_5\) como desafio: tente achar uma bipartição válida para ele (spoiler: não existe, como o teorema a seguir vai explicar).

C4 desenhado como quadrado (layout de ciclo) e, ao lado, redesenhado com V1={a,c} e V2={b,d} em colunas separadas, mostrando a mesma estrutura; e C5 em layout de ciclo como desafio
C4 nas duas formas equivalentes, e C5 como desafio

Um caso clássico: \(K_{3,3}\)

O grafo bipartido completo \(K_{3,3}\) é famoso por outro motivo: ele é o protagonista do “problema das três casas e três utilidades” — três casas que precisam se ligar a água, luz e gás sem que nenhuma linha se cruze. É um exemplo clássico usado para discutir planaridade de grafos, tema de um próximo artigo desta série.

Caracterização de Grafos Bipartidos
#

É um problema importante decidir se um grafo é bipartido ou não. O teorema a seguir mostra que basta procurar por ciclos ímpares.

Teorema

Um grafo \(G\) é bipartido se e somente se não contém nenhum ciclo de comprimento ímpar.

Prova (\(\Rightarrow\)): Seja \(G\) um grafo bipartido com bipartição \((V_1, V_2)\), e seja \(C = v_1, v_2, \ldots, v_k, v_1\) um ciclo em \(G\). Suponha \(v_1 \in V_1\). Como \(V_1\) e \(V_2\) são independentes, cada aresta do ciclo troca de lado: \(v_2 \in V_2\), \(v_3 \in V_1\), \(v_4 \in V_2\), e assim sucessivamente — de forma geral, \(v_{2i-1} \in V_1\) e \(v_{2i} \in V_2\). Como \((v_k, v_1) \in E\) e \(v_1 \in V_1\), o vértice \(v_k\) precisa estar em \(V_2\) (pois \(G\) é bipartido, e uma aresta nunca liga dois vértices do mesmo lado). Logo \(k = 2i\) para algum \(i\), ou seja, \(k\) é par — e portanto \(C\) é um ciclo par. Portanto, como isso vale para qualquer ciclo de \(G\), um grafo bipartido não contém ciclo ímpar.

Prova (\(\Leftarrow\)): Suponha agora que \(G\) não contém nenhum ciclo ímpar. Podemos supor \(G\) conexo: se não for, construímos uma bipartição para cada componente conexo e juntamos os lados correspondentes, já que nenhuma aresta liga componentes diferentes. Vamos construir uma bipartição de \(G\) explicitamente.

Seja \(v\) um vértice qualquer de \(G\), e defina:

$$V_1 = \{w \in V(G) : d(v,w) \text{ é par}\} \qquad V_2 = \{u \in V(G) : d(v,u) \text{ é ímpar}\}$$

Como \(G\) é conexo, todo vértice está a uma distância finita de \(v\), par ou ímpar (nunca as duas coisas), temos \(V_1 \cup V_2 = V(G)\) e \(V_1 \cap V_2 = \emptyset\). Falta mostrar que \((V_1, V_2)\) é de fato uma bipartição, isto é, que não existe aresta dentro de \(V_1\) nem dentro de \(V_2\).

Suponha, por contradição, que isso seja falso: existe uma aresta \((w_i, w_j)\) com \(w_i, w_j \in V_1\) (o caso de uma aresta dentro de \(V_2\) é análogo). Sejam \(P_i\) e \(P_j\) os caminhos mais curtos de \(v\) até \(w_i\) e de \(v\) até \(w_j\), respectivamente — ambos de comprimento par, já que \(w_i, w_j \in V_1\). Seja \(z\) o vértice comum a \(P_i\) e \(P_j\) mais distante de \(v\) (existe pelo menos o próprio \(v\) em comum). A figura abaixo esquematiza essa construção:

Diagrama esquemático com vértice v no topo, um trecho comum cinza até o vértice z, de onde saem dois caminhos coloridos até wi e wj, fechados por uma aresta tracejada entre wi e wj
Construção geométrica da prova: v, z, Pi, Pj e a aresta que fecha o ciclo ímpar

O subcaminho de \(P_i\) que vai de \(v\) até \(z\), denotado \(P_i(v{-}z)\), tem o mesmo comprimento do subcaminho \(P_j(v{-}z)\) — caso contrário, um dos dois caminhos não seria o mais curto até seu respectivo destino (bastaria trocar o trecho mais longo pelo trecho mais curto do outro caminho para obter um atalho). Como \(P_i\) e \(P_j\) têm comprimento par no total, e as partes de \(v\) até \(z\) têm o mesmo comprimento, os subcaminhos restantes \(P_i(z{-}w_i)\) e \(P_j(z{-}w_j)\) têm a mesma paridade entre si — ambos pares ou ambos ímpares.

Mas existe a aresta \((w_i, w_j)\) por hipótese. Logo o percurso \(P_i(z{-}w_i) \cup (w_i,w_j) \cup P_j(z{-}w_j)\) — de \(z\) até \(w_i\), depois até \(w_j\) pela aresta, e de volta a \(z\) por \(P_j\) — forma um ciclo de comprimento \(\text{comp}(P_i(z{-}w_i)) + 1 + \text{comp}(P_j(z{-}w_j))\). Como as duas parcelas têm a mesma paridade, essa soma é sempre ímpar (par + par + 1, ou ímpar + ímpar + 1). Isso produz um ciclo ímpar em \(G\) — contradizendo a hipótese de que \(G\) não contém ciclo ímpar.

Essa contradição mostra que a aresta \((w_i, w_j)\) não pode existir (e, de forma análoga, nenhuma aresta dentro de \(V_2\) pode existir). Logo \((V_1, V_2)\) é de fato uma bipartição de \(G\), ou seja, \(G\) é bipartido.

Portanto, com as duas direções, um grafo é bipartido se e somente se não contém nenhum ciclo de comprimento ímpar, que é o que queríamos demonstrar. \(\blacksquare\)

Corolário

Todo ciclo \(C_n\) com \(n\) par é bipartido. Todo ciclo \(C_n\) com \(n\) ímpar não é bipartido.

Aplicação: a prova vira algoritmo

A construção usada na prova (\(\Leftarrow\)) — colorir cada vértice pela paridade de sua distância até um vértice de referência — não é só um argumento teórico: é literalmente o algoritmo que bibliotecas de grafos usam para testar bipartição. Implementando a mesma ideia com a busca em largura (BFS) que vimos na seção de distâncias:

from collections import deque
import networkx as nx

def bipartir(G, origem):
    """V1 = distância par até origem, V2 = distância ímpar — a mesma
    construção usada na prova do teorema."""
    cor = {origem: 0}
    fila = deque([origem])
    while fila:
        v = fila.popleft()
        for viz in G.neighbors(v):
            if viz not in cor:
                cor[viz] = 1 - cor[v]
                fila.append(viz)
            elif cor[viz] == cor[v]:
                return None  # aresta dentro do mesmo lado: não é bipartido
    V1 = sorted(v for v, c in cor.items() if c == 0)
    V2 = sorted(v for v, c in cor.items() if c == 1)
    return V1, V2

print("C6:", bipartir(nx.cycle_graph(6), 0))
print("C5:", bipartir(nx.cycle_graph(5), 0))
C6: ([0, 2, 4], [1, 3, 5])
C5: None

Para \(C_6\) (ciclo par), a função devolve exatamente a bipartição alternada esperada: vértices em posição par de um lado, ímpar do outro — mostrada na figura abaixo. Para \(C_5\) (ciclo ímpar), a BFS eventualmente encontra uma aresta entre dois vértices da mesma cor — a mesma contradição explorada na prova — e a função retorna None, confirmando que não existe bipartição.

Grafo C6 com vértices coloridos alternadamente em azul e laranja pela busca em largura, confirmando a bipartição
C6 bipartido pelo algoritmo de BFS

A biblioteca networkx já traz essa checagem pronta em nx.is_bipartite, que concorda com a nossa implementação:

import networkx as nx

print(nx.is_bipartite(nx.cycle_graph(6)))  # True
print(nx.is_bipartite(nx.cycle_graph(5)))  # False
Aplicação: revisitando o Karate Club de Zachary

O grafo do Karate Club de Zachary — a rede social de 34 membros que já vimos ao estudar grau de vértice — é um bom teste para as propriedades deste artigo em um grafo real, bem maior do que os exemplos que resolvemos à mão:

import networkx as nx

G = nx.karate_club_graph()

print("Conexo:", nx.is_connected(G))
print("Diâmetro:", nx.diameter(G))
print("Centro:", nx.center(G))
print("Bipartido:", nx.is_bipartite(G))
Conexo: True
Diâmetro: 5
Centro: [0, 1, 2, 3, 8, 13, 19, 31]
Bipartido: False

O clube inteiro é um único componente conexo (\(\omega(G)=1\)), com diâmetro 5 — um pouco maior que o do grafo \(G\) deste artigo, mas ainda pequeno para uma rede social real. Vale notar que o vértice \(33\) (o administrador do clube, de maior grau — \(17\) conexões, como vimos no artigo sobre grau) não está no centro: sua excentricidade é \(4\), contra \(3\) do vértice \(0\). Ter mais conexões não garante estar mais “no meio” da rede em termos de distância — são noções de centralidade diferentes.

E o grafo não é bipartido: uma rede de amizades real tem grupos que formam triângulos (\(A\) e \(B\) são amigos, \(B\) e \(C\) são amigos, e \(A\) e \(C\) acabam se tornando amigos também), e todo triângulo é um ciclo ímpar — o que já basta, pelo teorema de caracterização, para eliminar qualquer chance de bipartição.

Exercícios
#

Os exercícios a seguir aplicam os conceitos de passeio, trajeto, caminho e bipartição vistos neste artigo, em ordem crescente de dificuldade.

Exercício 1: Classificação de sequências

Enunciado: Classifique cada sequência abaixo como passeio, trajeto, caminho ou nenhum desses, em um grafo com vértices \(\{1,2,3,4,5\}\) e arestas \(\{1,2\}, \{2,3\}, \{3,4\}, \{4,5\}, \{1,3\}, \{3,5\}\):

(a) \(1, 2, 3, 4, 5, 3, 1\) (b) \(1, 3, 2, 4\) (c) \(1, 2, 3, 5, 4\) (d) \(1, 3, 5, 4, 3, 2\)

Solução: (a) É um trajeto (fechado) — a sequência percorre as 6 arestas do grafo, cada uma exatamente uma vez: \(\{1,2\}, \{2,3\}, \{3,4\}, \{4,5\}, \{3,5\}, \{1,3\}\). Como nenhuma aresta se repete, é um trajeto — e como \(v_0 = v_6 = 1\), é fechado. Não é um caminho nem um ciclo, pois o vértice 3 é visitado duas vezes (nas posições 3 e 6). (b) Não é válido — não existe aresta \(\{2,4\}\). (c) É um caminho — nenhum vértice se repete e todas as arestas existem: \(\{1,2\}, \{2,3\}, \{3,5\}, \{5,4\}\). ✓ (d) É um trajeto — nenhuma aresta se repete, mas o vértice 3 aparece duas vezes.

O grafo usado no exercício, para visualizar as sequências acima:

Grafo com vértices 1 a 5, onde o vértice 3 é o hub central conectado a 1, 2, 4 e 5
Grafo do Exercício 1

Exercício 2: Bipartido?

Enunciado: Determine se o ciclo \(C_6\) é bipartido e, se for, exiba a bipartição.

Solução: \(C_6\) tem comprimento par, logo é bipartido pelo teorema da caracterização. Para \(C_6 = v_1-v_2-v_3-v_4-v_5-v_6-v_1\), a bipartição é \(V_1 = \{v_1, v_3, v_5\}\) e \(V_2 = \{v_2, v_4, v_6\}\). \(\blacksquare\)

Resumo
#

Conceito Descrição
Passeio Sequência de vértices \(v_0, \ldots, v_k\) com \((v_i,v_{i+1}) \in E\); repetições permitidas
Trajeto Passeio sem arestas repetidas
Caminho Passeio sem vértices repetidos
Trajeto fechado Passeio fechado (\(v_0=v_k\)) sem arestas repetidas
Ciclo Passeio fechado sem vértices internos repetidos
Ciclo par / ímpar Ciclo com número par / ímpar de arestas
Vértices conexos Existe caminho entre eles
Conexo Todo par de vértices é conexo
Componente conexo Subgrafo induzido conexo e maximal (partição de \(V\))
Distância \(d(v,w)\) Comprimento do menor caminho; \(\infty\) se \(v,w\) desconexos
Busca em largura (BFS) Calcula as distâncias a partir de uma origem em \(O(n+m)\)
Algoritmo de Dijkstra Distâncias em grafos com pesos não negativos, com fila de prioridade
Excentricidade \(e(v)\) Maior distância de \(v\) a qualquer outro vértice
Diâmetro Maior excentricidade do grafo (pressupõe \(G\) conexo)
Centro \(c(G)\) Vértices de menor excentricidade
Grafo trivial / nulo \(K_1\) (um vértice, sem arestas) / \(N_n\) (\(n\) vértices, sem arestas)
Bipartido \(V = V_1 \cup V_2\), \(V_1 \cap V_2 = \emptyset\), arestas só entre \(V_1\) e \(V_2\)
Bipartido ↔ Sem ciclos de comprimento ímpar

Próximos Passos
#

Respondemos à pergunta de abertura: sabemos agora reconhecer se dois pontos de um grafo estão conectados, medir a distância mais curta entre eles e decidir se a estrutura inteira é bipartida. Mas passeios, ciclos e conexidade não respondem a uma pergunta igualmente importante: qual é a menor quantidade de arestas capaz de manter um grafo conexo, sem nenhum ciclo sobrando? Essa é a pergunta que motiva as árvores, tema do próximo artigo da série — a estrutura mais fundamental da ciência da computação.

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

Relacionados