本篇博客同时作为 WC2021 Day1 的植入的搬迁出来的某篇文章。
适用范围
1D/1D 的动态规划中存在一个与状态有关的变量与转移有关变量的乘积项时所用的 DP 优化,可以将时间复杂度从平方阶优化到线性、
?
1.把状态方程变换为平面的斜率问题
例题 HDU 3507 Print Article
Description
给定序列 (c),将序列划分成若干段,每段的代价为 (sum c_i + M),(M) 是给定的常数,最小化总代价。
Solution
待填。
例题 HNOI2008 玩具装箱
Description
给定序列 (c) 和参数 (L),将 (c) 划分为若干段,每一段的代价为 ((sum c_i + length - 1 - L) ^ 2),最小化代价总和。
Solution
设 (f_{i}) 表示将前 (i) 个划分完成的最小代价,显然有:
其中 (sum_i) 是 (c) 的前缀和。
发现直接求解的复杂度是 (mathcal Oleft(n^2 ight)) 的,不够优秀。
考虑优化。
发现对于 (i in [1, n]),使得 (f_i) 最小时取得的 (j) 是随着 (i) 的递增单调不降的。
因此决策满足单调性。
设当决策点取 (j) 的时候,(f_i) 取得最优决策,则转移式的 (min) 可以去掉,即:
代换一下,设 (a_i = i - 1 + sum_i - L, b_i = sum_i + j),因此原式可以化为:
其中 (f_j) 在之前已经被求出,(a_i,b_j) 都是常亮,发现 这个式子就是一个自变量为 (b_i) 的方程,(2 a_i) 是斜率,(f_i - a_i^2) 是截距。
我们要实现的就是找让这个截距最小的过程。
用单调队列维护下凸壳,则队列内由相邻两个点构成线段所在的直线满足斜率单调递增,每次找到第一个斜率大于 (2 a_i) 的位置,即符合条件的决策点。
由于决策满足单调性,之前的决策点可以直接弹出。
每个决策点只进出了一次单调队列,因此复杂度为 (mathcal O left(n ight))。
#include <bits/stdc++.h>
#define int long long
using namespace std;
const int Maxn = 5e4 + 100;
int f[Maxn], c[Maxn], sum[Maxn], a[Maxn], b[Maxn];
inline double slope(int d1, int d2) {return (double)(f[d1] + b[d1] * b[d1] - f[d2] - b[d2] * b[d2]) / (double)(b[d1] - b[d2]);}
int q[Maxn], h = 0, t = 0;
int n, L;
signed main() {
ios::sync_with_stdio(false);
cin >> n >> L;
for(int i = 1; i <= n; ++i) {
cin >> c[i];
sum[i] = sum[i - 1] + c[i];
}
for(int i = 1; i <= n; ++i) {
a[i] = i - 1 + sum[i] - L;
b[i] = sum[i] + i;
}
for(int i = 1; i <= n; ++i) {
while((h < t) && (slope(q[h], q[h + 1]) < 2 * a[i])) h++;
f[i] = f[q[h]] + b[q[h]] * b[q[h]] - 2 * a[i] * b[q[h]] + a[i] * a[i];
while((h < t) && (slope(q[t], i) <= slope(q[t], q[t - 1]))) t--;
q[++t] = i;
//cout << h << " " << f[h] << "
";
}
cout << f[n];
return 0;
}