Escrito por Anita Almeida e Pedro Racchetti
A estrutura de dados Segment Tree já foi introduzida em uma aula anterior aqui em nosso curso que você pode conferir aqui. Nessa aula citada, para apresentar a estrutura, o seguinte problema foi utilizado:
Dado um vetor de posições , nós devemos conseguir efetuar as seguintes operações:
- Mudar o valor de uma posição específica do vetor para . Formalmente, precisamos que, para algum , .
- Encontrar a soma dos elementos de índice até , onde . Formalmente, queremos:
A solução mais eficaz proposta foi utilizar a Segment Tree da seguinte forma:
- Iniciar com a raiz da Segment Tree.
- Se o índice do vetor a ser atualizado não estiver no intervalo do nó atual, retorne.
- Caso contrário, atualize o nó atual e repita a ação para os filhos desse nó.
Assim, a função foi chamada para atualizar apenas um único valor no vetor, apesar dessa ação poder gerar várias atualizações na Segment Tree.
E se agora precisássemos atualizar um determinado intervalo de índices no vetor? Por exemplo, imagine adicionar 2 a todos os valores no intervalo dos índices 2 a 7 do vetor abaixo.
Obtemos:
Esse problema pode ser resolvido com Lazy Propagation, uma otimização para atualizar valores de um intervalo rapidamente.
Quando precisamos fazer muitas atualizações em um vetor e estas são feitas em um intervalo, podemos adiar algumas atualizações e realizá-las apenas quando necessárias. Considere o vetor , temos a seguinte Segment Tree:
Considere agora o nó com valor 9 que representa a soma do intervalo . Se precisarmos atualizar o intervalo [0,2], então precisamos atualizar esse nó com valor 9 e todos seus descendentes. A partir da Lazy Propagation, atualizamos apenas o nó com valor 9 e adiamos as atualizações de seus filhos, armazenando as informações de atualização em um vetor chamado . Iniciamos esse vetor com todos valores como 0. Conforme vamos atualizando os intervalos, se quer dizer que não há atualizações pendentes no nó da Segment Tree. Caso contrário, o valor de precisa ser adicionado ao nó na Segment Tree antes de consultar a soma do nó. Assim, antes de cada temos que checar se tem algum nó que precisa ser atualizado.
Implementação
A implementação de uma Segment Tree com Propagação em Lazy, é de fato bastante parecida com a implementação de uma Segment Tree normal, exceto por algumas modificações. A primeira, e mais importante é que, como as operações de são em um intervalo, temos que chamar sua recursão de forma parecida com a , atualizando ambos os filhos do nó atual. Além disso, temos que lidar com a propagação e manter o que foi propagado, e para isso, precisaremos de um novo vetor da árvore além do que já temos, nessa aula, o chamaremos de , e para de fato ocorrer a propagação usaremos a função , que irá atualizar o nó atual com o que foi. Essa função variará muito de problema a problema, mas possui um padrão que praticamente sempre será usado:
- Primeiro, verificamos que existe algo a ser propagado no nó
- Se não houver valores a serem propagados, podemos retornar
- Se houver algum valor, temos que diretamente atualizar o valor na Seg, seja lá como o problema pedir (analisaremos algumas situações posteriormente).
- Propagamos o valor para o dos filhos do nó, quando existirem (podemos verificar isso com o tamanho do intervalo de , sendo maior que 1, com certeza existirá filhos).
- Indicamos que em , não há valor a ser propagado.
Note que como essa função atualiza o vetor principal da árvore, podemos apenas modificar na , e chamar a função . Além disso, um dos erros mais comuns quando implementando problemas de é um nó não ser devidamente atualizado, logo é importante propagar sempre que se entrar em uma função de e de .
Para melhor compreensão, iremos analisar dois problemas, e mostrar suas soluções e implementações:
Problema 1: Dado um vetor inicialmente zerado de posições, efetue as seguintes operações:
- Operação 1: troque todos os valores nas posições do vetor no intervalo de à para o inteiro , com .
- Operação 2: imprima o valor máximo do vetor no intervalo de à .
Para esse problema, podemos ver que em cada nó da árvore, precisamos apenas guardar o valor máximo que o intervalo representa. Então, como só atualizamos o quando esse intervalo está inteiramente contido em um intervalo atualizado, a função é relativamente simples: basta mudar o vetor para o valor sendo propagado e propagar para os filhos. Segue o código com a implementação de cada função, para melhor compreensão desse ideia:
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
// Os parametros da função unlazy são: | |
// O nó atual, tl e tr, o intervalo que o nó atual abrange, | |
void unlazy(int node, int tl, int tr){ | |
// Se não houver valor a ser propagado, não precisamos propagar | |
if(lz[node] == 0) return; | |
// Primeiro, atualizamos a árvore com o novo valor | |
tree[node] = lz[node]; | |
// Se houver filhos, propagamos o vaor para eles | |
if(tl != tr){ | |
lz[2*node+1] = lz[node]; | |
lz[2*node+2] = lz[node]; | |
} | |
// Indicamos que não há mais valores a serem propagados em node | |
lz[node] = 0; | |
} | |
// Os parametros da função Update são: | |
// O nó atual, tl e tr, o intervalo que o nó atual abrange, l e r, o intervalo que está sendo atualizado | |
// e x, o o valor a ser atualizado. | |
void update(int node, int tl, int tr, int l, int r, int v){ | |
//LEMBRE-SE: Sempre chame a unlazy quando entrar numa função de update | |
unlazy(node, tl, tr) | |
// Se o intervalo abrangido pelo nó atual estiver completamente fora do intervalo a ser atualizado, | |
// não temos que atualizá-lo. | |
if(tl > r || tr < l) return; | |
// Se o intervalo abrangido pelo nó estiver inteiramente contido no intervalo atualizado, | |
// atualizaremos o nó. | |
if(tl >= l && tr <= r){ | |
lz[node] = x; | |
unlazy(node, tl, tr); | |
return; | |
} | |
// Caso contrário, declararemos mid como o índice que divide o nó atual em duas metades. | |
int mid = (tl+tr)/2; | |
//Agora, atualizaremos os filhos. | |
update(2*node+1,tl,mid,l, r ,v); | |
update(2*node+2,mid+1,tr,l,r,v); | |
// Após termos atualizado os dois filhos, atualizaremos o valor do nó atual. | |
tree[node] = max(tree[2*node + 1], tree[2*node + 2]); | |
} | |
// Os parametros da função Query são: | |
// O nó atual, tl e tr, o intervalo que o nó atual abrange, l e r, o intervalo que queremos calcular. | |
int query(int node, int tl, int tr, int l, int r){ | |
//LEMBRE-SE: Sempre chame a unlazy quando entrar numa função de query | |
unlazy(node, tl, tr) | |
// Se o intervalo que o nó atual abrange estiver totalmente fora do intervalo de l até r, retornaremos 0. | |
// Então, ou o início do intervalo do nó atual é maior que o fim dp intervalo que queremos calcular, ou | |
// o fim do intervalo do nó atual é menor que o início do intervalo que queremos calcular. | |
if(r<tl or l>tr) return 0; | |
// Se o intervalo que o nó atual representa estiver totalmente dentro do intervalo de l até r, então | |
// retornaremos o valor do nó atual. | |
if(l<=tl and r>=tr) return tree[node]; | |
// Caso contrário, declararemos mid como o índice que divide o nó atual em duas metades. | |
int mid = (tl+tr)>>1; | |
// Retornamos como resposta o máximo da resposta dos filhos do nó atual. | |
return max(query(2*node+1, tl, mid, l, r),query(2*node+2, mid+1, tr, l, r)); | |
} |
Problema 2: Dado um vetor inicialmente zerado de posições, efetuar as seguintes operações:
- Operação 1: Adicione a todos os valores nas posições do vetor no intervalo de à .
- Operação 2: imprima a soma dos valores do vetor no intervalo de à .
Nesse problema, devemos guardar o valor da soma do intervalo de cada nó, portanto não basta apenas somar diretamente o valor ao nó, já que se fizéssemos da forma lenta, atualizando valor por valor, sem propagação em lazy, o valor seria somado várias vezes no intervalo. Isso porém, é resolvível, já que sabemos que ele será somado exatamente o tamanho do intervalo vezes ao valor, ou seja, na função , iremos somar ao valor real na árvore, propagando apenas . Segue a implementação de cada função da árvore para esse problema.
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
// Os parametros da função unlazy são: | |
// O nó atual, tl e tr, o intervalo que o nó atual abrange, | |
void unlazy(int node, int tl, int tr){ | |
// Se não houver valor a ser propagado, não precisamos propagar | |
if(lz[node] == 0) return; | |
// Primeiro, atualizamos a árvore com o novo valor, somando ele vezes o número de vezes que aparecerá no intervalo | |
tree[node] += (tr-tl+1)*lz[node]; | |
// Se houver filhos, propagamos o vaor para eles, somando ao que já existe, pois pode ser que os filhos não | |
// tenham sido atualizados desde a última vez que essa função foi chamada em node | |
if(tl != tr){ | |
lz[2*node+1] += lz[node]; | |
lz[2*node+2] += lz[node]; | |
} | |
// Indicamos que não há mais valores a serem propagados em node | |
lz[node] = 0; | |
} | |
// Os parametros da função Update são: | |
// O nó atual, tl e tr, o intervalo que o nó atual abrange, l e r, o intervalo que está sendo atualizado | |
// e v, o o valor a ser atualizado. | |
void update(int node, int tl, int tr, int l, int r, int v){ | |
//LEMBRE-SE: Sempre chame a unlazy quando entrar numa função de update | |
unlazy(node, tl, tr) | |
// Se o intervalo abrangido pelo nó atual estiver completamente fora do intervalo a ser atualizado, | |
// não temos que atualizá-lo. | |
if(tl > r || tr < l) return; | |
// Se o intervalo abrangido pelo nó estiver inteiramente contido no intervalo atualizado, | |
// atualizaremos o nó. | |
if(tl >= l && tr <= r){ | |
lz[node] += x; | |
unlazy(node, tl, tr); | |
return; | |
} | |
// Caso contrário, declararemos mid como o índice que divide o nó atual em duas metades. | |
int mid = (tl+tr)/2; | |
//Agora, atualizaremos os filhos. | |
update(2*node+1,tl,mid,l, r ,v); | |
update(2*node+2,mid+1,tr,l,r,v); | |
// Após termos atualizado os dois filhos, atualizaremos o valor do nó atual. | |
tree[node] = tree[2*node + 1] + tree[2*node + 2]; | |
} | |
// Os parametros da função Query são: | |
// O nó atual, tl e tr, o intervalo que o nó atual abrange, l e r, o intervalo que queremos calcular. | |
int query(int node, int tl, int tr, int l, int r){ | |
//LEMBRE-SE: Sempre chame a unlazy quando entrar numa função de query | |
unlazy(node, tl, tr) | |
// Se o intervalo que o nó atual abrange estiver totalmente fora do intervalo de l até r, retornaremos 0. | |
// Então, ou o início do intervalo do nó atual é maior que o fim dp intervalo que queremos calcular, ou | |
// o fim do intervalo do nó atual é menor que o início do intervalo que queremos calcular. | |
if(r<tl or l>tr) return 0; | |
// Se o intervalo que o nó atual representa estiver totalmente dentro do intervalo de l até r, então | |
// retornaremos o valor do nó atual. | |
if(l<=tl and r>=tr) return tree[node]; | |
// Caso contrário, declararemos mid como o índice que divide o nó atual em duas metades. | |
int mid = (tl+tr)>>1; | |
// Retornamos como resposta a soma da resposta dos filhos do nó atual. | |
return query(2*node+1, tl, mid, l, r) + query(2*node+2, mid+1, tr, l, r); | |
} |
Problemas adicionais:
Segue alguns problemas interessantes que usam propagação em lazy: