逆序对的三种求法(归并排序,树状数组,线段树)

Posted galaxy-yr

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了逆序对的三种求法(归并排序,树状数组,线段树)相关的知识,希望对你有一定的参考价值。

求逆序对个数的三种方法

逆序对: 对于一个序列 $a_1$,$a_2$,$a_3$..$a_n$,如果存在$a_i$>$a_j$且i<j,则$a_i$和$a_j$为一个逆序对。

这里将介绍3种求逆序对对数的方法。
在此之前,默认为你已经会了归并排序,树状数组和线段树。(不会的可以百度学习一下)
学会后可以试着做一下这道题求逆序对

1.归并排序求逆序对

这应该是几乎人人都会的方法,原理是利用归并排序时,对于区间l~r,在mid左边和mid右边都已经是单调的序列来求逆序对,这种方法就不多讲了。时间复杂度约为O(nlogn)
代码如下:个人习惯打快读


#include<iostream>
using namespace std;
const int maxn=1e6+10;
template<typename T>
void read(T&res)
{
        char ch=getchar();
        T q=1;
        while(ch<‘0‘ or ch>‘9‘)
        {
                if(ch==‘-‘)q=-1;
                ch=getchar();
        }
        res=(ch^48);
        while((ch=getchar())>=‘0‘ and ch<=‘9‘)
                res=(res<<1)+(res<<3)+(ch^48);
        res*=q;
        return;
}
int n,a[maxn];
long long ans;
inline void init()
{
        read(n);
        for(int i=1;i<=n;i++)
                read(a[i]);
}
//归并排序
int temp[maxn];
void merge_sort(int l,int r)
{
        if(l==r)return;
        int mid=l+r>>1;
        merge_sort(l,mid);
        merge_sort(mid+1,r);
        int ll=l,st=l,rr=mid+1;
        while(l<=mid and rr<=r)
        {
                if(a[l]<=a[rr])
                        temp[ll++]=a[l++];
                else 
                        temp[ll++]=a[rr++],ans+=mid-l+1;
        }
        while(l<=mid)temp[ll++]=a[l++];
        while(rr<=r)temp[ll++]=a[rr++];
        for(int i=st;i<=r;i++)
                a[i]=temp[i];
        return;
}
int main()
{
    init();
    merge_sort(1,n);
    cout<<ans<<endl;
    return 0;
}

2.树状数组求逆序对

树状数组求逆序对也是一种很好的方法,它在求整个序列的逆序对的同时也可以求出每一个$a_i$的逆序对个数,
并且代码也比较短。 时间复杂度约为 O(nlogn)
具体方法:
我们知道树状数组可以快速的进行区间修改和区间查询,那么我们就要充分利用它的优点。
我的做法是:
用数组tree表示$a_i$的逆序对对数,倒过来扫一遍,边扫边把答案求出,并把比$a_i$大的每一个数字的逆序对对数都添加1。
树状数组我就不压行了
具体做法如下:

#include<iostream>
using namespace std;
const int maxn=1e6+10;
template<typename T>
void read(T&res)
{
        char ch=getchar();
        T q=1;
        while(ch<‘0‘ or ch>‘9‘)
        {
                if(ch==‘-‘)q=-1;
                ch=getchar();
        }
        res=(ch^48);
        while((ch=getchar())>=‘0‘ and ch<=‘9‘)
                res=(res<<1)+(res<<3)+(ch^48);
        res*=q;
        return;
}
int n,a[maxn];
long long ans;
inline void init()
{
        read(n);
        for(int i=1;i<=n;i++)
                read(a[i]);
}
//树状数组
int tree[maxn];
inline int lowbit(int x){return x&-x;}
inline void add(int x,int v) 
{ 
    while(x<=n) 
    { 
        tree[x]+=v;
         x+=lowbit(x);
     } 
     return;
 }
inline int sum(int x)
 {
   int res=0;
   while(x>=1) 
   {
     res+=tree[x]; 
     x-=lowbit(x);
   }
   return res;
}
void solve()
{
        for(int i=n;i>=1;i--)
                ans+=sum(a[i]), add(a[i],1);//可以同时记录下每个数字的逆序对
        cout<<ans<<endl;
}
int main()
{
    init();
    solve();
    return 0;
}

3.权值线段树

我们可以把线段树中的l,r表示为l~r中的每一个数字出现了的总数(l=r时,就是单个数字的个数),在建树时,不添加任何值,做一颗空树,扫一遍的时候添加数字进去。也可以计算出每个数字的逆序对个数
时间复杂度为 O(2nlogn)
具体做法如下:

#include<iostream>
using namespace std;
const int maxn=1e6+10;
template<typename T>
void read(T&res)
{
        char ch=getchar();
        T q=1;
        while(ch<‘0‘ or ch>‘9‘)
        {
                if(ch==‘-‘)q=-1;
                ch=getchar();
        }
        res=(ch^48);
        while((ch=getchar())>=‘0‘ and ch<=‘9‘)
                res=(res<<1)+(res<<3)+(ch^48);
        res*=q;
        return;
}
int n,a[maxn];
long long ans;
inline void init()
{
        read(n);
        for(int i=1;i<=n;i++)
                read(a[i]);
}
//权值线段树
struct{
        int l,r,tot;
}e[maxn*3];
void build(int k,int l,int r)
{
        e[k].l=l,e[k].r=r;
        if(l==r)return;
        int mid=(l+r)>>1;
        build(k<<1,l,mid);
        build(k<<1|1,mid+1,r);
}
void update(int k,int num)
{
        if(e[k].l==num and e[k].r==num)
        {
                e[k].tot++;
                return;
        }
        int mid=(e[k].l+e[k].r)>>1;
        if(num<=mid)update(k<<1,num);
        if(num>mid)update(k<<1|1,num);
        e[k].tot=e[k<<1].tot+e[k<<1|1].tot;
        return;
}
long long sum(int k,int l,int r)
{
        if(e[k].l>r or e[k].r<l)return 0;
        if(e[k].l==l and e[k].r==r)return e[k].tot;
        int mid=e[k].l+e[k].r>>1;
        if(mid>=r)return sum(k<<1,l,r);
        if(mid<l)return sum(k<<1|1,l,r);
        return sum(k<<1,l,mid)+sum(k<<1|1,mid+1,r);
}
void solve()
{
        build(1,1,maxn);
        for(int i=1;i<=n;i++)
        {
                ans+=sum(1,a[i]+1,maxn);
                update(1,a[i]);
        }
        cout<<ans<<endl;
}
int main()
{
    init();
    solve();
    return 0;
}

对于我个人而言还是比较喜欢树状数组的,因为比较短。当然这也只是个人意见。













以上是关于逆序对的三种求法(归并排序,树状数组,线段树)的主要内容,如果未能解决你的问题,请参考以下文章

逆序对

逆序对 线段树&树状数组

逆序对+离散树状数组+。。。。

数状数组求逆序对

模板逆序队(树状数组/归并排序)

P1908 逆序对-(树状数组)