Breadth First Search (BFS)
Aula relacionada recomendada
Introdução
O BFS é um algoritmo de Busca em Largura. Podemos interpretar a busca em largura como um incêndio em uma floresta. O incêncio começa de algum foco inicial, depois, ele se espalha pelas árvores mais próximas, até se tornar algo quase incontrolável. No algoritmo de BFS, a ideia é essencialmente a mesma: começamos de um vértice inicial e andamos primeiro para todos os vizinhos próximos, depois, ele se espalha para os vizinhos dos vizinhos, e assim por diante. Uma pequena ilustração de como o algoritmo funciona está demonstrado na sequência de imagens abaixo.
Vamos explorar melhor nessa sessão como implementar o BFS e quais são as aplicações práticas que podemos ter com ele.
Implementação
Nós vamos manter uma fila, onde o topo dessa fila é o nosso vértice atual. Percorreremos todos os vizinhos não visitados do vértice atual, marcamos eles como visitados e adicionamos eles na fila. Com isso, andaremos no grafo em níveis, como o exemplo do incêndio falado no começo dessa seção.
A complexidade de usar o BFS é \(\mathcal{O}(N + M)\), com \(N\) sendo o número de vértices do nosso grafo, e \(M\) o número de arestas.
Agora, tendo a ideia do algoritmo em mente, podemos pensar em algumas aplicações práticas do BFS em alguns problemas.
Aplicações
Vamos assumir que todos os grafos podem ter até \(N \le 2\cdot 10^{5}\) vértices e \(M \le 2\cdot 10^{5}\) arestas.
Contar componentes conexas
Dado um grafo \(G\), contar quantas componentes conexas ele possui.
A ideia principal para contar a quantidade de componentes conexas é caminhar nos vértices que ainda não foram visitados, pois, se esse vértice ainda não foi visitado, ele e todos os outros vértices alcançáveis por ele fazem parte da mesma componente conexa. Assim, a solução se torna fazer o BFS em cada vértice ainda não visitado em qualquer ordem, e para cada chamada, aumentar em \(1\) a resposta final.
| connected_component.cpp | |
|---|---|
Essencialmente, o código apresentado acima é o mesmo que o usado na sessão de DFS, mas com um BFS no lugar. Ambos algoritmos podem ser usados para resolver o problema de contar componentes conexas, e suas variadas aplicações.
Caminho mínimo
Dado um grafo \(G\) sem pesos nas arestas e dois vértices \(s\) e \(t\), achar qual o caminho mínimo de \(s\) a \(t\). Caso ele não exista, exiba isso.
Como o caminho feito pelo algoritmo é em largura, o que acontece é que ele também calcula a distância mínima de um vértice inicial para todos os outros pertencentes a mesma componente que ele, pois a ideia é que, agora, estamos andando em níveis, e só iremos visitar um nível novo quando já tivermos visitado todo o nível atual. Com isso, podemos caminhar no grafo e garantir que o caminho encontrado de um vértice inicial a todos os outros é o menor possível.
Para a implementação do algoritmo, vamos inicializar todos os valores de \(dist[i]\) com \(\infty\), sendo que \(\infty\) é um número muito grande. Suponha que o nosso vértice atual seja \(u\). Para todo vizinho de \(u\), vamos chamá-lo de \(v\), vamos verificar se devemos colocar ele na fila, vendo se a distância percorrida para chegar até ele é menor do que a distância percorrida até ele atualmente, ou seja, \(dist[v] > dist[u]+1\). Se for, atualize e coloque o vértice na fila.
Se quisermos recuperar o caminho de \(s\) a \(t\), podemos guardar mais informação. No nosso caso, podemos guardar um vetor que marca quem é o pai de um certo vértice \(u\), assim podemos manter, para cada \(u\) do grafo, por qual vértice eu passei para chegar nele, com exceção do vértice inicial, que não possui antecessor. Como queremos sair do vértice \(s\) até o \(t\), para recuperar o caminho, sairemos de \(t\) até \(s\). Abaixo ficará o código que ilustra essa implementação do caminho mínimo mais a recuperação do caminho.








