逆序对
百度百科定义:
设 A 为一个有 n 个数字的有序集 (n>1),其中所有数字各不相同。
如果存在正整数 i, j 使得 1 ≤ i < j ≤ n 而且 A[i] > A[j],则 <A[i], A[j]> 这个有序对称为 A 的一个逆序对,也称作逆序数。
逆序对解法
方法一:冒泡排序
对原序列进行冒泡排序,统计交换次数,得到的交换次数=逆序对数。
时间复杂度高,不推荐。
方法二:归并排序
题目链接
讲解
归并讲解
递归这东西,难者不会会者不难,理解之后再写会好写点,但是小细节上还会有点小错。
这不是我主要想讲的内容,所以直接挂链接了。
AC代码
//#include<bits/stdc++.h> #include<iostream> #include<algorithm> #include<cstring> #define ll long long using namespace std; const int N=5e5+10; ll ans=0; int a[N],b[N]; void merge_sort(int l,int r){ int mid=(l+r)/2; int p=l,q=mid+1;//p,q分别是被分割成两段序列中左边序列的头指针和右边序列的头指针 int lb=l;//lb表示b数组的头指针 if(r-l>0){ merge_sort(l,mid); merge_sort(mid+1,r); } while(p<=mid || q<=r){ if(q>r || (p<=mid && a[p]<a[q]))//这两种情况下左指针右移//大佬写的真简洁! b[lb++]=a[p++]; else b[lb++]=a[q++],ans+=mid-p+1; } for(int i=l;i<=r;i++) a[i]=b[i]; } int main(){ int n; while(cin>>n&&n){ for(int i=1;i<=n;i++){ cin>>a[i]; } ans=0; merge_sort(1,n); cout<<ans<<endl; } }
方法三:离散化+树状数组
题目链接
典型例题 (同上)
讲解
看了好多树状数组求逆序对的博客,讲的是真的** ,所以决定写个博客。(如果我讲的也很** ,请喷我,改正!)
离散化实现:
构建一个结构体,id表示输入是默认的序号,v表示数值大小,pos表示离散化后的序号,也就是按照v的大小排完序后的序号(或索引,或位置)
struct node{ int id,v,pos; }a[N];
id和v的赋值可以在输入的时候实现
for(int i=1;i<=n;i++){ cin>>a[i].v; a[i].id=i;//输入时的顺序对应的序号 }
再按照数值从小到大排序,对pos的进行赋值。
离散化的核心,数列中每个数的新值就是排完序的序号(或索引)
sort(a+1,a+n+1,cmp); for(int i=1;i<=n;i++){ a[i].pos=i; }
“假的”求逆序对:
我们先不讲如何用树状数组实现,先通过一种方法来理解。
例如:
5 3 4 1 2
建立一个数组a[6],向数组a中依次插入样例,
第一次插入5,那么就令a[5]=1,代表5插入数组a中了。
为了求逆序对,要计算有没有比5大的数已经插入数组a了,那么我们就要看看a[i],i>5的部分有没有1,有几个1就代表对5而言有多少个比5大的数在5之前,也就是关于5的逆序对;很显然,i>5的部分并没有1。
继续进行,第二次插入3,a[3]=1,还是一样,看看a[i],i>3的部分有没有1,发现a[5]=1,说明5在3之前插入了,因此,关于3的逆序对数为1。
第三次插入4,a[4]=1,a[i],i>4的部分中只有a[5]=1,所以关于4的逆序对数为1。
第四次插入1,a[1]=1,a[i],i>1的部分中a[3]=a[4]=a[5]=1,所以关于1的逆序对数为3。
第五次插入2,a[2]=1,a[i],i>2的部分中a[3]=a[4]=a[5]=1,所以关于2的逆序对数为3。
综上,累加一下,序列的逆序对数为8。
概括一下过程,无非就是:
顺序插入,每插入一个数,累加一下已经插入且比它大的数的个数,就是逆序对数。简单的一批吧!
“真的”求逆序对:
还是上面例子,经过离散化得到:
id: 1 2 3 4 5
v: 5 3 4 1 2
pos:5 3 4 1 2
sort(a+1,a+n+1,cmp1); for(int i=1;i<=n;i++){ update(a[i].pos,1); sum+=i-getsum(a[i].pos); }
sort的目的是要让序列排回原来的默认顺序(刚刚咱不是为了对pos赋值,改变了默认顺序了嘛)
每插入一个数我们就让树状数组update一下,意义与“假的”求逆序对中a[插入数的序号]=1有点类似,只不过在树状数组中,改变一个点,就要把与其有关的点的值都改变(“有关”俩字自行理解,不理解说明不明白树状数组是什么,建议看看 树状数组讲解 )
i-getsum(a[i].pos)
:
getsum(a[i].pos)
的目的是求前缀和,意义与“假的”求逆序对中求比当前插入数小且已经插入的数的个数。i表示已经插入的个数,那么 i-getsum(a[i].pos)
表示的不就是已插入数中比当前插入数大的个数嘛。这样一来,我们每次都进行累加就ok了。
同时,我们还应当注意,getsum与update函数的参数均为pos。类比“假的”求逆序对中a数组体现某个数是否已经插入,是通过以某个数在序列中的排完序后的位置作为索引,赋值为1实现的;同样的,c树状数组体现某个数是否已经插入,也是要通过排完序后的位置作为索引,去更改和统计,而排完序后的位置不正是pos嘛!这就是我们为什么用pos作为参数的原因。
AC代码
//#include<bits/stdc++.h>//poj这个用不了 #include<iostream> #include<algorithm> #include<cstring> #define ll long long using namespace std; const int N=5e5+10; int n,c[N]; struct node{ int id,v,pos; }a[N]; bool cmp(node a,node b){ return a.v<b.v; } bool cmp1(node a,node b){ return a.id<b.id; } int lowbit(int x){ return x&(-x); } void update(int x,int add){ while(x<=n){ c[x]+=add; x+=lowbit(x); } } ll getsum(int x){ ll ans=0; while(x>0){ ans+=c[x]; x-=lowbit(x); } return ans; } int main(){ while(cin>>n&&n){ //init ll sum=0;//最大数据:5e5*5e5/2>2147483647=int,所以开ll memset(c,0,sizeof c); //输入,并对id,v赋值 for(int i=1;i<=n;i++){ cin>>a[i].v; a[i].id=i; } //离散化,本质是对pos赋值 sort(a+1,a+n+1,cmp); for(int i=1;i<=n;i++){ a[i].pos=i; } sort(a+1,a+n+1,cmp1);//恢复默认排序,为以此插入做准备 for(int i=1;i<=n;i++){//依次插入,并统计 update(a[i].pos,1); sum+=i-getsum(a[i].pos); } cout<<sum<<endl; } }