Aula por Lucca Siaudzionis
Vamos olhar o problema Famílias de Troia (OBI 2013):
"A Guerra de Troia pode ter sido um grande conflito bélico entre gregos e troianos, possivelmente ocorrido entre 1300 a.C. e 1200 a.C. (fim da Idade do Bronze no Mediterrâneo). Recentemente foram encontradas inscrições numa caverna a respeito de sobreviventes. Após um trabalho árduo, arqueólogos descobritam que as incrições descreviam relações de parentesco numa certa população. Cada item da inscrição indicavam duas pessoas que pertenciam a uma mesma família. Seu problema é determinar quantas famílias distintas existem."
Primeiro, vamos analisar como será o grafo do problema.
Vértices: Cada pessoa que existe em Troia.
Arestas: Cada inscrição dizendo que duas pessoas pertenciam a mesma família.
O problema quer o número de famílias distintas, ou seja, quer o número de componentes conexas do grafo. Vamos olhar um exemplo da entrada:
É fácil ver que as três componentes são , e .
Um algoritmo para resolver estes tipos de problema se chama Flood Fill.
O Algoritmo
Flood Fill se trata da ideia de ir preenchendo um grafo como se fosse um fluxo (como o próprio nome, em inglês, sugere). Vamos abordar duas maneiras muito conhecidas para se resolver este tipo de problema: busca em profundidade (DFS) e busca em largura (BFS).
DFS (depth-first search) se trata de, em cada passo, olhar os vizinhos do nó que se está avaliando e, para cada um deles cuja componente ainda não foi determinada, fazer sua componente ser a mesma de e chamar a função recursivamente nele.
BFS (breadth-first-search) se trata de fazer o mesmo procedimento da DFS. Porém, em vez de chamar a função recursivamente em um vizinho, este é adicionado a uma fila.
A DFS é mais fácil de se implementar e de se debugar. A BFS também tem suas vantagens, que veremos em breve. Ambas rodam em tempo , onde é o número de vértices e o número de arestas.
Vamos começar estudando a DFS. O código, como dito anteriormente, se trata de uma função recursiva e é algo bem simples. Olhe o seguinte pseudocódigo:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// componente[i] se trata da componente do vértice i | |
// inicialmente, componente[i] = -1 para todo vértice | |
// faremos a DFS como sendo uma função recursiva | |
// antes de chamar a DFS no primeiro nó, definimos sua componente | |
DFS(vértice X): | |
para todo V vizinho a X: | |
se (componente[V] == -1): | |
componente[V] = componente[X] | |
DFS(V) |
Na hora de procurar os vizinhos do vértice , isso pode ser feito tanto pela matriz de adjacência, checando-se se existe uma aresta para todo vértice (complexidade ), quanto com lista de adjacência (complexidade ). Se você ainda não está acostumado ao uso de lista de adjacência, faça por matriz. A maior parte dos problemas iniciantes não tem problema com esse tempo.
Vamos agora simular a DFS partindo do vértice do exemplo acima. Vou mostrar, na tabela, apenas alguns nós (os valores omitidos são assumidos como sendo iguais a ).
Nó | Componente | |
1 | 1 |
Então, olhamos para o primeiro vizinho do , que é o vértice .
Nó | Componente | |
1 | 1 | |
2 | 1 |
De , vamos para .
Nó | Componente | |
1 | 1 | |
2 | 1 | |
3 | 1 |
De , vamos para .
Nó | Componente | |
1 | 1 | |
2 | 1 | |
3 | 1 | |
4 | 1 |
O vértice não tem nenhum vizinho cuja componente seja , então, voltamos para o vértice .
Nó | Componente | |
1 | 1 | |
2 | 1 | |
3 | 1 | |
4 | 1 | |
3 | 1 |
De , vamos para .
Nó | Componente | |
1 | 1 | |
2 | 1 | |
3 | 1 | |
4 | 1 | |
3 | 1 | |
6 | 1 |
De , vamos para .
Nó | Componente | |
1 | 1 | |
2 | 1 | |
3 | 1 | |
4 | 1 | |
3 | 1 | |
6 | 1 | |
5 | 1 |
Agora, não será encontrado nenhum vizinho cuja componente seja , então a recursão irá voltar até o vértice , depois para o , e , encerrando a função. Assim, encontramos a componente conexa .
Para fazer a BFS, em vez de chamar a função recursivamente, adicionamos o vizinho numa fila para ele ser posteriormente processado.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// o código a seguir é uma BFS partindo do vértice X | |
// o array componente está inicializado para -1 em todas suas casas | |
fila.insere(X) | |
componente[X] := valor | |
enquanto (fila.tamanho > 0) faça: | |
//vamos trabalhar com o primeiro da fila | |
V = fila.frente | |
fila.remove_frente | |
para todo Y vizinho de V, faça: | |
se (componente[Y] = -1): | |
componente[Y] = componente[V] | |
fila.insere(Y) |
Assim como na DFS, você escolhe que ferramenta usar na hora de procurar os vizinhos de um vértice. Podendo usar as duas ferramentas citadas (vão rodar na mesma complexidade).
Vamos agora simular a BFS partindo do vértice :
Fila | |
1 |
Acrescentamos os vértices e a fila e continuamos o procedimento.
Fila | |
1 | |
2 | |
4 |
Em , adicionamos os vértices e e continuamos o procedimento.
Fila | |
1 | |
2 | |
4 | |
3 | |
6 |
No vértice , não podemos adicionar ninguém, pois todos seus vizinhos estão marcados. Continuamos então o procedimento.
Fila | |
1 | |
2 | |
4 | |
3 | |
6 |
No vértice , ainda não podemos adicionar ninguém. Continuamos então o procedimento.
Fila | |
1 | |
2 | |
4 | |
3 | |
6 |
No vértice , adicionamos o vértice à fila.
Fila | |
1 | |
2 | |
4 | |
3 | |
6 | |
5 |
No , não podemos adicionar ninguém.
A fila chegou ao seu fim, assim como a BFS. Foi possível, como visto, encontrar a componente conexa .
Ok. Ainda não mostrei completamente a solução para o problema que iniciou a aula. Se você olhar o enunciado do problema, verá que , ou seja, uma solução usando Matriz de Adjacência (que ficaria ) é inviável. Portanto, nossa solução será usando Lista de Adjacência.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// | |
// familias_de_troia.cpp | |
// | |
// Created by Lucca Siaudzionis on 06/05/15. | |
// | |
// Famílias de Troia - OBI 2013 P2F2 | |
#include <cstdio> | |
#include <vector> | |
using namespace std; | |
//------------------------ | |
#define MAXN 50050 | |
int n, m; | |
int componente[MAXN]; | |
vector<int> lista[MAXN]; | |
//------------------------ | |
void dfs(int x){ | |
// percorremos por todos os vizinhos | |
for(int i = 0;i < (int)lista[x].size();i++){ | |
int v = lista[x][i]; | |
if(componente[v] == -1){ // checamos se V ainda não foi visitado | |
componente[v] = componente[x]; | |
dfs(v); | |
} | |
} | |
} | |
int main(){ | |
scanf("%d %d", &n, &m); | |
for(int i = 1;i <= n;i++) componente[i] = -1; // inicializamos as componentes | |
for(int i = 1;i <= m;i++){ | |
int a, b; | |
scanf("%d %d", &a, &b); | |
// adicionamos cada um a lista do outro | |
lista[a].push_back(b); | |
lista[b].push_back(a); | |
} | |
int numero_componentes = 0; | |
for(int i = 1;i <= n;i++){ | |
if(componente[i] == -1){ // i ainda não tem componente | |
// começaremos uma dfs a partir de i | |
// assim, i será o começo de uma nova componente | |
numero_componentes++; | |
componente[i] = numero_componentes; | |
dfs(i); | |
} | |
} | |
printf("%d\n", numero_componentes); // por fim, imprimimos a resposta | |
// Note que, por simplicidade, não precisávamos ter guardado | |
// a componente a que cada vértice pertence. | |
// Simplesmente poderíamos ter guardado se um vértice já tinha | |
// sido visitado ou não. É o que eu faria normalmente, já que | |
// o problema não pede as componentes de cada vértice. | |
// Porém, é interessante ver esta abordagem, resolvendo o | |
// problema de uma maneira mais completa | |
return 0; | |
} |
Isto deve ter sido suficiente pra se ter uma noção desse dois algoritmos. Tente, agora, resolver algumas questões para colocar em prática o aprendido! Caso tenha dificuldade em algum problema ou alguma dúvida sobre a teoria apresentada, volte para a página inicial do curso para preencher o formulário e nos enviar sua dúvida!
Problema 1 - Gincana - Clique aqui para ver a solução desse problema com Union-Find (se não entender, não se preocupe, ele só será usado em aulas posteriores).
Problema 7 - You want what filled?
As aulas do Curso Noic de Informática são propriedade do Noic e qualquer reprodução sem autorização prévia é terminantemente proibida. Se você tem interesse em reproduzir algum material do Curso Noic de Informática para poder ministrar aulas, você pode nos contatar por esta seção de contato para que possamos fornecer materiais específicos para reprodução.