斜率优化DP学习笔记
Tarantula
2021-08-16 21:46:04
~~我是个DP菜鸡实锤了~~
---
先来看[题](https://vjudge.net/problem/HDU-3507)
考虑DP,应该很容易可以得出状态转移方程。设 $dp_i$ 表示前 $i$ 个单词进行分组的代价,显然
$$dp_i=\min\limits_{0\le j<i }\{dp_j+(sum_i-sum_j)^2+m\}$$
其中 $sum$ 表示前缀和。
但是朴素计算这个东西的复杂度是 $O(n^2)$ 的,我们无法承受。因此必须优化。枚举 $dp_{1-n}$ 的循环显然是不可少的,因此只能在计算状态转移方程上做文章。
若 $j<k$,考虑决策 $k$ 比决策 $j$ 更优的条件:
$$dp_j+(sum_i-sum_j)^2+m\ge dp_k+(sum_i-sum_k)^2+m$$
将其拆开
$$dp_j+sum^2_i+sum^2_j-2sum_isum_j+m\ge dp_k+sum^2_i+sum^2_k-2sum_isum_k+m$$
$$dp_j+sum^2_j-2sum_isum_j\ge dp_k+sum^2_k-2sum_isum_k$$
将含 $sum_i$ 的项移到一边去
$$2sum_i(sum_k-sum_j)\ge dp_k+sum^2_k-dp_j-sum^2_j$$
$$2sum_i\ge\dfrac{dp_k+sum^2_k-dp_j-sum^2_j}{sum_k-sum_j}$$
这个式子的右边就长得很像斜率了。
而我们发现 $sum_i$ 是单调递增的。也就是说,如果对于状态 $i$,决策 $k$ 优于 $j$,那么随着 $sum_i$ 的增大,上述式子一定始终成立。这样的话,我们在之后的计算过程中,就完全可以不考虑 $j$ 这个“废点”。
因此,我们可以维护一个单调队列,保存所有合法的决策点。在整个过程中,一旦遇到队头两点 $j,k$ 组成的“斜率”——也就是上面式子的右边——小于等于 $2sum_i$,就可以直接把点$j$从队列里给`pop`掉(因为它将永远不会优于点 $k$)。同时,我们每枚举到一个状态 $i$,就要将点 $i$ 入队,同时维护队内相邻两点的斜率的单调性(这也是为了及时排除无用决策,原理同上)。
这样一来,每个点至多入队和出队一次,我们就做到了 $O(n)$ 的复杂度。
```cpp
#include<bits/stdc++.h>
#define int long long
using namespace std;
int n, m;
int a[500005];
int sum[500005], dp[500005];
int q[500005];
int up(int x, int y) {return dp[y] + sum[y] * sum[y] - dp[x] - sum[x] * sum[x];}//点x和点y组成的斜率的分子
int down(int x, int y) {return sum[y] - sum[x];}//同上,分母
void work() {
memset(dp, 0, sizeof(dp)); memset(q, 0, sizeof(q)); memset(sum, 0, sizeof(sum));
for (int i = 1; i <= n; i++)
cin >> a[i], sum[i] = sum[i - 1] + a[i];
int l = 1, r = 1; q[1] = 0;
for (int i = 1; i <= n; i++) {
while(l + 1 <= r && up(q[l], q[l + 1]) <= 2 * sum[i] * down(q[l], q[l + 1])) l++;//一旦发现推的式子成立,就排除队头决策
int j = q[l];
dp[i] = dp[j] + (sum[i] - sum[j]) * (sum[i] - sum[j]) + m;//这时可以直接取出队头计算了,由于队内斜率的单调性,q[l]一定最优
while (l + 1 <= r && up(q[r - 1], q[r]) * down(q[r], i) >= up(q[r], i) * down(q[r - 1], q[r])) r--;//维护决策单调性
q[++r] = i;//把i入队
}
cout << dp[n] << endl;
}
signed main() {
while (cin >> n >> m)
work();
return 0;
}
```
把分子和分母单独写一个函数应该会比较清楚吧。
---
再来看[这个](https://www.luogu.com.cn/problem/P3195)
考虑朴素DP:
$$dp_i=\min\limits_{0\le j<i}\{dp_j+(sum_i-sum_j+i-j-1-L)^2\}$$
发现这个式子比较臃肿,可以换个元。设 $h_i=sum_i+i$,$g_i=sum_i+i-1-L$。于是可以改写为
$$dp_i=\min\limits_{0\le j<i}\{dp_j+(g_i-h_j)^2\}$$
接下来就是一样的套路。考虑若 $j<k$,决策 $k$ 优于 $j$ 的条件:
$$dp_j+(g_i-h_j)^2\ge dp_k+(g_i-h_k)^2$$
$$dp_j+h^2_j-2g_ih_j\ge dp_k+h^2_k-2g_ih_k$$
$$2g_i(h_k-h_j)\ge dp_k+h^2_k-dp_j-h^2_j$$
$$2g_i\ge \dfrac{dp_k+h^2_k-dp_j-h^2_j}{h_k-h_j}$$
由于 $g_i$ 也是单调递增的,所以也可以维护一个斜率递增的单调队列,然后及时排除无用决策。复杂度 $O(n)$。这题代码和上面几乎一模一样,就不给了。
---
Q:何时可以考虑斜率优化DP?
A:状态转移方程遵循如下形式:
$$dp_i=\min\{dp_j+val(i,j)\}$$
其中 $val$ 是一个与 $i,j$ **均有关**的多项式(一般和前缀和有关,这样才能利用单调性进行决策点的维护)。这个时候就可以推一推式子,再利用决策单调性进行优化了。
---
[一道经典题](https://www.luogu.com.cn/problem/P5017)
这道题挺有意思的,做法很多。设第 $i$ 个人坐上车的时间为 $time_i$,那么答案就是 $\sum(time_i-t_i)$,即 $\sum time_i-\sum t_i$。发现后面那个 $\sum t_i$ 是固定的,所以只需要让前面的部分最小即可。
设 $dp_i$ 表示接送完前$i$分钟的所有学生所需的最小代价,易得状态转移方程:
$$dp_i=\min\limits_{0\le j\le i-m}\{dp_j+i(sum_i-sum_j)\}$$
其中 $sum_i$ 表示前 $i$ 分钟来等车的总人数。
我们发现 $i(sum_i-sum_j)$ 是一个与 $i,j$ 均有关的多项式,因此显然是可以推一推式子然后斜率优化的。式子这里就不推了,很套路。
代码稍有不同。
```cpp
#include<bits/stdc++.h>
#define int long long
using namespace std;
int n, m, maxt;
int a[505];
int sum[4000005];
int dp[4000005];
int q[4000005];
int ans = 1e18;
int up(int x, int y) {return dp[y] - dp[x];}
int down(int x, int y) {return sum[y] - sum[x];}
signed main() {
cin >> n >> m;
for (int i = 1; i <= n; i++) {
cin >> a[i];
sum[++a[i]]++; maxt = max(maxt, a[i]);
}
for (int i = 1; i <= maxt + m; i++)
sum[i] += sum[i - 1];
int l = 1, r = 1; q[1] = 0;
for (int i = 1; i <= maxt + m; i++) {
if (i > m) {
int x = i - m;
while (l + 1 <= r && up(q[r - 1], q[r]) * down(q[r], x) >= up(q[r], x) * down(q[r - 1], q[r])) r--;
q[++r] = x;
}//由于此时i-m也是决策的一个候选项,所以先将i-m入队
while (l + 1 <= r && up(q[l], q[l + 1]) <= i * down(q[l],q[l + 1])) l++;
int j = q[l];
dp[i] = dp[j] + (sum[i] - sum[j]) * i;
if (i >= maxt) ans = min(ans, dp[i]);//注意答案要取最小值
}
for (int i = 1; i <= n; i++)
ans -= a[i];
cout << ans << endl;
return 0;
}
```
---
几道练习题:
- [P2365 任务安排](https://www.luogu.com.cn/problem/P2365)
- [P3628 [APIO2010]特别行动队](https://www.luogu.com.cn/problem/P3628)
- [P3648 [APIO2014]序列分割](https://www.luogu.com.cn/problem/P3648)
- [P4072 [SDOI2016]征途](https://www.luogu.com.cn/problem/P4072)
---
~~没了~~