给定 {1,2,3,…,n}\{1,2,3,…,n\}{1,2,3,…,n} 的一个排列,从排列中每次删除一个数字,共删除 mmm 个数字,在每次删除数字之前输出该排列的逆序对数。数组 AAA 的逆序对数定义为二元组(iii,jjj)的数量:i<ji<ji<j 且 A[i]>A[j]A[i]>A[j]A[i]>A[j]。
输入
输入包含多组测试数据。每组测试数据的第一行包含两个整数 nnn 和 m(1≤n≤200000,1≤m≤100000)m(1\leq n \leq 200000,1 \leq m \leq 100000)m(1≤n≤200000,1≤m≤100000)。之后接着 nnn 行,表示最初的排列。然后是 mmm 行,表示被删除的数,按照删除的顺序排列。不会发生同一个数被删除两次的情形。输入以文件结束符表示输入结束。
输出
对于每个删除操作,输出在进行删除操作之前的逆序对数。
样例输入
5 4
1
5
3
4
2
5
1
4
2
样例输出
5
2
2
1
分析
由于给定的是 [1,n][1,n][1,n] 的一个排列,如果将给定的序列中某个元素的序号 iii 与其值 A[i]A[i]A[i] 视为一个二元组 (i,A[i])(i,A[i])(i,A[i]),则 (i,A[i])(i,A[i])(i,A[i]) 对应直角坐标系第一象限内的一个点。如果将某个元素 A[k]A[k]A[k] 删除,按照逆序对数的定义可知,从 A[1]A[1]A[1] 到 A[k−1]A[k-1]A[k−1] 中大于 A[k]A[k]A[k] 的数与 A[k]A[k]A[k] 构成逆序对,从 A[k+1]A[k+1]A[k+1] 到 A[n]A[n]A[n] 中小于 A[k]A[k]A[k] 的数也与 A[k]A[k]A[k] 构成逆序对,反映在二维平面上,即为点 (k,A[k])(k,A[k])(k,A[k]) 左上角区域内的点数和其右下角区域内的点数之和即为所求的逆序对数,如下图所示(仅为示意,未按本题的实际情况绘制):

可以将一维的根号分块拓展为二维的根号分块来解决这个问题。即将矩形区域 (0,0)(0,0)(0,0),(n,n)(n,n)(n,n) 划分为若干的方块,统计每个方块内所包含的点数。定义一个函数 countSum(x,y)countSum(x, y)countSum(x,y),用于统计左下角顶点为 (0,0)(0,0)(0,0),右上角顶点为 (x,y)(x,y)(x,y) 的矩形所包含的点数,若删除 (k,A[k])(k,A[k])(k,A[k]),则减少的逆序对数为:
countSum(x,n)+countSum(n,y)−2∗countSum(x,y)countSum(x, n) + countSum(n, y) - 2 * countSum(x, y)countSum(x,n)+countSum(n,y)−2∗countSum(x,y)
为了便于计算,可以维护每行方格所包含点数的前缀和,在统计点数时直接使用前缀和进行累加以便提高效率,在删除点时,使用 O(n)O(\sqrt{n})O(n) 的时间维护前缀和即可。
参考代码
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 200010, BLOCK_SIZE = 450, INF = 0x3f3f3f3f;
struct BLOCK { long long cnt, prefixSum; } blocks[BLOCK_SIZE][BLOCK_SIZE];
int n, m, X[MAXN], Y[MAXN];
void updatePrefixSum(int bx, int by)
{
long long sum = (bx > 0 ? blocks[bx - 1][by].prefixSum : 0);
for (int i = bx; i < BLOCK_SIZE; i++) {
sum += blocks[i][by].cnt;
blocks[i][by].prefixSum = sum;
}
}
void add(int x, int y)
{
X[y] = x, Y[x] = y;
int bx = x / BLOCK_SIZE, by = y / BLOCK_SIZE;
blocks[bx][by].cnt++;
updatePrefixSum(bx, by);
}
void remove(int x, int y)
{
X[y] = INF, Y[x] = INF;
int bx = x / BLOCK_SIZE, by = y / BLOCK_SIZE;
blocks[bx][by].cnt--;
updatePrefixSum(bx, by);
}
long long countSum(int x, int y)
{
long long cnt = 0;
int bw = (x + 1) / BLOCK_SIZE, bh = (y + 1) / BLOCK_SIZE;
for (int i = 0; i < bh; i++)
if (bw > 0)
cnt += blocks[bw - 1][i].prefixSum;
for (int i = bw * BLOCK_SIZE; i <= x; i++)
if (Y[i] < bh * BLOCK_SIZE)
cnt++;
for (int i = bh * BLOCK_SIZE; i <= y; i++)
if (X[i] <= x)
cnt++;
return cnt;
}
long long countInversions(int x, int y)
{
return countSum(x, n - 1) + countSum(n - 1, y) - 2 * countSum(x, y);
}
int main(int argc, char *argv[])
{
cin.tie(0), cout.tie(0), ios::sync_with_stdio(false);
while (cin >> n >> m) {
long long r = 0;
memset(X, 0x3f, sizeof X);
memset(Y, 0x3f, sizeof Y);
memset(blocks, 0, sizeof blocks);
for (int i = 0, ai; i < n; i++) {
cin >> ai; ai--;
add(i, ai);
r += countInversions(i, ai);
}
for (int i = 0, ai; i < m; i++) {
cout << r << '\n';
cin >> ai; ai--;
r -= countInversions(X[ai], ai);
remove(X[ai], ai);
}
}
return 0;
}
这篇博客介绍了UVa在线判题系统中的11990题——动态逆序对。题目要求在每次删除数组排列中的一个元素前,计算当前的逆序对数量。逆序对是指在一个排列中,较小的元素位于较大的元素之后的配对。文章通过分析问题并结合二维直角坐标系的点分布,提出了用根号分块的方法来统计逆序对,并给出了计算点数的前缀和策略,以在删除元素时高效地更新逆序对数量。
647

被折叠的 条评论
为什么被折叠?



