幾種逆序數問題(更新ing)

最近蒟蒻的我在上牛客的算法入門課,遇到了非常經典的逆序數問題,因此想寫篇博客總結一下。

首先看一下逆序數的定義。(摘自百度百科)

在一個排列中,如果一對數的前後位置與大小順序相反,即前面的數大於後面的數,那麼它們就稱爲一個逆序。一個排列中逆序的總數就稱爲這個排列的逆序數。

打個比方,一個序列 (4,5,1,3,2),其中有(4,1)、(4,3)、(4,2)、(5,1)、(5,3)、(5,2)、(3,2)七個逆序對,所以這個序列的逆序數爲 7 。(概念還是很容易理解的)

瞭解完基本概念後,我們來看一個經典問題,就是求解一個序列的逆序數。

題目一:逆序數
題目鏈接:https://ac.nowcoder.com/acm/problem/15163
問題規模是 1 <= n <= 100000,所以暴力是不行的。
這裏提供兩種做法。

做法一:歸併排序
歸併排序每一次遞歸回溯前都有一次合併操作,即將兩個有序的序列合併成一個有序的序列,這樣在每次比較兩個序列的元素時,如果第二個序列的元素小於第一個序列的元素了,那麼此時第二個序列的這個元素肯定小於第一個序列的所以未比較元素(因爲已經排好序了)。所以現在就有了第一個序列未比較元素數+1的逆序隊(別忘了剛比較的第一序列元素也比它大),更新答案,並且每次歸併都把逆序對消除了。
合併代碼段

void merge(int l,int r,int *c)//合併
{
    int mid=(l+r)>>1;
    int i=l,j=mid+1,cnt=l;
    while(i<=mid&&j<=r)
    {
        //小的放
        if(c[i]>c[j])
        {
            b[cnt++]=c[j++];
            ans+=mid-i+1;//更新答案
        }
        else
        b[cnt++]=c[i++];
    }
    //把沒比較的直接放
    while(i<=mid)
    b[cnt++]=c[i++];
    while(j<=r)
    b[cnt++]=c[j++];
    for(int k=l;k<=r;k++)//更新a數組
    c[k]=b[k];
}

這樣說可能比較抽象,我們來舉個例子,有兩個已排好序的序列,a1(1,5,7),a2(2,4,6),i指向第一列起始位置,j指向第二列起始位置,ans爲答案,ans每次更新ans+(3-i+1)。
第一次
a1[1]<a2[1],i++。
第二次
a1[2]>a2[1],產生逆序對,ans+(3-2+1)(5和7都比3大),j++。
第三次
a1[2]>a2[2],產生逆序對,ans+(3-2+1) (5和7都比4大),j++。
第四次
a1[2]<a2[3],i++。
第五次
a1[3]>a2[3],產生逆序對,ans+(3-3+1) (7比6大),j++。
此時比較完畢了,根據歸併排序,只需把沒比較的元素全部放進輔助數組即可,此時不會產生逆序對(不熟悉過程的再去看看歸併排序)。
所以此次合併一共產生5組逆序對。

下附完整代碼

#include <iostream>
#include <stdio.h>
using namespace std;
const int maxn=1e5+5;
int a[maxn];//原數組
int b[maxn];//輔助數組
long long ans;
void merge(int l,int r,int *c)//合併
{
    int mid=(l+r)>>1;
    int i=l,j=mid+1,cnt=l;
    while(i<=mid&&j<=r)
    {
        //小的放
        if(c[i]>c[j])
        {
            b[cnt++]=c[j++];
            ans+=mid-i+1;
        }
        else
        b[cnt++]=c[i++];
    }
    //把沒比較的直接放
    while(i<=mid)
    b[cnt++]=c[i++];
    while(j<=r)
    b[cnt++]=c[j++];
    for(int k=l;k<=r;k++)//更新a數組
    c[k]=b[k];
}
void mergesort(int l,int r,int *c)
{
    if(l==r)
    return;
    int mid=(l+r)>>1;
    mergesort(l,mid,c);
    mergesort(mid+1,r,c);
    merge(l,r,c);
}
int main()
{
    int n;
    scanf("%d",&n);
    for(int i=1;i<=n;i++)
    scanf("%d",&a[i]);
    mergesort(1,n,a);
    printf("%lld\n",ans);
    return 0;
}

做法二:樹狀數組
相比於利用歸併排序,樹狀數組解決這個問題就很容易理解了,這裏數據 (1 <= n <= 100000) (0 <= a[i] <= 100000),a[i]大小和n一樣並且才1e5,所以不需要離散化。我們在線處理即可。
樹狀數組維護小於等於這個數的個數。我們遇到一個數,查它之前有多少個數大於它,那麼就產生了多少個逆序對。比如此時遍歷到序列第 i 個元素,query(a[i]),查一下小於等於這個數的個數,那麼 i-1-query(a[i])就是大於這個數的個數,即加入這個數產生的逆序對個數。然後再把這個數加進去,更新數組數組,即add(a[i])。
總的來說就是暴力的思路,用數狀數組優化一下,感覺自己說了好多廢話
下附完整代碼

#include <iostream>
#include <stdio.h>
using namespace std;
const int maxn=1e5+5;
int tree[maxn];
int lowbit(int i)
{
    return i&(-i);
}
void add(int x)
{
    while(x<=100001)
    {
        tree[x]++;
        x+=lowbit(x);
    }
}
int query(int x)
{
    int res=0;
    while(x>0)
    {
        res+=tree[x];
        x-=lowbit(x);
    }
    return res;
}
int main()
{
    int n,x;
    scanf("%d",&n);
    long long ans=0;
    for(int i=1;i<=n;i++)
    {
        scanf("%d",&x);
        ans+=(long long)(i-1-query(x+1));
        add(x+1);//數狀數組不能從0開始,所以要+1
    }
    printf("%lld\n",ans);
    return 0;
}

補充:如果a[i]可以很大,那麼就必須離散化處理後,離線樹狀數組解決這個問題。
這題a[i]有1e9。。。
解法和上面一樣,就是多了離散化。
下附完整代碼

#include <bits/stdc++.h>
using namespace std;
const int MAXN=5e5+10;
int n;
int tree[MAXN],a[MAXN],b[MAXN];
int lowbit(int t)
{
return t&-t;
}
void update(int i,int v)
{
while(i<=n)
{
tree[i]+=v;
i+=lowbit(i);
}
}
int sum(int i)
{
int res=0;
while(i>0)
{
res+=tree[i];
i-=lowbit(i);
}
return res;
}
int main()
{
    scanf("%d",&n);
    for(int i=1;i<=n;i++)
    {
    scanf("%d",&a[i]);
    b[i]=a[i];
    }
    sort(b+1,b+1+n);
    int m=unique(b+1,b+1+n)-b-1;
    for(int i=1;i<=n;i++)
    a[i]=lower_bound(b+1,b+1+m,a[i])-b;
    long long s=0;
    for(int i=1;i<=n;i++)
    {
    s+=sum(n)-sum(a[i]);
    update(a[i],1);
    }
    printf("%lld\n",s);
    return 0;
}

題目二:洛谷P1521 求逆序對

題目鏈接:https://www.luogu.com.cn/problem/solution/P1521

題目大意:
整數1~n的特定排列,問逆序數爲k的特定排列有多少種。

解題思路:
動態規劃,dp[i][j]表示用1~i的數組成逆序數爲j的特定排列有多少種。
接下來我們推轉移方程。假設現在求dp[i][j]。就是原先1~i-1的序列,多個數字i。
一步一步來,先把 i 放到最後,即 #####i,那麼此時並不會產生新的逆序對,那麼dp[i][j]+dp[i-1][j]
再把 i 放到倒數第二個位置,即 ####i#,此時產生了一個新的逆序對,那麼dp[i][j]+dp[i-1][j-1]
再把 i 放到倒數第三個位置,即 ###i##,此時產生了兩個新的逆序對,那麼dp[i][j]+dp[i-1][j-2]

最後把 i 放到第一個位置,即 i#####,此時產生了i-1個新的逆序對,那麼dp[i][j]+dp[i-1][j-i+1]
由此我們能推出轉移方程
答案就是dp[n][k]。

下附完整代碼

#include <iostream>
#include <stdio.h>
using namespace std;
const int maxn=1e4+5;
const int mod=1e4;
int dp[101][maxn];//dp[i][j]表示用1~i組成的排列,有j個逆序對的方案有多少種
int main()
{
    int n,k;
    scanf("%d %d",&n,&k);
    dp[1][0]=1;
    for(int i=2;i<=n;i++)
    for(int j=0;j<=k;j++)
    for(int p=max(0,j-i+1);p<=j;p++)
    dp[i][j]=(dp[i][j]+dp[i-1][p])%mod;
    printf("%d\n",dp[n][k]);
    return 0;
}

持續更新。。。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章