Aula por João Pedro Castro
Nessa aula nós vamos falar sobre a ideia do "Binary Lifting". Uma técnica utilizada para resolver problemas onde cada elemento só tem um sucessor (ou ancestral), sendo possível encontrar o k-ésimo sucessor de um elemento em tempo logarítmico. A ideia consiste em fazer um preprocessamento de forma inteligente, com uma ideia similar a da programação dinâmica, para depois responder à perguntas rapidamente.
Problema - Planets Queries I
Em cada planeta existe um teletransportador para outro (ou o mesmo) planeta. Responda perguntas da forma: começando em um planeta e viajando por teletransportadores, em que planeta você irá parar?
Primeiramente, vamos pensar em uma forma trivial de resolver o problema. Como sabemos onde vamos parar saindo de cada planeta andando passo, podemos simular em aonde vamos parar andando passos. Uma forma de fazer isso é simplesmente guardando a posição atual e trocando ela pelo seu sucessor (o teletransportador localizado na posição atual) vezes. Segue um pseudo-código para melhor entendimento.
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
// Assumindo que todos os sucessores estão em t[i] | |
// Saindo do planeta x e andando k passos | |
proximo (planeta x, número k) { | |
enquanto (k > 0) { | |
x = t[x]; // avança o x em uma posição | |
k--; // diminui a quantidade de passos restantes em 1 | |
} | |
} |
Essa é uma implementação que funciona, mas é muito lenta para limites altos. Porém existem propriedades nessa trivialidade que podemos levar para a nossa solução final, sendo a principal delas avançar uma quantidade de posições que somadas equivalem à avanços, algo intuitivo e que realmente está correto, pois cada avanço individual contribui para o progresso geral, e a soma de múltiplos avanços resulta no mesmo deslocamento final que avançar tudo de uma só vez (por exemplo, avançar 4 passos e depois 2 passos é o mesmo que avançar 6 passos).
O real truque do Binary Lifting vem a partir da ideia de ao invés de avançar pelas potências (ou seja, o próprio ), vamos avançar pelas potências de . Como explicado na aula sobre Bitmask, todo número pode ser escrito como uma soma de potências de , afinal essa é exatamente a representação binária de um número, logo, ao passarmos por todas as potências de dois e escolhermos se queremos ou não andar até o sucessor, é possível andar exatamente qualquer quantidade inteira de avanços.
O primeiro passo deve ser preprocessar o sucessor de cada planeta, para isso vamos usar uma ideia similar à da Programação Dinâmica, decidindo o caso base de cada planeta para só depois calcular o resto. O caso base vai ser algo dado no problema, o próprio sucessor de cada planeta : o planeta que você chega ao entrar no teletransportador em . O código em C++ abaixo é uma das maneiras de guardar esse sucessor (estou usando o modelo de input do problema tratado).
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
#include <bits/stdc++.h> | |
using namespace std; | |
const int MAXN = 2e5 + 10; | |
const int MAXLOG = 30; | |
int prox[MAXLOG + 1][MAXN]; | |
int main() { | |
int n, q; | |
cin >> n >> q; // só usaremos o valor q no futuro | |
for (int x = 1; x <= n; x++) { | |
cin >> prox[0][x]; | |
} | |
} |
O que estamos fazendo é declarar um vetor , que tem as dimensões de por . A definição dele pode ser escrita como:
Por isso usamos o como caso base, já que ele vai ser o planeta que paramos logo após entrar em um teletransportador. O motivo de usarmos as dimensões também é bem simples, já que no máximo iremos precisar andar passos (um limite dado no enunciado do problema), e o , e como o array é 0-indexado é necessário adicionar mais um, tornando a verdadeira "altura" da matriz em . Já para a "largura", ela é só a quantidade máxima de planetas.
Agora, precisamos achar uma maneira de inteligentemente calcular todos os valores (pelo menos os necessários) dentro dessa matriz. Para isso, vou usar a propriedade que vimos na solução trivial anteriormente: andar passos, depois mais passos, para assim andar passos. Imagine que você quer encontrar o , ou seja, onde você vai parar andando passos. Para fazer isso, você pode descobrir onde você vai parar andando passo (algo que você já tem guardado), e depois buscar onde você vai parar andando mais passo saindo desse outro ponto. Exemplificando, diga que , então podemos dizer que . De uma forma mais geral:
Para descobrir :
E a resposta que nós buscamos vai ser , já que andamos passos, e depois mais passos: . Podemos escrever toda essa equação em uma linha, mas você pode usar a forma que preferir: .
Para calcular tudo usaremos um loop dentro do outro, com complexidade de . Não incluiremos o pois já temos ele calculado (e teríamos problemas já que ).
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
#include <bits/stdc++.h> | |
using namespace std; | |
const int MAXN = 2e5 + 10; | |
const int MAXLOG = 30; | |
int prox[MAXLOG + 1][MAXN]; | |
int main() { | |
int n, q; | |
cin >> n >> q; // só usaremos o valor q no futuro | |
for (int x = 1; x <= n; x++) { | |
cin >> prox[0][x]; | |
} | |
for (int i = 1; i <= MAXLOG; i++) { | |
for (int x = 1; x <= n; x++) { | |
prox[i][x] = prox[i-1][[prox[i-1][x]]; | |
} | |
} | |
} |
Por fim, só precisamos dividir o de cada query em soma de potências de , uma técnica novamente já mencionada na aula de Bitmask, e então avançar posições para cada bit na posição que estiver ativo, faremos isso em um loop de até checando se o bit está ativo pelo binário. A implementação completa se encontra abaixo:
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
#include <bits/stdc++.h> | |
using namespace std; | |
const int MAXN = 2e5 + 10; | |
const int MAXLOG = 30; | |
int prox[MAXLOG + 1][MAXN]; | |
int main() { | |
ios::sync_with_stdio(0);ios_base::sync_with_stdio(0);cin.tie(0);cout.tie(0); // otimizações de entrada/saída | |
int n, q; | |
cin >> n >> q; | |
for (int x = 1; x <= n; x++) { | |
cin >> prox[0][x]; | |
} | |
for (int i = 1; i <= MAXLOG; i++) { | |
for (int x = 1; x <= n; x++) { | |
prox[i][x] = prox[i-1][[prox[i-1][x]]; | |
} | |
} | |
while(q--) { | |
int x, k; | |
cin >> x >> k; | |
for (int i = 0; i <= MAXLOG; i++) { | |
if (k & (1 << i)) x = prox[i][x]; | |
} | |
cout << x << "\n"; | |
} | |
} |
Com essa ideia deve ser possível entender o Binary Lifting, e resolver problemas relacionados.
Problemas para praticar
É importante perceber que enquanto não existem muitos problemas que só usam Binary Lifting, ela é uma ideia imprescindível para diversas técnicas mais avançadas, como o próprio LCA (Menor Ancestral Comum).
*Extremamente difícil