逆序对
逆序对
1.归并算法
归并算法过程中,存在以下几种逆序对:
1)左区间内
2)右区间内
3)横跨两个区间
设归并数组已经排到第 \(k\) 个数
只考虑第三种逆序对的话,由归并排序的性质,我们先找到了已经分别单调的左右区间。
若左区间的 \(a[i]\) 大于右区间的 \(a[j]\) :
\(a[j]\) 和右区间内已经不会成逆序对了,而它和左区间中 \(a[i]\) 前的数也不可能是逆序对,否则在 \(a[i]\) 之前它就加入归并数组中了。
所以,\([str,i)\) 和 \([mid+1,j)\) 不对逆序对做贡献,而这两个区间数的总和是k个(因为 \(i\) 和 \(j\) 是从 \(str\) 和 \(mid\) 随着 \(k++\)上去的,\(i\) 和 \(j++\) 的总值就是 \(k++\) 的总值)所以从 \(str\) 到 \(j\) 做出贡献的数有 \(j-k\) 个, \(ans+=j-k\) 。
对于第一和第二种情况,它们在更小的归并操作中已经在第三种情况中被处理,所以不需要考虑
代码
void merge(ll s,ll t)
{
ll mid,i,j,k;
if(s==t) return;
mid = (s + t)>>1;
merge(s,mid);
merge(mid+1,t);
i=s,j=mid+1,k=s;
while(i<=m && j<=t)
{
if(a[i]<=a[j])
{
r[k]=a[i]; i++, k++;
}
else
{
r[k]=a[j];
ans += j-k;//核心代码
j++,k++;
}
}
while(i<=m)
{
r[k]=a[i];i++,k++;
}
while(j<=t)
{
r[k]=a[j];j++,k++;
}
for(int i=s;i<=t;++i) a[i]=r[i];//归并后放回原数组
}
2.树状数组
首先明白一个概念叫做离散化(Discretization)
树状数组中,只需要开一个与原序列中最大元素相等的长度数组就行,那么如果我的序列是1,5,3,8,999,本来5个元素,却需要开到999这么大,造成了巨大的空间浪费,
离散化就是另开一个数组,d, d[i]用来存放第i大的数在原序列的位置,比如原序列a={5,3,4,2,1},第一大就是5,他在a中的位是1,所以d[1]=1,同理d[2]=3,········所以d数组为{1,3,2,4,5}。
这样d中的正序等价于原数组的逆序
来看一下怎么求的:
首先把1放到树状数组t中,此时t只有一个数1,t中比1小的数没有,sum+=0
再把3放到树状数组t中,此时t只有两个数1,3,比3小的数只有一个,sum+=1
把2放到树状数组t中,此时t只有两个数1,2,3,比2小的数只有一个,sum+=1
把4放到树状数组t中,此时t只有两个数1,2,3,4,比4小的数有三个,sum+=3
把5放到树状数组t中,此时t只有两个数1,2,3,4,5,比5小的数有四个,sum+=4
最后算出来,总共有9个逆序对,可以手算一下原序列a,也是9个逆序对.
每次放入一个数的时候,是更新这个数的个数
查到 \(d[i]\) 时,找的就是 \(d[i]\) 前的所有数次数的前缀和,就可以用树状数组维护
#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
#define int long long
using namespace std;
const int maxn=5e5+7;
int lowbit(int x)
{
return x&(-x);
}
int a[maxn],d[maxn],t[maxn],n,ans;
void add(int x)
{
for(x=x;x<=n;x+=lowbit(x))
{
t[x]++;
}
}
int query(int x)
{
int res=0;
for(x=x;x>0;x-=lowbit(x))
{
res+=t[x];
}
return res;
}
bool cmp(int x,int y)
{
if(a[x]==a[y]) return x>y;
return a[x]>a[y];
}
signed main()
{
cin>>n;
for(int i=1;i<=n;++i)
{
cin>>a[i];d[i]=i;
}
sort(d+1,d+1+n,cmp);//离散化重排序
//d中第i个指第i大的数在a中位置
for(int i=1;i<=n;++i)
{
add(d[i]);
ans += query(d[i]-1);
}
cout<<ans;
return 0;
}