Otimização de Consultas de Intervalo com Decomposição em Raiz Quadrada e Árvore de Fenwick

O Problema das Atualizações Baseadas em Passsos

Ao lidar com problemas de estruturas de dados que exigem a adição de um valor $k$ a todos os múltiplos de um determinado número $D$, seguida de consultas de soma em um intervalo $[L, R]$, a escolha ingênua da estrutura de dados frequentemente leva a estouros de limite de tempo. A solução ideal exige o equilíbrio entre o tempo de atualização e o tempo de consulta, algo que pode ser alcançado combinando uma Árvore de Fenwick (Binary Indexed Tree) com a técnica de Decomposição em Raiz Quadrada (Square Root Decomposition).

Abordagem 1: Atualização Direta na Árvore de Fenwick

A ideia mais imediata é iterar por todos os múltiplos de $D$ e atualizar diretamente a Árvore de Fenwick. Durante uma consulta, a árvore responde em tempo logarítmico.


#include <iostream>
#include <vector>

using namespace std;

const int MAX_VAL = 200005;
long long bit[MAX_VAL];
int max_size, queries_count;

void add(int idx, long long val) {
    for (; idx <= max_size; idx += idx & -idx) {
        bit[idx] += val;
    }
}

long long prefix_sum(int idx) {
    long long total = 0;
    for (; idx > 0; idx -= idx & -idx) {
        total += bit[idx];
    }
    return total;
}

long long range_sum(int left, int right) {
    return prefix_sum(right) - prefix_sum(left - 1);
}

Embora as consultas sejam rápidas, $O(\log N)$, a atualização se torna proibitiva quando $D$ é pequeno. Se $D = 1$, a operação exigirá $N$ atualizações na árvore, resultando em uma complexidade de $O(N \log N)$ por operação de atualização, o que causa falha por limite de tempo (TLE) em cenários com muitas operações.

Abordagem 2: Avaliação Preguiçosa (Lazy Evaluation)

Para contornar o problema das atualizações custosas, podemos adiar o processamento. Em vez de atualizar a Árvore de Fenwick, armazenamos o valor a ser adicionado em um vetor auxiliar pending[D] += k. Quando uma consulta no intervalo $[L, R]$ é realizada, iteramos por todos os possíveis passos $i$ de $1$ até $N$, calculando quantos múltiplos de $i$ existem no intervalo e multiplicando pelo valor acumulado em pending[i].


long long process_query_lazy(int L, int R) {
    long long result = range_sum(L, R);
    for (int i = 1; i <= max_size; ++i) {
        if (pending[i] != 0) {
            long long multiples_in_range = (R / i) - ((L - 1) / i);
            result += multiples_in_range * pending[i];
        }
    }
    return result;
}

Esta abordagem inverte o gargalo. As atualizações agora ocorrem em $O(1)$, mas as consultas exigem uma iteração completa até $N$, tornando-as $O(N)$. Se o valor de $D$ for consistentemente grande, o vetor pending se enche e as consultas se tornam extremamente lentas.

Solução Híbrida: Decomposição em Raiz Quadrada

A solução definitiva une os dois métodos usando um limiar baseado na raiz quadrada do tamanho máximo do array, $B = \sqrt{N}$.

  • Para passos pequenos ($D \le B$): Utilizamos a avaliação preguiçosa. A atualização é $O(1)$ e, durante a consulta, iteramos apenas até $B$, o que leva $O(\sqrt{N})$.
  • Para passos grandes ($D > B$): Atualizamos diretamente a Árvore de Fenwick. Como $D$ é grande, existem no máximo $N/D$ múltiplos, o que equivale a $O(\sqrt{N})$ atualizações. A consulta permanece $O(\log N)$ para a parte da árvore.

Abaixo está a implementação completa integrando essa lógica híbrida:


#include <iostream>
#include <vector>
#include <cmath>

using namespace std;

const int MAX_VAL = 200005;
long long fenwick_tree[MAX_VAL];
long long lazy_updates[MAX_VAL];
int array_size, total_queries;

void update_fenwick(int idx, long long val) {
    for (; idx <= array_size; idx += idx & -idx) {
        fenwick_tree[idx] += val;
    }
}

long long query_fenwick(int idx) {
    long long sum = 0;
    for (; idx > 0; idx -= idx & -idx) {
        sum += fenwick_tree[idx];
    }
    return sum;
}

long long get_range_sum(int left, int right) {
    return query_fenwick(right) - query_fenwick(left - 1);
}

void execute() {
    if (!(cin >> array_size >> total_queries)) return;
    
    int threshold = sqrt(array_size);
    
    for (int q = 0; q < total_queries; ++q) {
        int operation, param1;
        long long param2;
        cin >> operation >> param1 >> param2;
        
        if (operation == 1) {
            int step = param1;
            long long value_to_add = param2;
            
            if (step <= threshold) {
                lazy_updates[step] += value_to_add;
            } else {
                for (int i = step; i <= array_size; i += step) {
                    update_fenwick(i, value_to_add);
                }
            }
        } else {
            int L = param1;
            int R = param2;
            
            long long final_answer = get_range_sum(L, R);
            
            for (int step = 1; step <= threshold; ++step) {
                if (lazy_updates[step] != 0) {
                    long long count_multiples = (R / step) - ((L - 1) / step);
                    final_answer += count_multiples * lazy_updates[step];
                }
            }
            cout << final_answer << "\n";
        }
    }
}

int main() {
    ios_base::sync_with_stdio(false);
    cin.tie(NULL);
    execute();
    return 0;
}

Tags: árvore de Fenwick Decomposição em Raiz Quadrada C++ Estruturas de Dados Algoritmos Competitivos

Publicado em 7-2 00:42