专题 逆序对的求法

概念解释

        逆序对,偏序问题的一种。我们先给出定义:

定义

        设i<j,且有 a_{i}>a_{j} ,则称这样的二元组 (i,j) 为逆序对。

        下面讨论这种问题的求法。 

问题

        给出一个整数序列 A ,元素个数为 n 统计这个序列中逆序对的数量。

        我们主要会用到两种方法,分别是归并排序法和树状数组法。本专题同时收录在两者的专栏中。

算法分析

方法一 归并排序

        方法一主要是运用归并的手段。首先给出一个想法:如果暴力地搜索,很显然是逐一比较,打擂台。考虑 n 个元素的规模,复杂度是 O(n^{2}) 的。很有危险。所以必须考虑更优。用归并:分解子问题:每次二分这个序列,直到不可分,这就是可以直接解决的子问题;然后合并回溯,逐步求解,在回溯的时候统计数量。下面给出一般流程。

  1. 分解原问题,直到可以直接解决。对于本题,也就是分到一个子区间中只有一个元素;
  2. 解决这个小问题;
  3. 合并(回溯)这些子区间,并统计逆序对数量。

        现在只需要进行一次归并排序即可。

方法二 树状数组

        方法二借助树状数组的性质。但事实上是这么想到的:

        还是从朴素的算法思考起。很显然是逐一比较,打擂台。现假设所有i<j 中a_{i}=k 的数量记为t_{k},统计这样所有t_{k}的数量,也就是所谓逆序对的数量。也就是说:查询一个数列 t 的前缀和。可以发现,我们统计逆序对的过程等效于对该数列进行前缀和与单点修改操作。所以考虑使用树状数组。

        关注一个细节:数列 t 长度不好确定。由于A 中元素可以很大,所以这个数列的索引很有可能会爆掉。这里考虑离散化。下面给出具体的算法流程。

  1.  t 数组初始化全为0,总共进行 n 次修改,也就是说最多 n 个位置非0,记录这些位置即可。
  2. 排序,去重;
  3. 使用二分查找,在 O(log_{2}n) 的复杂度之内找到原 A 数列中某个元素的相对位置,那么 t_{k} 就可以表示序列中第 k 小的数的个数。

        所以维护一个树状数组即可。 

        对于这两个方法,时间复杂度都是O(nlog_{2}n) 的,空间复杂度是O(n) 的,所以还是非常优秀的。

参考程序

方法一 归并排序

有两种写法,都给出来。

//
#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++];

        好好分析一下。首先变量ans统计是逆序对的数量,所以为什么会有两个分支涉及到?这是因为排序之后在后面一个区间,也就是[mid+1,r]中,所有的元素趋于有序,而且[mid+1,k-1] 中的元素都会比 a_{j} 要小,所以会产生这么多的逆序对。归并的思路还是非常有味道的,毕竟分治算法(尤其是二分)非常著名,也非常好用。 

总结归纳

        一维偏序问题主要就是这么两种解法都很优秀,后期本专题会填补与归并排序、树状数组的联系,目前还没有写出来,所以这只是一个初稿,后期会更加完善。当然,不影响阅读学习,都是笔者学习的体会,结合一些参考资料总结而出的。

posted @ 2025-07-15 21:23  枯骨崖烟  阅读(59)  评论(0)    收藏  举报  来源