题目连接: 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; }