究极精神污染题(可能是我根本抵挡不住这类 DP 吧),从下午调到晚上。绿题恶评的有点严重了啊,感觉紫题都不为过(这不要比某个 agc007D 复杂多了?)。(第一次给绿题写题解?)
首先指出题目中一处错误:(|k-i|<|k-j|) 应该为 (|k-i|<|i-j|)(毕竟总有 (|k-j|=1)。。。)。意思就是:我一旦经过 (i) 见死不救,等再往 (i) 方向走一步的时候,就必须沿直线直接走向 (i) 并治愈之。我们考虑演绎一下方案:先从 (i) 开始一直不掉头往前走,中途可能治愈部分节点。直到我想掉头的时候,我必须要回到第一个没被治愈的节点,并把沿途所有点全部治愈。然后呢?然后只能回到掉头处继续往前走,此时变成了一个后缀的子问题,容易想到以掉头再回来的区间作为阶段进行 DP。
我们考虑对 (1sim n) 来决策,考虑找到最右边一个阶段,这样转移到的是一个前缀以及一个单独的阶段。这样一直剥最右边的阶段,转移到的永远是 (1sim n) 的一个前缀,于是我们设 (f_i) 表示考虑到 (i) 并且一个完整的阶段恰好结束的最小贡献(至于贡献是啥稍后再谈)。这样只需要直接枚举最右边阶段的断点即可平方转移,同时还要预处理每个区间作为一整个阶段的最小贡献。而我一开始想的是转移枚举断点分成前后缀子问题(确实是降智了吧。。。),这样需要区间 DP(显然严格烦于上述找最右边断点的方法),无论如何感觉也无法小于三方。
现在考虑具体化这个 DP 状态。先明确阶段是什么。我们注意到如果阶段第一个位置被一开始就治愈的话,这样我们无法确定最终要回到哪个位置;而如果没被治愈,最终肯定是要回到开头。而对于第一个位置被治愈的阶段,我们完全可以把第一个位置剥掉,成为一个独立的阶段,这很合理对否。于是我们强制规定:一个阶段是一段区间,它的开头不选,结尾一定选(因为是掉头位置)。然后考虑 DP 状态:注意到除了最后一个阶段,每个阶段在回到开头后都要回到结尾;而最后一段强行令它回到结尾也没有任何关系,因为那时候所有人都被治愈了,不会有人死了,不影响答案。于是可以设 (f_i) 为将 (1sim i) 值域好并停在 (i) 的最小贡献。然后贡献是什么?完全将 (1sim i) 当作子问题?这时候你会发现,在转移的时候,枚举的最右边阶段不仅有其内部的贡献,还要加上之前墨迹的次数乘以该阶段的和,而我们根本没记录之前墨迹的次数!!对于这类后效性,我们有一个 trick 叫做费用提前计算,就是说不将以后转移时需要用到前面的信息记录下来而在后面统一贡献上,而在前面 DP 时就把后面需要用到当前信息的贡献提前给加上,这样后面就不需要担心了,相当于一个在 DP 中应用的换贡献体。对于这题来说,应用费用提前计算后的结果比较容易理解:其实就是每过一秒钟,把全局未治愈的人数贡献上。
设 (dp_{l,r}) 表示区间 (l,r) 作为一个单独的阶段的最小贡献(这里指的是内部贡献,稍后将会看到这不导致后效性,所以不需要提前计算全局贡献)。那么 (f_i) 转移就枚举最右边阶段 ([j,i]),之前走了 (f_{j-1})(这其中也包含了 ([j,n]) 的贡献),然后往右走一步到达 (j)(这一步要累计贡献哦!并且当 (j=1) 时不存在这一步),然后将 (dp_{j,i}) 累上去,然而这是内部的,并不包括 ([i+1,n]) 的贡献。考虑加上,此时发现在阶段 ([j,i]) 内墨迹的次数是固定的(这也是为什么没有后效性)!容易发现走路一定是从左往右再从右往左,然后还要回右边(这一部分对 (dp_{j,i}) 内部已经没有贡献了,因为已经治愈完了);而治愈顺序只是被打乱而已,总是要治愈所有节点。所以贡献是 ((4(i-j)+1)sum(i+1,n))。所以 (f) 的转移为(在求出 (dp) 的情况下):
暴力转移复杂度是平方的,可接受。
下面考虑计算 (dp)(重头戏)。(dp_{l,r}) 的定义并不关心最终停留在 (l) 还是要回到 (r),因为这两者没有贡献差。
考虑枚举最左边提前治疗的位置(跟据规定,不能是 (l)!)(i)。那么先 (l o i),贡献显然是 ((i-l)sum(l,r))。然后治愈 (l),并且再往右边走一步,贡献是 (2(sum(l,r)-a_i))。这时候就进入子问题 (dp_{i+1,r}) 了。这时候注意到一个问题!我们规定了阶段开头不能被选,但有可能有相邻的被选的,而直接进入子问题 (dp_{i+1,r}) 的话,就默认了 (i) 选了时 (i+1) 不能被选,就漏情况了!如何补救?我们考虑将阶段的最左边不能被选的规定去掉,但这样原来设这个规定所修补的漏洞——不知道该往左回到哪儿又出现了。考虑强行令它回到 (l),这只会使应用该阶段的最小贡献不降,并且保留了原来的最优决策,(f) 转移时自然而然地会选择 (l) 不被选的阶段,所以其实是不会影响答案的。这就是 DP 中另一个 trick 了——退一步海阔天空,适当拓宽 DP 数组的定义,使得答案不变(部分决策变劣且最优决策保留),使得转移更容易。
这样一来就可以 (i=l) 了,并且可以直接进入子问题 (dp_{i+1,r})。继续,将 (dp_{i+1,r}) 加上,此时没有算出在 ([i+1,r]) 内墨迹时 ([l,i-1]) 内的贡献,而这墨迹次数也是一定的,贡献是 ((3(r-i-1)+1)sum(l,i-1))。此时回到了 (i+1),再往 (l+1) 走,这样 (xin[l+1,i-1]) 被贡献的次数容易知道是 (2(i-x))。那么总贡献容易维护 (a_i) 前缀和 (A(sum)) 以及 (a_ii) 前缀和 (B(Sum)) 来计算。以及 (i=r) 特殊一点,因为 ([i+1,r]) 是空的,直接特殊转移掉,是 (mathrm O(1)) 的,式子就不给了在代码里看吧。下面给出 (iin[l,r-1]) 的转移式:
这样由于区间中枚举断点,复杂度是三方的,考虑优化。注意到转移到的右端点总是 (r),所以可以看作 (n) 次 (r) 固定的关于 (l) 的从后往前的线性 DP。将式子拆开,(sum) 和 (Sum) 都拆成 (A) 和 (B),我一看,这里面有 (i) 和 (l) 的乘积项!woc 这不就要斜率优化了吗?再定睛一看,所有的这些项竟然都抵消掉了!那真是皆大欢喜,可以直接分离变量,关于 (l) 的是常数放一边,关于决策变量 (i) 的放另一边,这样就可以递推优化了,复杂度平方。放个式子:
总结一下:
- 分阶段的 DP 考虑线性 DP,而非区间 DP(不降智都能想到好吧。)。
- 无法在转移时当场计算的贡献,考虑费用提前计算。
- 为了方便,在不改变答案的情况下适当扩展 DP 数组的定义。
code
#include<bits/stdc++.h>
using namespace std;
#define int long long
const int inf=0x3f3f3f3f3f3f3f3f;
const int N=3010;
int n;
int a[N],A[N],B[N];
int sum(int l,int r){return A[r]-A[l-1];}
int Sum(int l,int r){return B[r]-B[l-1];}
int dp[N][N],f[N];
signed main(){
cin>>n;
for(int i=1;i<=n;i++)scanf("%lld",a+i),A[i]=A[i-1]+a[i],B[i]=B[i-1]+i*a[i];
for(int r=1;r<=n;r++){
int now=inf;
for(int l=r;l;l--){
if(l<r){
int i=l;
now=min(now,i*A[r]-2*a[i]+dp[i+1][r]+(3*(r-1)+1)*A[i-1]-i*A[i-1]-2*B[i-1]);
}
dp[l][r]=(r-l)*sum(l,r)+(sum(l,r)-a[r])+(2*r-1)*sum(l,r-1)-2*Sum(l,r-1);
dp[l][r]=min(dp[l][r],-l*sum(l,r)+2*sum(l,r)-(3*(r-1)+1)*A[l-1]+2*B[l-1]+now);
// for(int i=l;i<r;i++){
//// dp[l][r]=min(dp[l][r],(i-l)*sum(l,r)+2*(sum(l,r)-a[i])+dp[i+1][r]+(3*(r-i-1)+1)*sum(l,i-1)+2*i*sum(l,i-1)-2*Sum(l,i-1));
//// dp[l][r]=min(dp[l][r],-l*sum(l,r)+i*sum(l,r)+2*sum(l,r)-2*a[i]+dp[i+1][r]+(3*(r-1)+1)*A[i-1]-(3*(r-1)+1)*A[l-1]-3*i*A[i-1]+3*i*A[l-1]+2*i*A[i-1]-2*i*A[l-1]-2*B[i-1]+2*B[l-1]);
// dp[l][r]=min(dp[l][r],-l*sum(l,r)+2*sum(l,r)-(3*(r-1)+1)*A[l-1]+2*B[l-1]+(i*A[r]-2*a[i]+dp[i+1][r]+(3*(r-1)+1)*A[i-1]-i*A[i-1]-2*B[i-1]));//消掉了!
//// cout<<l<<' '<<r<<" "<<i<<":"<<dp[l][r]<<"!
";
// }
}
}
for(int i=1;i<=n;i++){
f[i]=inf;
for(int j=1;j<=i;j++)f[i]=min(f[i],f[j-1]+(j>1)*sum(j,n)+dp[j][i]+(4*(i-j)+1)*sum(i+1,n));
}
cout<<f[n];
return 0;
}