Informática - Ideia 01

Escrito por Lúcio Cardoso:

A técnica de compressão de coordenadas consiste, essencialmente, em mapear valores grandes dados em um problema para valores menores, a fim de reduzir gastos de memória e tempo. Vamos ilustrar a ideia com os seguintes exemplos:

Exemplo 1

Dado um vetor V de N números inteiros, calcule a quantidade de inversões em V, isto é, a quantidade de pares (i, j) tais que i < j e V[i] > V[j].

Restrições: N \leq 10^5 e 1 \leq V[i] \leq 10^9, \forall i \leq N.

Solução: Perceba que a quantidade total de inversões em V é o mesmo que o somatório, para cada índice i, da quantidade de inversões que i participa, ou seja, a quantidade de índices j tais que i < j e V[i] > V[j]. Para calcular isso, usaremos a estrutura de dados BIT (ou Binary Indexed Tree) da seguinte maneira: Criamos uma BIT onde cada um de seus índices guardará a quantidade de números em V com valor igual a tal índice. De início, deixamos a BIT vazia e iteramos por cada posição i do vetor, começando de N e indo até 1. Assim, para descobrir de quantas inversões i participa, basta calcular a soma dos valores desde o índice 1 ao V[i]-1 na BIT (o total de números menores que V[i] até o momento). Depois, acrescentamos 1 ao índice V[i] da estrutura, realizando o mesmo procedimento nos índices seguintes. Como a BIT realiza as duas operações de soma e atualização em O(\log{}n), a complexidade final será O(n\log{}n). Porém, a BIT precisa de uma posição reservada para cada um dos valores em V, que podem ser muito grandes (até 10^9), e já que criar uma BIT com 10^9 elementos é muito custoso, precisamos de outra solução.

Observe que como a quantidade de números distintos em V é no máximo N, o vetor da BIT possui muitos "espaços vazios". Além disso, o valor de cada elemento em V é irrelevante; o que nos interessa ao calcular as inversões são apenas as relações (> ou <) entre tais valores. Queremos então reduzir (comprimir) os números no vetor sem modificar as relações entre eles.

Para isso, vamos inserir cada valor de V em um vetor auxiliar e então ordená-lo. Depois, mapeamos cada número neste vetor para um número menor de acordo com a ordem presente (ou seja, o menor número será mapeado para 1, o segundo menor para 2, etc.), comprimindo, assim, os valores de V. Podemos, assim, substituir cada um dos números no vetor original pelos seus respectivos valores mapeados, pois como o vetor auxiliar foi ordenado, as relações entre os números foram mantidas. Utilizando o próprio map do C++, é possível realizar a compressão desejada em O(N\log{}N), e já que o maior valor comprimido de um número em V será N, podemos usar a BIT seguindo o mesmo algoritmo descrito acima. A complexidade final será O(N\log{}N). Confira o código para melhor entendimento:


// NOIC - Ideia 1
// Exemplo 1
// Complexidade: O(N log N)
#include <bits/stdc++.h>
using namespace std;
const int maxn = 1e5+10;
int n, V[maxn], bit[maxn];
// somar v na posição x
void upd(int x, int v)
{
for (; x <= n; x += (x&-x))
bit[x] += v;
}
// soma de 1 até x
int soma(int x)
{
int ans = 0;
for (; x > 0; x -= (x&-x))
ans += bit[x];
return ans;
}
int main(void)
{
cin >> n;
vector<int> C; // vetor auxiliar
for (int i = 1; i <= n; i++)
{
cin >> V[i];
C.push_back(V[i]);
}
sort(C.begin(), C.end()); // ordenamos C
int ind = 0;
map<int, int> Mp;
for (int i = 0; i < C.size(); i++)
if (Mp.find(C[i]) == Mp.end()) // se C[i] não foi mapeado ainda
Mp[C[i]] = ++ind; // valor mapeado de C[i]
long long ans = 0; // resposta (que pode chegar a long long)
for (int i = n; i >= 1; i--)
{
// a partir de agora trocamos os V[i]s pelos seus valores mapeados
ans += soma(Mp[V[i]]-1); // inversões que i participa
upd(Mp[V[i]], 1);
}
cout << ans << "\n";
}

Exemplo 2

Temos um vetor V de N inteiros com seus valores inicialmente iguais a 0. São dados M pares (i, x) (x \in \mathbb{Z}_+^*) indicando que o valor de V[i] foi atualizado para x. Os pares são dados em ordem estritamente crescente de i. Após isso, K perguntas serão feitas, sendo representandas por um par (l, r). Para cada pergunta, calcule a soma dos valores em V com índices entre (e incluindo) l e r.

Restrições: N \leq 10^9, M \leq 10^5, K \leq 10^5.

Solução: Novamente, não é eficiente guardar o vetor V, já que o valor de N pode ser muito grande. No entanto, há muitos números iguais a 0 no vetor, e como nenhum deles irá contribuir para a resposta das perguntas, podemos enxergá-los como "gaps". Vamos então fazer uma compressão apenas com as posições dadas nos M pares, inserindo cada um deles em um vetor C na ordem em que são dados. Note, assim, que C já estará ordenado pelo valor i (o primeiro elemento) de cada par. Feito isso, comprimimos as M posições dadas, já que pela descrição do problema, são todas distintas (considere que o valor mapeado do índice i num par em C é a própria posição do par neste vetor).

Porém, os números nos índices l e r dados nas perguntas podem ser 0, e, consequentemente, não serão considerados na compressão; logo, a ordem relativa de tais índices com aqueles em C será perdida. Apesar disso, note que se o valor no índice l de uma pergunta, de fato, for 0, a soma no intervalo [l...r] será a mesma que no intervalo [a...r], onde a é o primeiro índice em V — maior ou igual a l —, cujo valor no vetor é positivo. Isso vale porque todos os números entre a e l são 0. De maneira similar para r, se o valor em r é 0, a soma em [l...r] será a mesma que em [l...b], onde b é último índice em V — menor ou igual a r —, cujo valor no vetor é positivo. É possível encontrar os valores mapeados de a e b em O(\log{}M) fazendo-se uma busca binária no vetor C, para cada uma das perguntas (mais detalhes no código abaixo). De tal forma, conseguimos respondê-las trabalhando apenas com índices do vetor C.

A fim de finalizar o problema, precisamos calcular a soma dos valores entre dois índices l e r quaisquer em C. Se x_i for o valor x do i-ésimo par em C, então observe que

\sum_{i=l}^r x_i = \sum_{i=1}^r x_i - \sum_{i=1}^{l-1} x_i

Portanto, se soma[i] é igual à soma dos valores x desde a posição 1 até i,

\sum_{i=l}^r x_i = soma[r] - soma[l-1]

Logo, é possível calcular a soma referida acima em O(1), precalculando o vetor soma para cada posição no vetor com complexidade O(M) (basta ver que soma[i] = soma[i-1] + x_i). Esta técnica é bastante útil e é conhecida como soma de prefixos. A complexidade final do problema será O(K\log{}M), devido à busca binária realizada em cada uma das K perguntas. Confira o código abaixo para melhor entendimento:


// NOIC - Ideia 1
// Exemplo 2
// Complexidade: O(K log M)
#include <bits/stdc++.h>
using namespace std;
const int maxn = 1e5+10;
typedef pair<int, int> pii;
int soma[maxn];
int main(void)
{
int n, m, k;
cin >> n >> m >> k;
vector<pii> C;
for (int j = 1; j <= m; j++)
{
int i, x;
cin >> i >> x;
C.push_back({i, x}); // inserimos cada par em C
}
soma[0] = 0; // caso base
for (int i = 1; i <= m; i++)
soma[i] = soma[i-1] + C[i-1].second; // calculamos a soma de prefixos
for (int i = 1; i <= k; i++)
{
int l, r;
cin >> l >> r;
// a função lower bound do C++ realiza uma busca binária
// ela retorna a primeira posição em um vetor ordenado que tem valor maior ou igual a um certo parâmetro dado
// no caso, queremos a primeira posição em C cujo índice no par é maior ou igual a l
// por isso usamos o par {l, 0}
int a = (int)(lower_bound(C.begin(), C.end(), make_pair(l, 0))-C.begin());
// fazemos o mesmo pra posição b, nesse caso para achar a última posição menor
// ou igual a r encontramos a primeira maior ou igual a r+1 e subtraímos 1 dela
int b = (int)(lower_bound(C.begin(), C.end(), make_pair(r+1, 0))-C.begin()-1);
a++, b++; // como C é 0-indexado, aumentamos 1 em a e b
if (a > b) cout << "0\n"; // se a >= b, todos os números em [l...r] são iguais a 0
else cout << soma[b]-soma[a-1] << "\n"; // soma em [a...b]
}
}

 

Problemas para praticar

Obs.: Os problemas acima utilizam compressão de coordenadas de alguma maneira, porém podem precisar de alguma estrutura de dados (como BIT ou Segment Tree) ou outra técnica.