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 e dois nós e , ache o nó que é ancestral comum de e de e está o mais longe possível da raiz.
Olhe um exemplo onde e e a árvore está enraizada em :
Vemos que o vértice desejado, o Menor Ancestral Comum, é o vértice .
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:
Primeiro, temos que descobrir, para cada vértice, seu nível e seu pai. Para isso, faremos uma DFS:
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
// 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) |
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:
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
LCA(vértice a, vértice b): | |
enquanto (a != b): | |
se (nivel[a] > nivel[b]) a = pai[a] | |
senao b = pai[b] | |
retorna a |
É fácil de entender porque que o algoritmo funciona. Vamos tentar agora melhorar isso para um algoritmo . Para isso, usaremos um pouco de programação dinâmica. De início, calcularemos uma tabela onde representará o -ésimo ancestral de . Para calcular essa tabela, note que:
- , para .
Para montar essa tabela, o código, em C++, fica assim:
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
// 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 . Note que, se , podemos achar o LCA deles fazendo uma busca-binária: se é diferente de , o LCA deles está acima desses dois, caso sejam iguais, é o contrário. Suponha então que , ou seja, está mais abaixo na árvore. Nesse caso, podemos subir (no máximo passos) para fazer com que . O código, em C++, fica assim:
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
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]; | |
} |
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 , por simplicidade). Então, na seguinte figura, vamos ver como podemos calcular a distância do vértice ao vértice .
- se é um ancestral de , então a distância é igual a .
- a distância de a equivale a soma das distância e .
Então, se quisermos a distância de a , sabemos que:
Portanto, a distância dos vértice ao vértice é
e é isso que usaremos para resolver o problema.
Então, o código do Jogo da Memória fica assim:
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
// | |
// 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 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.