浅谈如何求逆序对
浅谈求逆序对的两种方式
本篇博客讲解一下信息学奥林匹克竞赛中一种小技巧(我也不知道这是算法还是技巧)——求逆序对。
逆序对的概念
先放一波啥是逆序对...
对于一个数列\(a\),假如\(a[i]>a[j]\)并且\(i<j\),那么这个\(a[i],a[j]\)就叫做这个数列的一个逆序对。
简单理解一下:假如本来这个数列是单调递增的,突然出来了一对不和谐的,它非要皮一下,两个数调换一下位置。那么这个不和谐的数对就叫做逆序对。
归并排序求逆序对
归并排序是求逆序对的一个常见并好用的手段。
如果又小可爱不太了解归并排序,那么请翻阅我的这篇博客:
其实这篇博客已经讲了咋用归并排序求逆序对...就是我下面讲的那些(Ctrl CV大法)
首先来放结论:如果在归并排序过程中,出现\(a[i]>a[j]\),那么就会产生\(mid-i+1\)个逆序对。
很奇妙吧?下面来讲证明:
因为我们做归并排序是用到了分治的思想,最后的操作其实就是递归回溯,从小到大地合并,所以这个时候,我们的两个子序列(即\(l-mid\),\(mid+1-r\)其实都是已经排好序的),这个时候,出现了一个不和谐的\(a[i]\),说明从这个数一直到\(a[mid]\)的所有数都是不和谐的。我们直接累加就好。
代码只需要在模板归并排序的基础上再加一行即可:(已经用\(Attention\)标明)
void merge_sort(int l,int r)
{
if(l==r)
return;
int mid=(l+r)>>1;
merge_sort(l,mid);
merge_sort(mid+1,r);
int i=l,j=mid+1,k=l;
while(i<=mid && j<=r)
{
if(a[i]<=a[j])
b[k++]=a[i++];
else
{
b[k++]=a[j++];
ans+=mid-i+1;//Attention
}
}
while(i<=mid)
b[k++]=a[i++];
while(j<=r)
b[k++]=a[j++];
for(int p=l;p<=r;p++)
a[p]=b[p],b[p]=0;
}
树状数组求逆序对
使用树状数组求逆序对的想法可能更加的抽象一些。
关于树状数组,如果有需要补习的小伙伴,推荐自己的这篇博客:
首先,我们需要明白,为什么逆序对可以使用树状数组来求:其实,根据逆序对的定义,我们会发现:其实求解逆序对的过程就是在找一个序列中,在一个数的前面有多少比它大的数。那么,这种区间统计的题,完全可以使用树状数组这种数据结构来解决。
树状数组的功能就是单点修改,区间查询。那么,我们开一个树状数组
(注意:这里的数据可能需要进行离散化,当数据量过大的时候,需要把区间大小离散成元素数量,如有离散化不明白的小伙伴,可以看我的这篇博客:)
这个树状数组表示的是在当前元素前且比当前元素大的元素个数。我们从头开始扫,每碰到一个数,就用树状数组的修改操作向上修改,加1。我们查询的时候就可以知道有多少个数比它大,然后用现在已经插入的元素个数减去查询的数量(包括它自己),就可以得出当前逆序对的个数。
模板大致如下:(JDOJ 1927 求逆序对)
#include<iostream>
#include<cstdio>
#include<algorithm>
using namespace std;
int c[500010],rk[500010],n;
long long ans;
struct point
{
int num,val;
}a[500010];
inline bool cmp(point q,point w)
{
if(q.val==w.val)
return q.num<w.num;
return q.val<w.val;
}
inline void fix(int p,int d)
{
for(;p<=n;p+=p&-p)
c[p]+=d;
}
inline int getsum(int x)
{
int sum=0;
for(;x;x-=x&-x)
sum+=c[x];
return sum;
}
int main()
{
scanf("%d",&n);
for(int i=1;i<=n;i++)
scanf("%d",&a[i].val),a[i].num=i;
sort(a+1,a+1+n,cmp);
for(int i=1;i<=n;i++)
rk[a[i].num]=i;
for(int i=1;i<=n;i++)
{
fix(rk[i],1);
ans+=i-getsum(rk[i]);
}
printf("%lld",ans);
return 0;
}