专题 逆序对的求法
概念解释
逆序对,偏序问题的一种。我们先给出定义:
定义
设,且有
,则称这样的二元组
为逆序对。
下面讨论这种问题的求法。
问题
给出一个整数序列 ,元素个数为
统计这个序列中逆序对的数量。
我们主要会用到两种方法,分别是归并排序法和树状数组法。本专题同时收录在两者的专栏中。
算法分析
方法一 归并排序
方法一主要是运用归并的手段。首先给出一个想法:如果暴力地搜索,很显然是逐一比较,打擂台。考虑 个元素的规模,复杂度是
的。很有危险。所以必须考虑更优。用归并:分解子问题:每次二分这个序列,直到不可分,这就是可以直接解决的子问题;然后合并回溯,逐步求解,在回溯的时候统计数量。下面给出一般流程。
- 分解原问题,直到可以直接解决。对于本题,也就是分到一个子区间中只有一个元素;
- 解决这个小问题;
- 合并(回溯)这些子区间,并统计逆序对数量。
现在只需要进行一次归并排序即可。
方法二 树状数组
方法二借助树状数组的性质。但事实上是这么想到的:
还是从朴素的算法思考起。很显然是逐一比较,打擂台。现假设所有 中
的数量记为
,统计这样所有
的数量,也就是所谓逆序对的数量。也就是说:查询一个数列
的前缀和。可以发现,我们统计逆序对的过程等效于对该数列进行前缀和与单点修改操作。所以考虑使用树状数组。
关注一个细节:数列 长度不好确定。由于
中元素可以很大,所以这个数列的索引很有可能会爆掉。这里考虑离散化。下面给出具体的算法流程。
-
数组初始化全为0,总共进行
次修改,也就是说最多
个位置非0,记录这些位置即可。
- 排序,去重;
- 使用二分查找,在
的复杂度之内找到原
数列中某个元素的相对位置,那么
就可以表示序列中第
小的数的个数。
所以维护一个树状数组即可。
对于这两个方法,时间复杂度都是 的,空间复杂度是
的,所以还是非常优秀的。
参考程序
方法一 归并排序
有两种写法,都给出来。
//
#include<iostream>
#include<cstdio>
using namespace std;
const int N=5e5+5;
int a[N],t[N];
long long ans;
void merge_sort(int a[],int l,int r)
{
if(l==r) return;
int mid=l+r>>1;
merge_sort(a,l,mid);
merge_sort(a,mid+1,r);
for(int i=l,j=l,k=mid+1;i<=r;i++)
if(j==mid+1) t[i]=a[k++];
else if(k==r+1) t[i]=a[j++],ans+=k-mid-1;
else if(a[j]<=a[k]) t[i]=a[j++],ans+=k-mid-1;
else t[i]=a[k++];
for(int i=l;i<=r;i++) a[i]=t[i];
}
int main()
{
int n; scanf("%d",&n);
for(int i=1;i<=n;i++) scanf("%d",&a[i]);
merge_sort(a,1,n);
printf("%lld\n",ans);
return 0;
}
//
#include<iostream>
#define N 500000
using namespace std;
int a[N+5],b[N+5];
long long ans;
void solve(int l,int r)
{
if(l==r) return;
int m=(l+r)>>1,k=1,i=l,j=m+1;
solve(l,m),solve(m+1,r);
while(i<=m && j<=r)
{
if(a[i]<=a[j]) b[k++]=a[i++];
else ans+=(m-i+1),b[k++]=a[j++];
}
while(i<=m) b[k++]=a[i++];
while(j<=r) b[k++]=a[j++];
for(int w=l,s=1;w<=r;w++) a[w]=b[s++];
}
int main()
{
ios::sync_with_stdio(0),cin.tie(0);
int n;
cin>>n;
for(int i=1;i<=n;i++) cin>>a[i];
solve(1,n);
cout<<ans;
return 0;
}
方法二 树状数组
//
#include<iostream>
#include<algorithm>
using namespace std;
const int N=5e5+5;
int m,a[N],b[N],c[N];
#define lowbit(x) ((x)&(-(x)))
void add(int x,int y)
{
for(int i=x;i<=m;i+=lowbit(i)) c[i]+=y;
}
inline int sum(int x)
{
int ans=0;
for(int i=x;i;i-=lowbit(i)) ans+=c[i];
return ans;
}
int main()
{
int n; scanf("%d",&n);
for(int i=1;i<=n;i++) scanf("%d",&a[i]),b[i]=a[i];
sort(b+1,b+n+1);
m=unique(b+1,b+n+1)-b-1;
long long ans=0;
for(int i=n;i;i--)
{
int k=lower_bound(b+1,b+m+1,a[i])-b;
ans+=sum(k-1);
add(k,1);
}
printf("%lld",ans);
return 0;
}
细节实现
注意一点。在归并的过程中统计数据时,有一段
for(int i=l,j=l,k=mid+1;i<=r;i++)
if(j==mid+1) t[i]=a[k++];
else if(k==r+1) t[i]=a[j++],ans+=k-mid-1;
else if(a[j]<=a[k]) t[i]=a[j++],ans+=k-mid-1;
else t[i]=a[k++];
好好分析一下。首先变量统计是逆序对的数量,所以为什么会有两个分支涉及到?这是因为排序之后在后面一个区间,也就是
中,所有的元素趋于有序,而且
中的元素都会比
要小,所以会产生这么多的逆序对。归并的思路还是非常有味道的,毕竟分治算法(尤其是二分)非常著名,也非常好用。
总结归纳
一维偏序问题主要就是这么两种解法都很优秀,后期本专题会填补与归并排序、树状数组的联系,目前还没有写出来,所以这只是一个初稿,后期会更加完善。当然,不影响阅读学习,都是笔者学习的体会,结合一些参考资料总结而出的。

浙公网安备 33010602011771号