cco 101 processamento de dados
TRANSCRIPT
CIC 111Análise e Projeto de Análise e Projeto de
Algoritmos IIAlgoritmos II
Universidade Federal de Itajubá
Prof. Roberto Affonso da Costa Junior
AULA 16AULA 16
– Directed graphs• Topological sorting• Dynamic programming• Successor paths • Cycle detection
Directed GraphsDirected Graphs
Nesta aula, vamos estudar duas classes de grafos direcionados:
Grafos Acíclicos: não há ciclos no gráfico, portanto, não há nenhum caminho de nenhum nó para ele próprio1.
Grafos Sucessores: o grau de saída de cada nó é 1, então cada nó possui um sucessor único.
Acontece que em ambos os casos, podemos ter algoritmos eficientes baseados nas propriedades especiais dos grafos.1Os grafos acíclicos direcionados às vezes são chamados DAGs
Topological sortingTopological sorting
Uma ordenação topológico é uma ordenação dos nós de um grafo direcionado, de modo que, se houver um caminho do nó a para o nó b, o nó a aparece antes do nó b no pedido. Por exemplo, para o grafo
uma ordenação topológico é [4, 1, 5, 2, 3, 6]:
1 3
4 5 6
1 3
4 5 6
4 1 34 5 6
Sites a visitarSites a visitar
PROGRAMA Kahn
AlgoritmoAlgoritmo
A ideia é passar pelos nós do grafo e sempre começar por uma busca em profundidade do nó atual, que ainda não tiver sido processado. Durante as pesquisas, os nós possuem três estados possíveis:Estado 0: o nó não foi processado (branco)Estado 1: o nó está em processamento (cinza claro)Estado 2: o nó foi processado (cinza escuro)
Inicialmente, o estado de cada nó é 0. Quando uma pesquisa atinge um nó pela primeira vez, seu estado se torna 1. Finalmente, depois de todos os sucessores do nó terem sido processados, seu estado se torna 2.
AlgoritmoAlgoritmo
Se o grafo contiver um ciclo, descobriremos isso durante a pesquisa, porque, mais cedo ou mais tarde, chegaremos a um nó cujo estado é 1. Neste caso, não é possível construir um grafo topológico ordenado.
Se o grafo não contém um ciclo, podemos construir um grafo topológico ordenado adicionando cada nó a uma lista quando o estado do nó se tornar 2. Essa lista na ordem inversa é um grafo topológico ordenado.
Exemplo 1Exemplo 1
No grafo do exemplo, a pesquisa primeiro procede do nó 1 ao nó 6:
1 3
4 5 6
1 3
4 5 6
Exemplo 1Exemplo 1
Agora o nó 6 foi processado, então ele é adicionado à lista. Depois disso, também nós 3, 2 e 1 são adicionados à lista
1 3
4 5 6
1 3
4 5 6
Exemplo 1Exemplo 1
Neste ponto, a lista é [6, 3, 2, 1]. A próxima pesquisa começa no nó 4:
1 3
4 5 6
1 3
4 5 6
Exemplo 1Exemplo 1
Assim, a lista final é [6, 3, 2, 1, 5, 4]. Nós processamos todos os nós, portanto, um tipo topológico foi encontrado. O tipo topológico é a lista inversa [4, 5, 1, 2, 3, 6]:
Observe que um tipo topológico não é único, e pode haver vários tipos topológicos para um gráfico.
1 34 5 6
Exemplo 2Exemplo 2
Consideremos agora um grafo para o qual não podemos construir um tipo topológico, porque o grafo contém um ciclo:
1 3
4 5 6
1 3
4 5 6
Exemplo 2Exemplo 2
A pesquisa prossegue da seguinte forma:
A pesquisa atinge o nó 2 cujo estado é 1, o que significa que o gráfico contém um ciclo. Neste exemplo, há um ciclo 2 → 3 → 5 → 2.
1 3
4 5 6
1 3
4 5 6
Sites a visitarSites a visitar
PROGRAMA Kahn
Sites a visitarSites a visitar
Programação DinâmicaProgramação Dinâmica
Se um grafo direcionado é acíclico, a programação dinâmica pode ser aplicada a ele. Por exemplo, podemos resolver de forma eficiente os seguintes problemas relacionados aos caminhos de um nó inicial para um nó final:
quantos caminhos diferentes existem?qual é o caminho mais curto / mais longo?qual é o número mínimo / máximo de arestas em um caminho?quais nós certamente aparecem em qualquer caminho?
Programação DinâmicaProgramação Dinâmica
Contando o número de caminhos
Como exemplo, vamos calcular o número de caminhos do nó 1 ao nó 6 no seguinte grafo:
1 3
4 5 6
1 3
4 5 6
Programação DinâmicaProgramação Dinâmica
Há três desses caminhos:
1 → 2 → 3 → 61 → 4 → 5 → 2 → 3 → 61 → 4 → 5 → 3 → 6
Deixe path(x) indicar o número de caminhos do nó 1 para o nó x. Como um caso base, path(1) = 1. Então, para calcular outros valores de path(x), podemos usar a recursão
path(x) = path(a1) + path(a
2) + … + path(a
k)
Programação DinâmicaProgramação Dinâmica
onde a1, a
2, … , a
k são os nós dos quais existe uma
direção de x. Uma vez que o grafo é acíclico, os valores dos path(x) podem ser calculados na ordem de um grafo topológico ordenado. Um grafo topológico ordenado para o grafo acima é o seguinte:
1 34 5 6
Sites a visitarSites a visitar
ExercícioExercício
● Monte um programa para determinar quais os caminhos a ser percorrido no grafo a seguir, saindo do nó 5. Primeiramente, com o caminhos preto, em seguida colocando os cinzas.
Programação DinâmicaProgramação Dinâmica
Portanto, o número de caminhos é o seguinte:
1 3
4 5 6
1 3
4 5 6
1 2 3
1 1 3
Programação DinâmicaProgramação Dinâmica
Por exemplo, para calcular o valor dos path(3), podemos usar da fórmula: path(2) + path(5), porque existem arestas dos nós 2 e 5 ao nó 3. Como os path(2) = 2 e path(5) = 1, concluímos que path(3) = 3.
2
Programação DinâmicaProgramação Dinâmica
Estendendo o algoritmo de DijkstraUm subproduto do algoritmo de Dijkstra é um grafo acíclico dirigido que indica para cada nó do grafo original as formas possíveis de alcançar o nó usando um caminho mais curto a partir do nó inicial. Por exemplo, no grafo:
1
3 4
5
3
5 2 4
8
1
2
Programação DinâmicaProgramação Dinâmica
Agora, podemos, por exemplo, calcular o número de caminhos mais curtos do nó 1 para o nó 5 usando a programação dinâmica:
1
3 4
5
3
5 2 4
1
2
Programação DinâmicaProgramação Dinâmica
Os caminhos mais curtos que o nó 1 pode usar para chegar ao nó 5, passa nas seguintes arestas:
1
3 4
5
3
5 2 4
1
1 1
2 3
3
Representando problemas como Representando problemas como grafosgrafos
Na verdade, qualquer problema de programação dinâmica pode ser representado como um grafo acíclico direcionado. Em tal grafo, cada nó corresponde a um estado de programação dinâmico e as arestas indicam como os estados dependem um do outro.Por exemplo, considere o problema de formar uma soma de dinheiro usando n moedas {c
1, c
2, …, c
k}.
Neste problema, podemos construir um grafo onde cada nó corresponde a uma soma de dinheiro e as arestas mostram como as moedas podem ser escolhidas.
Representando problemas como Representando problemas como grafosgrafos
Por exemplo, para moedas {1, 3, 4} e n = 6, o grafo é o seguinte:
Usando essa representação, o caminho mais curto do nó 0 para o nó n corresponde a uma solução com o número mínimo de moedas e o número total de caminhos do nó 0 para o nó n é igual ao número total de soluções.
3 51 2 60
Caminhos sucessoresCaminhos sucessores
Os grafos sucessores, o grau sucessor de cada nó é 1, isto é, exatamente uma borda começa em cada nó. Um grafo sucessor consiste em um ou mais componentes, cada um dos quais contém um ciclo e alguns caminhos que o levam.Os grafos sucessores às vezes são chamados de grafos funcionais. A razão para isso é que qualquer grafo sucessor corresponde a uma função que define as arestas do grafo. O parâmetro para a função é um nó do grafo, a função dá o sucessor desse nó.
Caminhos sucessoresCaminhos sucessores
Por exemplo, a função:
define o seguinte grafo:
x 1 2 3 4 5 6 7 8 9
succ(x) 3 5 7 6 2 2 1 6 3
9 3 1
7 6
2 5
4 8
ExercícioExercício
Monte um programa que dado o grafo a seguir, mostre a tabela do sucessor de cada um dos nós.
Caminhos sucessoresCaminhos sucessores
Uma vez que cada nó de um grafo sucessor possui um sucessor único, também podemos definir uma função succ(x,k) que dê o nó que alcançaremos se começarmos no nó x e caminhar avançar k vezes. Por exemplo, no grafo acima succ(4,6) = 2, porque alcançaremos o nó 2 caminhando 6 passos a partir do nó 4:
52 26 2 54
Caminhos sucessoresCaminhos sucessores
Uma maneira direta de calcular um valor de succ(x,k) é começar no nó x e caminhar k passos para a frente, o que leva O(k) tempo. No entanto, usando o pré-processamento, qualquer valor de succ(x,k) pode ser calculado apenas no tempo O(log k).A ideia é precalcular todos os valores de succ(x,k) onde k é uma potência de dois e, no máximo u, onde u é o número máximo de etapas que sempre vamos caminhar. Isso pode ser feito eficientemente, porque podemos usar a seguinte recursão:
Caminhos sucessoresCaminhos sucessores
Precalcular os valores leva o tempo O(n log u), porque os valores de O(log u) são calculados para cada nó. No grafo acima, os primeiros valores são os seguintes:
fsucc (x , k)={ fsucc(x) k=1fsucc( fsucc(x , k /2) , k /2) k>1
Caminhos sucessoresCaminhos sucessores
Depois disso, qualquer valor de succ(x,k) pode ser calculado apresentando o número de etapas k como uma soma de potência de dois. Por exemplo, se queremos calcular o valor de succ(x,11), primeiro formamos a representação 11 = 8 + 2 + 1. Usando isso,
x 1 2 3 4 5 6 7 8 9
succ(x,1) 3 5 7 6 2 2 1 6 3
succ(x,2) 7 2 1 2 5 5 3 2 7
succ(x,4) 3 2 7 2 5 5 1 2 3
succ(x,8) 7 2 1 2 5 5 3 2 7
Caminhos sucessoresCaminhos sucessores
succ(x, 11) = succ(succ(succ(x, 8), 2), 1)
Por exemplo, no grafo anterior
succ(4, 11) = succ(succ(succ(4, 8), 2), 1) == succ(succ(2, 2), 1) =
= succ(2, 1) = 5
Essa representação sempre consiste em partes de O(log k), portanto, calcular um valor de succ(x,k) leva tempo de O(log k).
ExercícioExercício
No exercício anterior, você montou um programa que construía a tabela dos valores sucessores de um grafo. Com a ideia do caminho sucessor, preencha a tabela a seguir.
Nó (x) Passo (p) succ(x, p)
S 4
T 7
Z 8
W 2
V 5
Detecção de cicloDetecção de ciclo
Considere um grafo sucessor que contém apenas um caminho que termina em um ciclo. Podemos fazer as seguintes perguntas: se iniciarmos nossa caminhada no nó de partida, qual é o primeiro nó no ciclo e quantos nós o ciclo contém?Por exemplo, no grafo
1 2 3 54
6
Detecção de cicloDetecção de ciclo
começamos nossa caminhada no nó 1, o primeiro nó que pertence ao ciclo é o nó 4 e o ciclo consiste em três nós (4, 5 e 6).
Uma maneira simples de detectar o ciclo é andar no grafo e acompanhar todos os nós que foram visitados. Uma vez que um nó é visitado pela segunda vez, podemos concluir que o nó é o primeiro nó no ciclo. Este método funciona no tempo O(n) e também usa a memória O(n).
Detecção de cicloDetecção de ciclo
No entanto, existem melhores algoritmos para detecção de ciclo. A complexidade do tempo de tais algoritmos ainda é O(n), mas eles apenas usam memória O(1). Esta é uma melhoria importante se n for grande. Em seguida, vamos discutir o algoritmo da Floyd que atinge essas propriedades.
ExercícioExercício
Faça um programa para determinar o ciclo do grafo a seguir. Se não houver, basta dizer que não tem.
Algoritmo de FloydAlgoritmo de Floyd
O algoritmo de Floyd avança no grafo usando dois ponteiros a e b. Ambos os ponteiros começam em um nó x que é o nó inicial do grafo. Então, em cada rodada, o ponteiro a percorre um passo em frente e o ponteiro b anda dois passos para a frente. O processo continua até que os ponteiros se encontrem:
a = succ(x);b = succ(succ(x));while (a != b) { a = succ(a); b = succ(succ(b));}
Algoritmo de FloydAlgoritmo de Floyd
Neste ponto, o ponteiro a andou k passos e o ponteiro b caminhou 2k etapas, então o comprimento do ciclo divide k. Assim, o primeiro nó que pertence ao ciclo pode ser encontrado movendo o ponteiro a para o nó x e avançando os ponteiros passo a passo até se encontrar novamente.
a = x;while (a != b) { a = succ(a); b = succ(b);}first = a;
Algoritmo de FloydAlgoritmo de Floyd
Depois disso, o comprimento do ciclo pode ser calculado da seguinte forma:
b = succ(a);length = 1;while (a != b) { b = succ(b); length++;}
ExercícioExercício
Utilizando o Algoritmo de Floyd, faça um programa para determinar o ciclo do grafo a seguir. Se não houver, basta dizer que não tem.
Sugestão para a provaSugestão para a prova
Guarde e estude esses programas.
Procure resolver os problemas do URI:1100, 1655 e 1738
Procure resolver os problemas do UVa186, 341, 423, 10793 e 534