2018 ICPC Pacific Northwest Regional Contest I-Inversions 題解

題目連接: 2018 ICPC Pacific Northwest Regional Contest - I-Inversionsios

題意

給出一個長度爲\(n\)的序列,其中的數字介於0-k之間,爲0表示這個位置是空的。如今能夠在這些空的位置上任意填入1-k之間的數字(能夠重複)。問最多能夠總共有多少對逆序對。(若是\(i<j,p_i>p_j\),則稱\((i,j)\)是一對逆序對)
\(1\leq n\leq 2*10^5,\ 1\leq k\leq 100\)c++


思路

  • 第一步,先證實最優的填入的序列必定是非降序的。這裏能夠用反證法。假設\(a\lt b\),對於序列\(seq_1,a,seq_2,b,seq_3\),這裏seq表示一段數字。若是咱們交換a和b的位置,能夠發現,a本來的貢獻中:算法

    1. \(seq_1\)裏比\(a\)大的,保留了下來。
    2. \(seq_2\)裏比\(a\)小的,一定也比\(b\)小。
    3. \(seq_3\)裏比\(a\)小的,保留了下來。

    一樣地,在b本來的貢獻中:數組

    1. \(seq_1\)裏比\(b\)大的,保留了下來。
    2. \(seq_2\)裏比\(b\)大的,一定也比\(a\)大。
    3. \(seq_3\)裏比\(b\)大的,保留了下來。

    所以,用非降序的序列來填空,至少不會比這個序列的其餘排列方式差,也就能夠認爲這是最優的了。優化

    這裏稍微提一下,若是用類似的方法,沒法證實非升序是最優的。spa

  • 第二步,明確算法過程須要什麼數據。這裏須要用到的有:code

  1. 原序列的:
    • 對於全部的數字1-k,每一個位置與前面可組成的逆序對數。
    • 已產生的全部逆序對數。
  2. 填入空位的數:
    • 可與原序列產生的逆序對數
    • 填入序列之間產生的逆序對數。(大概是一個等差數列求和再減去相同部分)
  • 而後是核心部分。觀察到k很小,因此咱們枚舉從左到右,從大到小填入序列。具體方法以下:ci

    1. 枚舉從\(k到1\)的每一個數字\(val\)。而後枚舉任意連續的空位填入一串val。這樣直接作的複雜度是\(O(n^2)\),因此須要一些優化。
    2. 首先用前綴和、差分把計算連續串的複雜度壓到\(O(n)\)創建,\(O(1)\)查詢。
    3. 而後再額外維護一個dp數組,記錄對於這個val的狀況,這樣在取到更優值的時候,這個首部之前的地方就能夠保證後續也可用,不然,按照前面的推導,這個地方若是不能放入val,那後面也不能放了。這樣首部隨着枚舉尾部向後遷移,更新dp數組的複雜度也從\(O(n^2)\)降到了\(O(n)\)
    4. 最後再用這個額外的dp數組去更新答案的dp數組,直接覆蓋。算法執行完,就能夠獲得填空與原序列產生的總貢獻,和填空序列裏不變的子序列減小的貢獻。
    5. 最終答案就是原序列的貢獻+填空與原序列產生的貢獻+填空之間產生的貢獻(等差數列求和再減去不變的子序列減小的貢獻,代碼中爲方便計算,減去的部分在填空與原序列產生的貢獻中提早計算了)。

    這裏由於題目比較複雜的緣由,很難只靠文字講清楚。具體須要看代碼裏的一些註釋輔助理解。get


代碼

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int maxn=2e5+10;
const int maxk=1e2+10;
const ll inf=1e18;
int n,k;
int seq[maxn];
int preGt[maxn][maxk];//i,j: 下標從1到i這麼多個大於j的數
int cnt[maxk];//至今有多少個數大於i
ll dp[maxn];//在i以前填空的貢獻,減去相等序列本應有的貢獻
ll curdp[maxn];
ll presum[maxn];//前綴和
ll getsum(ll num){
    //1...n等差數列求和
    return num*(num+1)/2;
}
int main(){
    // freopen("in.txt","r",stdin);
    ios::sync_with_stdio(false);
    cin>>n>>k;
    int empty=0;
    ll ori=0;//原序列的逆序對數
    for(int i=1;i<=n;++i){
        cin>>seq[i];
        if(seq[i]==0){
            ++empty;
        }
        else{
            for(int j=0;j<seq[i];++j)
                ++cnt[j];
            ori+=cnt[seq[i]];
        }
        for(int j=0;j<=k;++j)
            preGt[i][j]=cnt[j];
    }
//////////////////////
    for(int i=1;i<=n;++i)
        dp[i]=-inf;
    for(int val=k;val>=1;--val){
        //先用大的數填空
        int tot=0;
        ll sum=0;//要注意的是,這裏的sum是前綴和
        for(int i=1;i<=n;++i){
            if(seq[i]) continue;

            //加上i左邊比val大的數
            sum+=preGt[i][val];
            //再加上i右邊比val小的數,這裏用總數-比val-1大的數量=比val小的數量
            //而後再總的減掉左邊的,就是右邊的。
            sum+=(preGt[n][0]-preGt[n][val-1])-(preGt[i][0]-preGt[i][val-1]);
            presum[++tot]=sum;
        }
        int emptyR=1;
        int emptyL=1;
        int curL=1;
        int curst=1;
        //枚舉這串val填空的尾部
        for(int ed=1;ed<=n;++ed){
            ll mx=-inf;
            if(seq[ed]) continue;
            curL=emptyL;
            for(int st=curst;st<=ed;++st){
                if(seq[st]) continue;
                //從上一個狀態加上,L到R之間填空val的貢獻,減去這串本該降低的空位產生的貢獻。
                //例如2,1貢獻了一個逆序對,但2,2就不貢獻了。而後對比已有狀態看是否更優。
                ll tmp=dp[curL-1]+(presum[emptyR]-presum[curL-1])-getsum(emptyR-curL);
                if(tmp<mx) break;
                if(tmp>mx){
                    mx=tmp;
                    emptyL=curL;
                    curst=st;
                }
                ++curL;
            }
            curdp[emptyR]=max(dp[emptyR],mx);
            ++emptyR;
        }
        swap(dp,curdp);
    }
    cout<<ori+dp[empty]+getsum(empty-1)<<'\n';
    return 0;
}
相關文章
相關標籤/搜索