Aula - Ciclo Euleriano

Aula por Arthur Lobo

Nessa aula nós vamos falar sobre algoritmos para achar um ciclo euleriano e alguns problemas que os utilizam.

Um Caminho Euleriano é definido como um caminho no grafo que começa num vértice V_I, passa por todas as arestas exatamente uma vez, e termina num vértice V_F \ne V_I. Ou seja, um Caminho Euleriano num grafo G com M arestas é uma sequência de arestas E_{i_1},E_{i_2},...,E_{i_M}, tal que todas são diferentes dois a dois, elas formam um caminho contínuo (saindo de uma aresta é possível ir para a próxima) e o início é diferente do fim.

Um Ciclo Euleriano é um "caso especial" do Caminho Euleriano: um caminho no grafo que começa num vértice qualquer V_I, passa por todas as arestas exatamente uma vez, e termina num vértice V_F = V_I. Ou seja, a única diferença é que ele começa e termina no mesmo vértice, por isso o nome Ciclo.

Agora vamos ver como saber se existe e encontrar um Ciclo Euleriano, que é mais simples do que o Caminho Euleriano, vamos dividir entre 2 casos: quando o Grafo é Direcionado e quando é Não-direcionado.

Ciclo Euleriano em Grafo Direcionado

Imagine o seguinte problema: Dado um grafo direcionado G com N vértices e M arestas, diga se existe um Ciclo Euleriano em G e, caso exista, imprima a ordem dos vértices que você passa em algum Ciclo Euleriano.

A primeira vista, saber se existe um Ciclo Euleriano parece ser difícil, mas na verdade não é. Um Ciclo Euleriano existe em um grafo direcionado, se e somente se essas 2 condições forem verdadeiras:

  • Todos os vértices tem seu grau de entrada igual ao seu grau de saída, ou seja, todos os vértices tem mesma quantidade de arestas entrando e saindo.
  • Ignorando os vértices isolados (com grau de entrada e saída sendo 0), o grafo é fracamente conexo, ou seja, se nós "transformarmos" todas as aresta direcionadas em não-direcionadas, o grafo vira conexo.

É fácil ver que essas duas condições são necessárias, pois a quantidade de vezes que se entra num vértice é igual à quantidade de vezes que sai dele, e se o grafo for quebrado, não é possível passar por  todas as arestas. Mas além de serem necessárias, essa condições também são suficientes! O que quer dizer que basta checar elas para ver se existe um Ciclo Euleriano.

A ideia é que se todos os vértices tem grau de entrada igual ao grau de saída, então é possível dividir as arestas em vários ciclos simples disjuntos; começando com o caminho sendo apenas um ciclo simples, é fácil ver que podemos ir incrementando o Ciclo Euleriano adicionando os outros ciclos que passam por algum vértice que já está no Ciclo Euleriano atual.

A prova dessa primeira afirmação (que é possível dividir em ciclos simples disjuntos) foi feita por Carl Hierholzer, em 1873, no livro Graph Theory, 1736–1936, mas não vamos discuti-la mais a fundo aqui.

Então para checar se um grafo possui um Ciclo Euleriano, é só fazer um for verificando se todos os vértices seguem a primeira restrição, e fazer uma dfs para ver se ele é conexo. Mas sabendo que o grafo o possui, como encontrar um Ciclo Euleriano?

A nossa estratégia vai ser dividir o grafo em vários ciclos simples (que não repetem aresta) de um modo que cada aresta esteja em exatamente um deles, e depois juntar eles formando o Ciclo Euleriano. Veja alguns exemplos nesses grafos:

EXEMPLOS

Esse processo de separar em ciclos e juntá-los pode ser feito com uma função recursiva chamada 'AcharCicloEuler(int u)'. Quando ela estiver em u, ela vai fazer o seguinte:

  • Enquanto ainda existir alguma aresta saindo de u inutilizada
    • Pega alguma aresta que não foi usada ainda e saí de u para algum vértice v.
    • Apaga ela do grafo (marca como utilizada).
    • Tenta construir algum ciclo simples saindo do vértice v chamando 'AcharCicloEuler(v)'.
  • Adiciona u na resposta (no caminho).

Já que adicionamos u apenas depois de ver todas as arestas, colocamos os vértices de cada ciclo simples na resposta na ordem inversa, então depois precisamos inverter o vetor que guarda o caminho.

No final, essa ideia se traduz no seguinte código:

Vale ressaltar que não podemos trocar o 'while' por um 'if', porque ele pode encontrar um ciclo que contém o vértice inicial e acabar o código sem verificar adicionar os outros ciclos. Além disso, não podemos adicionar o vértice na resposta no início da função, senão a ordem não ficará correta nem se não invertermos.

Para tentar entender o código um pouco melhor, vale a pena testar manualmente o que ele faz em alguns grafos. Tente fazer isso com os exemplos que demos a cima e também com exemplos que você mesmo pode criar!

Ciclo Euleriano em Grafo Não-Direcionado

Em um grafo não-direcionado, não podemos simplesmente criar as arestas para as duas direções e rodar o código assim, a ideia do Ciclo Euleriano é que passemos exatamente uma vez por cada aresta, independente da direção. Se existe uma aresta u - v, então vamos passar ou de u para v, ou de v para u.

A condição de existência do Ciclo Euleriano em grafos não-direcionados é muito semelhante à de grafos direcionados, essas 2 condições precisão ser verdadeiras:

  • Todos os vértices tem grau par, ou seja, todos os vértices são pontas de uma quantidade par de arestas.
  • Ignorando os vértices isolados (com grau sendo 0), o grafo é conexo.

Assim como no grafo direcionado, é fácil ver que essas condições são necessárias, pois a quantidade de vezes que você entra num vértice u é igual a quantidade de vezes que você sai, e ambos são iguais à metade do número de arestas conectadas ao vértice u (portanto, tem que ser par), e o grafo deve ser conexo.

A prova que também são suficientes é semelhante a do grafo direcionado. Todos os vértices tem grau par \Rightarrow É possível dividir em vários ciclos simples disjuntos \Rightarrow É possível montar um Ciclo Euleriano.

Para encontrar o Ciclo Euleriano em grafos não-direcionados, vamos fazer um algoritmo análogo à de grafos direcionados. A única diferença é que vamos ter que chegar se a aresta já foi usada ou não na direção inversa; para isso, vamos guardar as duas pontas de cada aresta em um array de pair 'edges', a lista de adjacência de cada vértice vai ser um vector 'edges_id', e vamos guardar em um array 'vis' se cada aresta já foi ou não visitada.

O código para Encontrar Ciclo Euleriano em grafos não-direcionados fica assim:

 

O problema do Dominó

Imagine o seguinte problema:

Você tem M peças de Dominó, cada uma delas tem um número, que vai de 1 até N, escrito em cada um de suas pontas. Assim como num jogo de dominó normal, você pode conectar duas peças juntando duas pontas com o mesmo número escrito. Você pode rotacionar os dominós.

É possível colocar colocar esses dominós enfileirados de modo a usar todos eles exatamente uma vez e o número mais a esquerda é igual ao mais a direita?

Exemplo:

Dominós disponíveis
Exemplo de construção que dá certo do exemplo a cima.
Solução

Podemos reduzir isso para um Ciclo Euleriano: Os números escritos no dominó (de 1 até N) vão ser os vértices e os dominós vão ser as arestas.

Se um dominó tem A em uma ponta e B na outra, vamos criar uma aresta não-direcionada entre A e B.

Queremos que o primeiro número seja igual ao último, portanto temos que ter um ciclo; também queremos usar todos os dominós, portanto vamos usar todas as arestas.

Portanto, reduzimos o problema para encontrar um Ciclo Euleriano em um grafo não-direcionado.

[collapse]