「HNOI 2008」玩具装箱TOY
首先O(n2)做法是显然的,使用前缀和然后暴力枚举转移
dp[0] = 0;
for(int i = 1; i <= n; i ++) {
dp[i] = 1LL << 62;
for(int j = 0; j < i; j ++) {
LL x = i - (j + 1) + s[i] - s[j];
dp[i] = min(dp[i], dp[j] + (x - L) * (x - L));
}
}
上面的dp转移方程为:(s[k]=∑i=1kc[i] 为前缀和)
dp[i]=min(dp[j]+(i−j−L−1+s[i]−s[j])2)(0≤j<i)
令ai=s[i]+i−L−1,bj=s[j]+j,则:
dp[i]=min(dp[j]+(ai−bj)2)(0≤j<i)
假设选j转移:dp[i]=dp[j]+(ai−bj)2
dp[i]=dp[j]+ai2+bj2−2aibj
移项得:
2aibj+dp[i]−ai2=dp[j]+bj2
把这看成是kx+b=y的直线形式,则其斜率为2ai,x=bj,y=dp[j]+bj2,截距dp[i]−ai2
也就是说假设我们用j转移,y=2aix+b这条直线经过点Pj(bj,dp[j]+bj2)时,它的截距+ai2就是dp[i]。
这样我们每次选择一个最优的j转移就行了
显然是找一个斜率为2ai、经过Pj,截距最小的j。
根据图像,这些最优的j点是会构成一个下凸包的
然后每次找到第一个满足的位置就是最优答案(截距最小),如图

记slope(P1,P2)表示经过P1,P2的直线斜率
可以发现最优解处就是找到凸包上的第一个满足slope(Pj,Pj+1)>2ai的点j
注意每次需要查找的ai=s[i]+i−L−1是递增的,因为s[i]+i递增,−L−1是常量
因此可以使用单调队列维护凸包
考虑队首:根据单调性,若slope(Pj,Pj+1)<2ai,Pj就可以以后都不再考虑,把它出队(pop_front)
这样就可以更新dp[i]了。
再考虑队末:把Pi(bi,dp[i]+bi2)加入凸包。如图所示,红线优于绿线,因此Pback−1将改为连Pi

因此单调队列具体做法是:记末端2个点分别为Pback−1,Pback
若slope(Pback−1,Pback)>slope(Pback−1,Pi),从队尾弹出(pop_back)
直到只剩一个点或者不满足大于条件时插入。
#include <cstdio>
typedef long long LL;
const int N = 5e4 + 10;
int n, L, c[N];
LL dp[N], s[N], a[N], b[N];
inline LL x(int i) { return b[i]; }
inline LL y(int i) { return dp[i] + b[i] * b[i]; }
inline double slope(int i, int j) { return (y(j) - y(i)) / (double) (x(j) - x(i)); }
int main() {
scanf("%d%d", &n, &L);
for(int i = 1; i <= n; i ++) {
scanf("%d", &c[i]);
s[i] = s[i - 1] + c[i];
a[i] = (b[i] = s[i] + i) - L - 1;
}
static int q[N], hd = 0, bk = 0;
q[bk ++] = 0; dp[0] = a[0] = b[0] = 0;
for(int i = 1; i <= n; i ++) {
for(; hd + 1 < bk && slope(q[hd], q[hd + 1]) < 2.0 * a[i]; ++ hd) ;
dp[i] = dp[q[hd]] + (a[i] - b[q[hd]]) * (a[i] - b[q[hd]]);
for(; hd < bk - 1 && slope(q[bk - 2], q[bk - 1]) > slope(i, q[bk - 1]); -- bk) ;
q[bk ++] = i;
}
printf("%lld\n", dp[n]);
return 0;
}