1. 程式人生 > 實用技巧 >區間翻轉 歸併排序 + 逆序對

區間翻轉 歸併排序 + 逆序對

區間翻轉

這個題目如果不知道怎麼用歸併排序求逆序對還是很難想的,但是知道用歸併排序求逆序對也不是一個很好寫的題目。

注意題目是每一個數只出現一次!!!

思路:

整體求考慮每一次操作的影響。

因為每次操作都是2的冪次,那麼我們先求出劃分成\(2^x\) 塊的每一個塊內的逆序對之和,這個可以用歸併排序做,所以假設 \(dp[x]\) 表示每一個塊大小是 \(2^x\) 次方的所有塊逆序對之和(注意這個定義是大小是\(2^x\) 塊),那麼容易知道,一個塊的逆序對+順序對=\(len*(len-1)/2\) 這個 \(len\) 表示塊的大小。

接下來考慮如果要求劃分成 $2^q $ 塊,那麼每塊大小就是 \(2^x = 2^n - 2^q\)

,因為每塊劃分大小是 \(2^x\) 這樣對更大的塊和更小的塊的影響是不同的,對比這個塊更小的塊的影響就是:順序對變成逆序對,逆序對變成順序對,對比這個塊更大的塊的影響是:變化的塊順序對逆序對交換,但是塊與塊直接的順逆序對數量不變。

最後得出來的式子就是:\(f[i]\) 表示 \(2^i\) \(temp[i]\) 表示這次操作未更新之前的 \(dp[i]\)

\(i<=x\)\(dp[i] = f[i]*(f[i]-1)/2*f[n-i]-dp[i]\)

\(i>x\)\(dp[i] = dp[i]-temp[i-1]+dp[i-1]\)

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int maxn = 3e6+10;
ll a[maxn],temp[maxn],dp[40],f[40];
ll merge(int pos,int l,int r){
    if(pos==0){ dp[pos] = 0;return 0;}
    ll ans = 0;
    int mid = (l+r)>>1;
    ans += merge(pos-1,l,mid);
    ans += merge(pos-1,mid+1,r);
    int now = 0,x = l,y = mid + 1;
    while(x<=mid||y<=r){
        if(x<=mid&&y<=r) {
            if(a[x]<=a[y]) temp[++now]=a[x],x++;
            else temp[++now]=a[y],y++,ans+=mid-x+1;
        }
        else if(x<=mid) temp[++now] = a[x],x++;
        else if(y<=r) temp[++now] = a[y],y++;
    }
    dp[pos] += ans;
    for(int i=l,j=1;i<=r&&j<=now;i++,j++) a[i] = temp[j];
    return ans;
}

void init(){
    f[0] = 1;
    for(int i=1;i<=22;i++) f[i]=f[i-1]*2;
}
int main(){
    init();
    int n,m;
    scanf("%d%d",&n,&m);
    int len = 1<<n;
    for(int i=1;i<=len;i++) scanf("%lld",&a[i]);
    merge(n,1,len);
    while(m--){
        int x;
        scanf("%d",&x);
        x = n-x;
        for(int i=0;i<=n;i++) temp[i] = dp[i];
        for(int i=0;i<=x;i++) dp[i] = f[n-i]*f[i]*(f[i]-1)/2-dp[i];
        for(int i=x+1;i<=n;i++) dp[i] = dp[i] - temp[i-1] + dp[i-1];
        printf("%lld\n", dp[n]);
    }
    return 0;
}