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 de números inteiros, calcule a quantidade de inversões em , isto é, a quantidade de pares tais que e .
Restrições: e .
Solução: Perceba que a quantidade total de inversões em é o mesmo que o somatório, para cada índice , da quantidade de inversões que participa, ou seja, a quantidade de índices tais que e . 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 com valor igual a tal índice. De início, deixamos a BIT vazia e iteramos por cada posição do vetor, começando de e indo até 1. Assim, para descobrir de quantas inversões participa, basta calcular a soma dos valores desde o índice 1 ao na BIT (o total de números menores que até o momento). Depois, acrescentamos 1 ao índice da estrutura, realizando o mesmo procedimento nos índices seguintes. Como a BIT realiza as duas operações de soma e atualização em , a complexidade final será . Porém, a BIT precisa de uma posição reservada para cada um dos valores em , que podem ser muito grandes (até ), e já que criar uma BIT com elementos é muito custoso, precisamos de outra solução.
Observe que como a quantidade de números distintos em é no máximo , o vetor da BIT possui muitos "espaços vazios". Além disso, o valor de cada elemento em é 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 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 . 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 , e já que o maior valor comprimido de um número em será , podemos usar a BIT seguindo o mesmo algoritmo descrito acima. A complexidade final será . Confira o 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
// 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 de inteiros com seus valores inicialmente iguais a 0. São dados pares indicando que o valor de foi atualizado para . Os pares são dados em ordem estritamente crescente de . Após isso, perguntas serão feitas, sendo representandas por um par . Para cada pergunta, calcule a soma dos valores em com índices entre (e incluindo) e .
Restrições: .
Solução: Novamente, não é eficiente guardar o vetor , já que o valor de 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 pares, inserindo cada um deles em um vetor na ordem em que são dados. Note, assim, que já estará ordenado pelo valor (o primeiro elemento) de cada par. Feito isso, comprimimos as posições dadas, já que pela descrição do problema, são todas distintas (considere que o valor mapeado do índice num par em é a própria posição do par neste vetor).
Porém, os números nos índices e 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 será perdida. Apesar disso, note que se o valor no índice de uma pergunta, de fato, for 0, a soma no intervalo será a mesma que no intervalo , onde é o primeiro índice em — maior ou igual a —, cujo valor no vetor é positivo. Isso vale porque todos os números entre e são 0. De maneira similar para , se o valor em é 0, a soma em será a mesma que em , onde é último índice em — menor ou igual a —, cujo valor no vetor é positivo. É possível encontrar os valores mapeados de e em fazendo-se uma busca binária no vetor , para cada uma das perguntas (mais detalhes no código abaixo). De tal forma, conseguimos respondê-las trabalhando apenas com índices do vetor .
A fim de finalizar o problema, precisamos calcular a soma dos valores entre dois índices e quaisquer em . Se for o valor do -ésimo par em , então observe que
Portanto, se é igual à soma dos valores desde a posição 1 até ,
Logo, é possível calcular a soma referida acima em , precalculando o vetor para cada posição no vetor com complexidade (basta ver que . Esta técnica é bastante útil e é conhecida como soma de prefixos. A complexidade final do problema será , devido à busca binária realizada em cada uma das perguntas. Confira o código abaixo 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
// 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
- Arco e Flecha (OBI 2016 - P1 - F2)
- BGSHOOT
- Wowow (Olimpíada Canadense de Computação (CCO) 2010)
- Enemy is weak
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.