Grafos 02 - Flood Fill

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:

Slice 11

É fácil ver que as três componentes são \{1, 2, 3, 4, 5, 6\}, \{7, 8\} e \{9\}.

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ó v que se está avaliando e, para cada um deles cuja componente ainda não foi determinada, fazer sua componente ser a mesma de v 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 O(V+E), onde V é o número de vértices e 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:


// 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)

view raw

DFS_pseudo

hosted with ❤ by GitHub

Na hora de procurar os vizinhos do vértice X, isso pode ser feito tanto pela matriz de adjacência, checando-se se existe uma aresta para todo vértice (complexidade O(V)), quanto com lista de adjacência (complexidade O(grau(X))). 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 1 do exemplo acima. Vou mostrar, na tabela, apenas alguns nós (os valores omitidos são assumidos como sendo iguais a -1).

Slice 12g Componente
1 1

Então, olhamos para o primeiro vizinho do 1, que é o vértice 2.

Slice 13g Componente
1 1
2 1

De 2, vamos para 3.

Slice 14g Componente
1 1
2 1
3 1

De 3, vamos para 4.

Slice 15g Componente
1 1
2 1
3 1
4 1

O vértice 4 não tem nenhum vizinho cuja componente seja -1, então, voltamos para o vértice 3.

Slice 16g Componente
1 1
2 1
3 1
4 1
3 1

De 3, vamos para 6.

Slice 17g Componente
1 1
2 1
3 1
4 1
3 1
6 1

De 6, vamos para 5.

Slice 18g 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 -1, então a recursão irá voltar até o vértice 6, depois para o 3, 2 e 1, encerrando a função. Assim, encontramos a componente conexa \{1, 2, 3, 4, 5, 6\}.

 

Para fazer a BFS, em vez de chamar a função recursivamente, adicionamos o vizinho numa fila para ele ser posteriormente processado.


// 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)

view raw

BFS_pseudo

hosted with ❤ by GitHub

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 1:

Slice 12g Fila
1

Acrescentamos os vértices 2 e 4 a fila e continuamos o procedimento.

Slice 19g Fila
1
2
4

Em 2, adicionamos os vértices 3 e 6 e continuamos o procedimento.

Slice 20g Fila
1
2
4
3
6

No vértice 4, não podemos adicionar ninguém, pois todos seus vizinhos estão marcados. Continuamos então o procedimento.

Slice 21g Fila
1
2
4
3
6

No vértice 3, ainda não podemos adicionar ninguém. Continuamos então o procedimento.

Slice 22g Fila
1
2
4
3
6

No vértice 6, adicionamos o vértice 5 à fila.

Slice 23g Fila
1
2
4
3
6
5

No 5, 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 \{1, 2, 3, 4, 5, 6\}.

 

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 N = 5\times 10^4, ou seja, uma solução usando Matriz de Adjacência (que ficaria O(N^2)) é inviável. Portanto, nossa solução será usando Lista de Adjacência.


//
// 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 2 - Colorindo

Problema 3 - Tarzan

Problema 4 - Batalha Naval

Problema 5 - Oil Deposits

Problema 6 - The die is cast

Problema 7 - You want what filled?

Problema 8 - Dominos 2


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.