Grafos 04 - LCA (Menor Ancestral Comum)

Aula por Lucca Siaudzionis

"Pedro e Paulo resolveram complicar um pouco o tradicional Jogo da Memória, em que os jogadores precisam virar duas cartas iguais. Eles colocam as cartas no chão, viradas para baixo, e fazem algumas linhas ligando pares de cartas, usando giz, de modo que para qualquer par de cartas (A, B) existe uma e apenas uma sequência de cartas distintas que leva de A até B através das linhas que eles desenharam. Com isso, ao virar duas cartas, o jogador ganha uma quantidade de pontos igual ao tamanho da sequência de linhas entre as duas cartas, se elas forem iguais. Se forem diferentes, o jogador perde aquela quantidade de pontos.

Pedro e Paulo, agora, estão estudando qual é a melhor estratégia para esse jogo e precisam da sua ajuda para resolver uma tarefa específica: dadas as ligações entre as N cartas, calcular a soma dos tamanhos das sequências entre todos os N/2 pares de cartas iguais!"

(Jogo da Memória, OBI 2014)

Antes de resolver esse problema, vamos resolver um outro:

Dada uma árvore enraizada T e dois nós x e y, ache o nó que é ancestral comum de x e de y e está o mais longe possível da raiz.

Olhe um exemplo onde x = 9 e y = 11 e a árvore está enraizada em 1:

LCA 1

Vemos que o vértice desejado, o Menor Ancestral Comum, é o vértice 3.

Primeiro, vamos definir, para cade vértice, seu nível de profundidade, que basicamente será igual a sua distância até a raiz. Definiremos também o pai de um vértice, que é o vértice adjacente a ele que possui um nível menor. Ficando assim:

LCA 2

Primeiro, temos que descobrir, para cada vértice, seu nível e seu pai. Para isso, faremos uma DFS:


// inicializamos o nível de cada vertice como sendo igual a -1
// antes de chamar a DFS em 1 (a raiz), colocamos seu nivel igual a 0
DFS(vértice U):
para todo vértice V vizinho de U:
se (nivel[V] == -1):
pai[V] := U
nivel[V] := nivel[U] + 1
DFS(V)

view raw

dfs_lca

hosted with ❤ by GitHub

Ok, agora temos o nível e o pai de cada vértice. Tendo apenas essas informações, já é possível descobrir o LCA de dois vértices (de uma maneira lenta, mas dá). Olhe para o seguinte pseudo-código:


LCA(vértice a, vértice b):
enquanto (a != b):
se (nivel[a] > nivel[b]) a = pai[a]
senao b = pai[b]
retorna a

view raw

lca_trivial

hosted with ❤ by GitHub

É fácil de entender porque que o algoritmo funciona. Vamos tentar agora melhorar isso para um algoritmo O(N \ lg N). Para isso, usaremos um pouco de programação dinâmica. De início, calcularemos uma tabela ancestral onde ancestral[i][j] representará o 2^j-ésimo ancestral de i. Para calcular essa tabela, note que:

  • ancestral[i][0] = pai[i]
  • ancestral[i][j] = ancestral[ \ ancestral[i][j-1] \ ][j-1], para j > 0.

Para montar essa tabela, o código, em C++, fica assim:


// declarar a tabela
ancestral[MAXN][MAXL]; // MAXL representa lg N (com uma margem a mais, claro)
// primeiro, inicializamos tudo para -1
for(int i = 0;i < MAXN;i++)
for(int j = 0;j < MAXL;j++)
ancestral[i][j] = -1;
// definimos os pais de carda vértice
for(int i = 1;i <= N;i++) ancestral[i][0] = pai[i];
// montamos o restante da tabela com programação dinâmica
for(int j = 1;j < MAXL;j++)
for(int i = 1;i <= N;i++)
if(ancestral[i][j-1] != -1)
ancestral[i][j] = ancestral[ ancestral[i][j-1] ][j-1];

Vamos tentar, com isso, calcular LCA(u, \ v). Note que, se nivel[u] = nivel[v], podemos achar o LCA deles fazendo uma busca-binária: se ancestral[u][j] é diferente de ancestral[v][j], o LCA deles está acima desses dois, caso sejam iguais, é o contrário. Suponha então que nivel[u] > nivel[q], ou seja, u está mais abaixo na árvore. Nesse caso, podemos subir (no máximo lg passos) para fazer com que nivel[u] = nivel[q]. O código, em C++, fica assim:


int LCA(int u, int v){
if(nivel[u] < nivel[v]) swap(u, v); // isto é para definir u como estando mais abaixo
// vamos agora fazer nivel[u] ser
// igual nivel[v], subindo pelos
// ancestrais de u
for(int i = MAXL-1;i >= 0;i--)
if(nivel[u] - (1<<i) >= nivel[v])
u = ancestral[u][i];
// agora, u e v estão no mesmo nível
if(u == v) return u;
// subimos o máximo possível de forma
// que os dois NÃO passem a ser iguais
for(int i = MAXL-1;i >= 0;i--)
if(ancestral[u][i] != -1 && ancestral[u][i] != ancestral[v][i]){
u = ancestral[u][i];
v = ancestral[v][i]
}
// como subimos o máximo possível
// sabemos que u != v e que pai[u] == pai[v]
// logo, LCA(u, v) == pai[u] == pai[v]
return ancestral[u][0];
}

view raw

lca_query.cpp

hosted with ❤ by GitHub

O algoritmo para achar o LCA de dois vértices está completo!

 

Apesar de sabermos achar o LCA de dois vértices, ainda não ficou claro como se aplicar esse tipo de algoritmo. Vamos então continuar a resolução do Jogo da Memória.

Se montarmos o grafo das cartas, vemos que o grafo tem que ser uma árvore. Podemos enraizar a árvore num vértice qualquer (vamos enraizar no vértice 1, por simplicidade). Então, na seguinte figura, vamos ver como podemos calcular a distância do vértice 9 ao vértice 11.

LCA 3Podemos observar duas coisas:

  • se x é um ancestral de u, então a distância x \rightarrow u é igual a nivel[u] - nivel[x].
  • a distância de u a v equivale a soma das distância u \rightarrow LCA(u, \ v) e LCA(u, \ v) \rightarrow v.

Então, se quisermos a distância de u a v, sabemos que:

  • u \rightarrow LCA(u, \ v) = nivel[u] - nivel[LCA(u, \ v)]
  • LCA(u, \ v) \rightarrow v= nivel[v] - nivel[LCA(u, \ v)]

Portanto, a distância dos vértice u ao vértice v é

nivel[u] + nivel[v] - 2 \times nivel[LCA(u, \ v)]

e é isso que usaremos para resolver o problema.

Então, o código do Jogo da Memória fica assim:


//
// jogo_da_memoria.cpp
//
// Created by Lucca Siaudzionis on 10/05/15.
//
// Jogo da Memória - OBI 2014 P1 F2
#include <cstdio>
#include <vector>
#include <algorithm>
using namespace std;
//--------------------------
#define MAXL 20
#define MAXN 50500
// básico
int n;
int c[MAXN];
int par[MAXN];
// LCA
int pai[MAXN];
int nivel[MAXN];
int ancestral[MAXN][MAXL];
// grafo
vector<int> lista[MAXN];
//--------------------------
void dfs(int u){
for(int i = 0;i < (int)lista[u].size();i++){
int v = lista[u][i];
if(nivel[v] == -1){
pai[v] = u;
nivel[v] = nivel[u]+1;
dfs(v);
}
}
}
int LCA(int u, int v){
if(nivel[u] < nivel[v]) swap(u, v);
for(int i = MAXL-1;i >= 0;i--)
if(nivel[u] - (1<<i) >= nivel[v])
u = ancestral[u][i];
if(u == v) return u;
for(int i = MAXL-1;i >= 0;i--)
if(ancestral[u][i] != -1 && ancestral[u][i] != ancestral[v][i]){
u = ancestral[u][i];
v = ancestral[v][i];
}
return pai[u];
}
int main(){
scanf("%d", &n);
for(int i = 1;i <= n;i++){
int x;
scanf("%d", &x);
// isto é somente para definir os pares de cartas
if(c[x]){
par[i] = c[x];
par[c[x]] = i;
}
c[x] = i;
}
for(int i = 1;i < n;i++){
int a, b;
scanf("%d %d", &a, &b);
// montar a lista
lista[a].push_back(b);
lista[b].push_back(a);
}
// inicializações
for(int i = 0;i < MAXN;i++) pai[i] = nivel[i] = -1;
for(int i = 0;i < MAXN;i++)
for(int j = 0;j < MAXL;j++)
ancestral[i][j] = -1;
// descobrir o pai e o nível de todo mundo
nivel[1] = 0;
dfs(1);
// montar tabela de ancestral
for(int i = 1;i <= n;i++) ancestral[i][0] = pai[i];
for(int j = 1;j < MAXL;j++)
for(int i = 1;i <= n;i++)
ancestral[i][j] = ancestral[ ancestral[i][j-1] ][j-1];
// agora, sim, calcular a resposta
long long resposta = 0LL;
for(int i = 1;i <= n;i++)
resposta += (long long)( nivel[i] + nivel[par[i]] - 2*nivel[ LCA(i, par[i]) ]);
// dividimos por 2 porque calculamos cada distâncias duas vezes
printf("%lld\n", resposta/2);
return 0;
}

Tente, agora, resolver os problemas abaixo para colocar em prática seus conhecimentos. 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 - Colônia de Formigas

Problema 2 - LCA

Problema 3 - Query on a Tree II

Problema 4 - Distance in the Tree


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.