求逆序对常用的两种算法 ----归并排 & 树状数组

时间:2021-08-21 14:18:57

网上看了一些归并排求逆序对的文章,又看了一些树状数组的,觉得自己也写一篇试试看吧,然后本文大体也就讲个思路(没有例题),但是还是会有个程序框架的
好了下面是正文


  1. 归并排求逆序对
  2. 树状数组求逆序对

一、归并排求逆序对

温馨提示:阅读这段内容需要的知识点:归并排序

— 首先的话,归并排序大家应该都知道的吧?归并排是利用分治的思想,先分后和,分到左右区间相等或相交时在返回上一层进行两个有序小数组交错插入排序,形成一个有序数组,然后层层返回排好序的数组,作为新的小数组插入大数组排序,这就是一个n log n的排序算法(带 log 的算法一般都算是比较快的,只要常数不过大)。然后还是不懂的同学可以百度,这里不细讲了。另外提一提,实在是不会用归并排的话冒泡也是一样可以求逆序对的,累加的话就变成了判断到需要交换时进行,但冒泡的复杂度高了点,是 n^2 了,提交 后会爆几个点就不知道了,得看具体题目和数据。(反正数据一般不会水到让你满分【斜眼笑ing】)

— 其次的话,用归并排求逆序对无非也就是在插入的过程中将 逆序数 ans 累加,然后也没什么不同的了,只要记得归并排模板的话基本也是码的出来的。(个人感觉归并排求逆序队还是挺清晰的,因为这样基本就是套套模板不用想太多)

模板如下,但请别直接复制粘贴,好歹自己打一遍


int n,ans;
const int mod=99999997;
int f[100005],g[100005]; void merge_sort(int l,int r)
{
if(l>=r) //如果说l、r交错的话直接return不管
return ; int mid=(l+r)>>1; //以l、r的中点为界向下分支排序
merge_sort(l,mid);
merge_sort(mid+1,r); int i=l,j=mid+1,k=l;
while(i<=mid && j<=r) //保证两个小的数组不超边界
{
if(f[i]<f[j])
g[k++]=f[i++];
else
{ //大概要在模板上做修改的就是这块了,用ans把逆序对累加
ans=(ans+mid-i+1)%mod; //如果题目中有取余就%mod
g[k++]=f[j++];
}
}
// 然后把剩下的数直接插入到大的数组末尾(但不会对ans进行累加操作)
while(i<=mid)
g[k++]=f[i++];
while(j<=r)
g[k++]=f[j++];
for(int i=l;i<=r;++i) //g数组只是一个中间量,用完就丢了,f才是要排序的数组
f[i]=g[i]; }

二次分析

–然后我觉得还得解释一下为什么ans在j数组(即第二个小数组)中的值插入到达数组的时候才累加。试想,逆序对就是大的数字在前面,小的数字在后面,每次发现一组这样的数字对那么整个数组中的逆序对数量就可以+1了。
如:1 2 6 8 和 3 5 7 9 ,初始i指向1,j指向3,k指向8,l指向9,ans=0
在第一次比较时,1<3,则1插入进大数组,i++,ans不变
第二次比较式,i指向了2, 2<3,则2插入进大数组,还是i++,ans不变
第三次,i指向6,6>3,3入大数组,j++,ans+=2 。
这里就是重点了,3小于6,则3也一定小于6后面的数,并且可以和这些数(共两个)分别对应形成n个逆序对(n为k-i+1,即6和6的后面总共还剩多少个数)
第四次也一样,是j++,ans+=2,此时ans为4,原理同上,不再解释
然后就是继续向大数组队尾插入数了,我们发现直到 i 数组为空时(全被插入完毕了),j 数组仍有剩余,那么就将 j 数组直接插入进大数组,但ans不进行累加(因为此时 i 数组空了,无法与 j 数组中剩下的数形成逆序对)

呼~这样总该解释的差不多了,同志们自个儿好好消化消化吧。

二、树状数组求逆序对

温馨提示:阅读本段需要具备的知识点:树状数组的基本操作(update、getsum、lowbit之类的)

–首先的话,树状数组我也不来说这么详细了,许多细节方面(如 getsum 时x为什么要减去一个lowbit(x)了之类的)的理解就麻烦请自己思考得出或是去问百度了。

–其次的话,树状数组其实就是代码短一点(短一点就好码一点,好码一点就好调试一点,好调试一点就不容易出错一点),看着舒服吧。
然后我就不啰嗦了,直接上代码吧。

int lowbit(int x)       //lowbit求最末尾的1所在的位置
{
return x&(-x);
} void update(int x,int k) //update等会儿讲
{
for(;x<=n;x+=lowbit(x))
g[x]+=k;
} long long getsum(int x) //getsum的话。。。也等会儿讲
{
long long res=0;
for(;x;x-=lowbit(x)) //一直跳向比x小的数,如7->6->4->0(结束)
//或是6->4->0(结束)
res+=g[x];
return res;
} void BIT() //这个BIT啊,我看书的时候也不知道是什么鬼,
//然后才发现原来是树状数组英文名(Binary Indexed Trees)的缩写
{
for(int i=1;i<=n;++i)
{
update(f[i],1);
ans=(ans+i-getsum(f[i]))%mod;
}
}

另外提一点,不要看着这个代码好像行数很多,码一遍之后会发现真的很短

然后讲讲update和getsum吧(主要是给学过的人讲,谈谈我的理解)

上图!

求逆序对常用的两种算法   ----归并排 & 树状数组
在这里的话,你可以认为每个三角形的顶端都是一个BOSS,一旦他们的下属出现了之后,下属会先+1,再逐级向上汇报(也就是说有小三角形的话就先向小三角形上的BOSS先汇报,然后再由这个小的BOSS向更大的BOSS汇报,直到顶层), 这样的话我们最后就可以清晰地得到一个实时更新的树状数组,每个g中所存的就是它以及它的下属目前已出现的个数

求逆序对常用的两种算法   ----归并排 & 树状数组

这里的话 6 在getsum的时候路径为6->4->0(结束),得到的 res 为 1,即已出现的数字中,小于等于6的数字只有一个

求逆序对常用的两种算法   ----归并排 & 树状数组

那么上面演示的是有逆序对的情况,同样的,你也可以自行演示一下 先 2 后 6的情况,这时候你会发现 ans 并没有累加,即没有逆序对的情况……
总之,还是要熟知树状数组操作里的含义吧。

好了,心血来潮写的一篇博客终于搞定了。(大概花了两个多小时的样子,是不是蒟蒻?)
然后如果说有哪里我讲的不对的话,欢迎各位 神(da)犇(lao)在评论区里喷我。_ (:зゝ∠) _
bye bye(下次见)!
1000010 1011001 1000101————!(一串ASCII码)