Ideia 08 - Sparse Table

Escrito por Thiago Mota

Pense no seguinte problema, dado um vetor de N elementos iremos fazer Q perguntas no vetor do tipo [l, r] perguntando qual o menor valor do vetor no intervalo de l até r. Esse problema pode ser facilmente resolvido utilizando uma Segment Tree com a complexidade de O(q \log n) para cada pergunta, mas nesse problema não temos update logo não é necessário o uso da Segment Tree, logo iremos resolver utilizando uma técnica de "programação dinâmica".

Iremos definir uma tabela tab de tal forma que tab(i, j) seja o menor valor no intervalo que começa em i e tem tamanho 2^j, ou seja, para o vetor v = [1, 5, 3, 6, 2] teremos a seguinte tabela:

tab(1, 0) = 1 \: [1, 1]

tab(1, 1) = 1 \: [1, 2]

tab(1, 2) = 1 \: [1, 4]

tab(2, 0) = 5 \: [2, 2]

tab(2, 1) = 3 \: [2, 3]

tab(2, 2) = 2 \: [2, 5]

tab(3, 0) = 3 \: [3, 3]

tab(3, 1) = 3 \: [3, 4]

tab(4, 0) = 6 \: [4, 4]

tab(4, 1) = 2 \: [4, 5]

tab(5, 0) = 2 \: [5, 5]

O tamanho dessa tabela será por volta de O(n \log n) de memória, pois o segundo argumento só vai até o logaritmo do tamanho total.

Para computar essa tabela, iremos olhar paras os valores calculados anteriormente, olhe para a imagem:

Com isso, notamos que para calcular a tab(i, j) podemos olhar o menor entre tab(i, j-1) e tab(k, j-1), sendo k o início do próximo intervalo, como o tamanho do intervalo é 2^{j-1}, o k vai ser i + 2^{j-1}, logo:

tab(i, j) = min(tab(i, j-1), tab(i + 2^{j-1}, j-1))


// (n) é o tamanho do vetor
// (log_n) é o log na base 2 de n
// (v) é o vetor
void computa() {
// Caso inicial (tamanho 1, j = 0)
for (int i = 1; i <= n; i++)
tab[i][0] = v[i]; // Intervalo [i, i]
for (int j = 1; j <= log_n; j++) {
// Note que o for do j vem primeiro, isso é necessário pois
// o jeito que tab(i, j) é calculado necessita que todos os i's
// para j-1 estejam calculados.
for (int i = 1; i <= n; i++) {
// Checando se o intervalo que começa em i e tem tamanho 2^j existe.
// Para isso basta checar se o fim do intervalo é menor ou igual a n
if (i + (1<<j) - 1 > n) break;
tab[i][j] = min(tab[i][j-1], tab[i + (1<<(j-1))][j-1]);
}
}
}

view raw

ideia_08_01.cpp

hosted with ❤ by GitHub

Agora para calcular o mínimo de qualquer intervalo [l, r] iremos simplesmente calcular o k = \left \lfloor{\log_2 tam}\right \rfloor sendo tam o tamanho do intervalo e pegar o mínimo dos intervalos que começam em l e tem tamanho 2^k, ou seja, tab(l, k) e outro que termina e r e tem tamanho 2^k, ou seja, tab(r - 2^k + 1, k), tendo assim o mínimo do intervalo todo.

Segue o código:


// (n) é o tamanho do vetor
// (v) é o vetor
int flog(int x) { // Calcula a parte inteira do log2 de x em O(1) ( para int )
return 31 - __builtin_clz(x);
}
int flog(long long x) { // Calcula a parte inteira do log2 de x em O(1) ( para long long )
return 63 - __builtin_clzll(x);
}
int query(int l, int r) {
int k = flog(r - l + 1);
return min(tab[l][k], tab[r - (1<<k) + 1][k]);
}

view raw

ideia_08_02.cpp

hosted with ❤ by GitHub

Note que essa tabela só funciona pois o menor entre x e x é o próprio x, pois existe sobreposição de intervalos na hora de fazer a query, ou seja, não funcionaria caso a pergunta fosse a soma do intervalo.

Essa ideia de montar uma tabela nas potências de dois tem o nome de Sparse Table e é muito utilizada na hora de calcular valores em uma árvore como no Menor Ancestral Comum, uma questão que usa uma ideia similar é a Crayfish ScrivenerIOI 2012 caso você queira exercitar a ideia.