题目描述
猫猫 TOM 和小老鼠 JERRY 最近又较量上了,但是毕竟都是成年人,他们已经不喜欢再玩那种你追我赶的游戏,现在他们喜欢玩统计。
最近,TOM 老猫查阅到一个人类称之为“逆序对”的东西,这东西是这样定义的:对于给定的一段正整数序列,逆序对就是序列中 a_i>a_jai>aj 且 i<ji<j 的有序对。知道这概念后,他们就比赛谁先算出给定的一段正整数序列中逆序对的数目。注意序列中可能有重复数字。
解答
逆序对在一开始学习归并排序的时候就有了一个O(nlogn)级别的解法,已经是比较快了,然后今天学习了一下树状数组,找到了另外一个O(nlogn)的解法,但在这一道题上面可能还会慢一点,借鉴了一些别人的题解。
统计一个序列中有多少个逆序对,也就是找每个数字前面有多少个比这个数字大的。只要每进来一个数字,我就能直接查询到多少个数字比这个数字要大,那么任务就可以完成,因此可以用一个后缀和数组来维护每次加入这个数字后,比这个数字大的数字的数目,从而实现快速查询,就比如加入了3,那么1,2都要加1,因为1,2,都多了一个比这个数字大的数目,然后再进来2的话,这时候2已经是1了,直接加1到答案中,不过每次给区间都增加是耗时的,这时候引入差分数组,对点进行更新,也就是树状数组添加操作;同时对后缀和求和就可以得到每个点的值,这其实就是树状数组的查询操作。
两个问题:
1. 这里的数据范围是1e9,所以要维护这么大的范围的数据其实是不现实的,无论是空间还是时间都是不可以的,但是这里比较特殊的是数字之间只有大小的关系,1 2 3和1 2 10本质是一样的,因此只要将数组排序之后用编号给各个数字重新赋值,利用形式大小来进行统计就可以,比如1 10 100直接变换为1 2 3,这样1e5的维护长度是可以接受的。
2. 关于后缀和的我还是不太清楚,因此我将其转化为了求前缀和,这样差分求和也比较熟悉直观,区别就是要反过来,本来应该大的变为小的,也就是把1 10 100转为了 3 2 1,然后统计每个数加入时有多少个比它小的,道理还是一样的,为什么转和怎么转如下。
下面详细说一下排序这里。每个数字可以读为一个结构体,里面存放了每个数字的号次和数字大小,然后对结构体数组排序,先按照大小排序,大小一样的话,就把先放进来的放在前面。
具体过程是这样的,需要统计每次进入一个值后有多少个比这个小的值。比如1 3 3 2,首先排序为3(2) 3(3) 2(4) 1(1),括号里面是出现的号次,观察第一个是在第二个出现,我们现在希望把第二个出现的数字等效替换为一个数字,这个数字就是编号,所以f[2]=1,重复操作,重新编号为4 1 2 3。设定初值ans=0,差分数组b,第一个拿到4,发现b(4)为0,也就是比4小的没有,ans还是0,更新b[5]+1,代表的是所有比4大的+1,这样假设来了一个5,那么差分求和,sum(5)=b(1)+...b(5)=1,就可以加到ans里面去。
这个解法因为涉及到排序,也是O(nlogn)所以可能比归并要慢一点,但是如果不用排序,也就是数据范围小,那就不一定了。
下面是代码:
#include<stdio.h>
#include<algorithm>
#define ll long long
using namespace std;
int n;
ll ans = 0;
int f[600000];
int t[600000];
struct node
{
int num;//实际数据
int bh;//编号
}a[600000];
int lowbit(int x)
{
return x & (-x);
}
void add(int x, int k)
{
while (x <= n)
{
t[x] += k;
x = x + lowbit(x);
}
}
int ask(int x)
{
int ans = 0;
while (x)
{
ans += t[x];
x -= lowbit(x);
}
return ans;
}
int cmp(node x, node y)
{
if (x.num != y.num)
{
return x.num > y.num;
}
else
{
return x.bh > y.bh;
}
}
int main()
{
scanf("%d", &n);
for (int i = 1; i <= n; i++)
{
scanf("%d", &a[i].num);
a[i].bh = i;
}
sort(a + 1, a + n + 1, cmp);//排序,大小相同,次序优先,保证相同的值不能算逆序对
for (int i = 1; i <= n; i++)//重新编号操作
f[a[i].bh] = i;
for (int i = 1; i <= n; i++)//树状数组
{
//利用差分计算
ans += ask(f[i]);
add(f[i] + 1, 1);
}
printf("%lld", ans);
return 0;
}