題目連接: 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本來的貢獻中:算法
- \(seq_1\)裏比\(a\)大的,保留了下來。
- \(seq_2\)裏比\(a\)小的,一定也比\(b\)小。
- \(seq_3\)裏比\(a\)小的,保留了下來。
一樣地,在b本來的貢獻中:數組
- \(seq_1\)裏比\(b\)大的,保留了下來。
- \(seq_2\)裏比\(b\)大的,一定也比\(a\)大。
- \(seq_3\)裏比\(b\)大的,保留了下來。
所以,用非降序的序列來填空,至少不會比這個序列的其餘排列方式差,也就能夠認爲這是最優的了。優化
這裏稍微提一下,若是用類似的方法,沒法證實非升序是最優的。spa
第二步,明確算法過程須要什麼數據。這裏須要用到的有:code
- 原序列的:
- 對於全部的數字1-k,每一個位置與前面可組成的逆序對數。
- 已產生的全部逆序對數。
- 填入空位的數:
- 可與原序列產生的逆序對數
- 填入序列之間產生的逆序對數。(大概是一個等差數列求和再減去相同部分)
而後是核心部分。觀察到k很小,因此咱們枚舉從左到右,從大到小填入序列。具體方法以下:ci
- 枚舉從\(k到1\)的每一個數字\(val\)。而後枚舉任意連續的空位填入一串val。這樣直接作的複雜度是\(O(n^2)\),因此須要一些優化。
- 首先用前綴和、差分把計算連續串的複雜度壓到\(O(n)\)創建,\(O(1)\)查詢。
- 而後再額外維護一個dp數組,記錄對於這個val的狀況,這樣在取到更優值的時候,這個首部之前的地方就能夠保證後續也可用,不然,按照前面的推導,這個地方若是不能放入val,那後面也不能放了。這樣首部隨着枚舉尾部向後遷移,更新dp數組的複雜度也從\(O(n^2)\)降到了\(O(n)\)。
- 最後再用這個額外的dp數組去更新答案的dp數組,直接覆蓋。算法執行完,就能夠獲得填空與原序列產生的總貢獻,和填空序列裏不變的子序列減小的貢獻。
- 最終答案就是原序列的貢獻+填空與原序列產生的貢獻+填空之間產生的貢獻(等差數列求和再減去不變的子序列減小的貢獻,代碼中爲方便計算,減去的部分在填空與原序列產生的貢獻中提早計算了)。
這裏由於題目比較複雜的緣由,很難只靠文字講清楚。具體須要看代碼裏的一些註釋輔助理解。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; }