「HAOI 2006」数字序列
Description
给定一长度为 \(n\) 的数列 \(a\),可将 \(a_i\) 改为任意整数 \(k\),代价为 \(\mid a_i-k\mid\)。
问最少改变多少个数能把它变成一个单调严格上升的序列。
输出最少需要改变的数的个数,以及在改变的数最少的情况下,最小的代价和。
\(1\leq n\leq 3.5\times 10^4,1\leq a_i\leq 10^5\)。
Solution
Part 1
Solve Problem 1:需要改变的数最少,则需保留的数要尽可能多。考虑取一个补集,问题转化为求最多保留多少个数。
对于两个数 \(a_i,a_j\)(不妨设 \(i<j\)),若可同时保留 \(i\) 和 \(j\),则 \(a_i,a_j\) 需满足:
-
\(a_i<a_j\)(显然)。
-
改变 \([i+1,j-1]\) 内的数能够使 \([i,j]\) 严格单调上升。所以 \(a_j-a_i\geq j-i\)。移项可得,\(a_i-i\leq a_j-j\)。
构造数列 \(b_i=a_i-i\),问题转化为求 \(b\) 的最长不降子序列。可 \(O(n\log n)\) 求得。
Part 2
Solve Problem 2:使 \(a\) 单调上升的代价,就是使 \(b\) 单调不降的代价。
考虑在 \(b\) 的最长不降子序列中,任意两个相邻的元素。设它们 \(b\) 中的位置分别为 \(l,r\),则一定 不存在 \(i\in[l,r]\),使得 \(b_l\leq b_i\leq b_r\)。否则取上 \(i\),保证合法,而且可以使最长不降子序列更长。
所以对于 \(\forall i\in[l,r]\),\(b_i<b_l\) 或 \(b_i>b_r\)。
考虑如何改变 \(b_i\) 的值,能使序列合法且代价和最小。
结论:存在一个数 \(k\in [l,r]\):对于 \(\forall i\in[l,k]\),把 \(b_i\) 改成 \(b_l\)。对于 \(\forall i\in[k+1,r]\),把 \(b_i\) 改成 \(b_r\)。此时代价和最小。
假设 \([l,r]\) 之间有 \(n\) 个数。
-
当 \(n=1\) 时,结论显然成立。(因为 \(b_i<b_l\) 或 \(b_i>b_r\),\(b_i\) 改为 \(b_l\) 或 \(b_r\) 显然比改为取值在 \([b_l,b_r]\) 之间的数优)
-
当 \(n>1\) 时:
-
前 \(n-1\) 个数一半改为 \(b_l\) 一半改为 \(b_r\):当 \(b_n>b_r\) 时,显然将 \(b_n\) 改为 \(b_r\) 比较优。当 \(b_n<b_l\) 时,若 \(b_n\) 不改为 \(b_r\) 改为了 \(b_l+k\)(\(0\leq k\leq b_r-b_l\)),为了使序列单调不降,前面所有改为 \(b_r\) 的数都应改成 \(b_l+k\)(设这样的数有 \(x\) 个)。\(x\times (b_r-(b_l+k))+((b_l+k)-b_n)=xb_r-(x-1)(b_l+k)-b_n\geq b_r-b_n\),所以此时 \(b_n\) 改为 \(b_r\) 更优。
-
前 \(n-1\) 个数全改为 \(b_l\) 或 \(b_r\):略。
-
Part 3
令 \({dp}_i\) 表示最后一位是 \(b_i\) 时单调不降的最小代价。
枚举 \(j\),枚举的 \(j\) 需满足:
-
\(j<i,b_j<b_i\)。
-
以 \(b_j\) 结尾的最长不降子序列长度 \(=\) 以 \(b_i\) 结尾的 \(-1\)。
枚举分界点 \(k\),有:
\(\displaystyle{dp}_i=\min\{{dp}_j+\sum\limits_{p=j+1}^k\mid b_p-b_j\mid+\sum\limits_{p=k+1}^{i-1}\mid b_p-b_i\mid\}\)
即:对于 \(p\in[j+1,k]\),将 \(b_p\) 改为 \(b_j\)。对于 \(p\in[k+1,i-1]\),将 \(b_p\) 改为 \(b_i\)。
前缀和优化转移即可。
Code
#include<bits/stdc++.h> #define int long long using namespace std; const int N=4e4+5; int n,a[N],b[N],len,f[N],g[N],dp[N],pre[N],suf[N]; vector<int>v[N]; //v[i]: 记录长度为 i 的最长不降子序列的结尾 void solve(int l,int r,int L,int R){ pre[l]=suf[r+1]=0; for(int i=l+1;i<=r;i++) //前缀和 pre[i]=pre[i-1]+abs(b[i]-L); for(int i=r;i>=l+1;i--) //后缀和 suf[i]=suf[i+1]+abs(b[i]-R); } signed main(){ scanf("%lld",&n); for(int i=1;i<=n;i++) scanf("%lld",&a[i]),b[i]=a[i]-i; b[0]=-1e9,b[++n]=1e9; //边界。加上前后最小值最大值方便操作。最大值不要设太大,不然算前缀和的时候可能会爆。 f[1]=b[1],len=1,g[1]=1,v[1].push_back(1); for(int i=2;i<=n;i++){ //O(n log n) 求最长不降子序列 if(b[i]>=f[len]) f[++len]=b[i],g[i]=len; else{ int x=upper_bound(f+1,f+1+len,b[i])-f; f[x]=b[i],g[i]=x; //g[i]: 以第 i 个数结尾的最长不降子序列的长度 } } for(int i=0;i<=n;i++) v[g[i]].push_back(i); memset(dp,0x3f,sizeof(dp)),dp[0]=0; for(int i=1;i<=n;i++) for(int p=0;p<(int)v[g[i]-1].size();p++){ //如果 b[j] 要拼上前面合适的 b[i],就去前面找长度为 g[i]-1 且能拼上的 int j=v[g[i]-1][p]; //以 b[j] 结尾的 最长不降子序列长度 = 以 b[i] 结尾的 -1 if(j>i||b[j]>b[i]) continue; //j<i,b[j]<=b[i] 才行 solve(j,i,b[j],b[i]); for(int k=j;k<i;k++) //枚举分界点 k dp[i]=min(dp[i],dp[j]+pre[k]+suf[k+1]); } printf("%lld\n%lld\n",n-len,dp[n]); return 0; }