斜率优化DP#
五道题,套路基本相同,期望2h讲完.
P3195 - 玩具装箱#
题意#
有 个玩具,第 个玩具价值为 。要求将这 个玩具排成一排,分成若干段。对于一段 ,它的代价为:
求分段的最小代价。
.
DP#
令 表示前 个物品,分若干段的最小代价。
状态转移方程:
其中 表示 数列中,前 个数的和,即 。
时间复杂度为 ,无法解决本题。
优化#
简化状态转移方程式:令 ,则
我们目前优化的目的,是尽快地找到上面式子中的 ,也就是令 最小化的继承状态。
为了表示方便,设 为能使 最小化的继承状态,所以有:
考虑一次函数的斜截式 ,将方程转化为这个形式。
其中变量 与 有关, 与 无关,且要求 随 单调递增, 中包含 (建模需要)。
按照上面的规则,对方程进行整理:
也就是:
与一次函数 对照地看,其中:
\left\{ \begin{array}{**lr**} y = f_j+(s_j+L')^2\\ k = 2s_i\\ x = s_j+L\\ b = f_i-{s_i}^2 \end{array} \right.我们把 看成平面直角坐标系上的点。那么现在 的意义就是:直线 上的一个点。
又因为我们的目的是最小化 ,在上面的表示当中, 只与直线的截距 有关。所以显然地,问题转化为:如何选取 ,才能使得过点 的直线的截距 最小。

注意到:直线的斜率不变。于是相当于平移直线 ,直到其经过图中的一个点。
如图,为了最小化 ,显然除了折线上面的点,都不可能是最优解。(这条折线上的点集就是凸包的一部分。
在折线上任取三个点 满足 ,一定满足 .
由图像又易知,最优解 就是第一个 的点。
于是可以得到 的算法。(处理凸包后,每次利用单调性二分)
由于随着 的增长, 也是单调递增的,不难发现可以用单调队列得到 的算法。
设单调队列(递增)为 ,队首为 ,队尾为 ,当前要转移 号点,那么具体步骤:
- 当 时,++,(保证最优解)
- 此时的 就是最优转移点,根据它和转移方程计算出 ,
- 当 时,—,(维护下凸包)
- 在队尾插入 .
代码#
#include <bits/stdc++.h>
#define int long long
using namespace std;
const int MAXN = 5e4 + 10;
int n, L;
int c[MAXN], s[MAXN], f[MAXN];
double slope(int i, int j) {
return ((f[j]+(s[j]+L)*(s[j]+L)) - (f[i]+(s[i]+L)*(s[i]+L))) / (double)(s[j] - s[i]);
}
int head, tail, q[MAXN];
signed main() {
scanf("%lld%lld", &n, &L);
L += 1;
for(int i = 1; i <= n; i++) {
scanf("%lld", &c[i]);
s[i] = s[i - 1] + c[i] + 1;
}
head = tail = 1;
for(int i = 1; i <= n; i++) {
while(head < tail && slope(q[head], q[head + 1]) <= 2 * s[i]) {
head++;
}
f[i] = f[q[head]] + (s[i] - s[q[head]] - L) * (s[i] - s[q[head]] - L);
while(head < tail && slope(q[tail - 1], q[tail]) >= slope(q[tail], i)) {
tail--;
}
q[++tail] = i;
}
printf("%lld", f[n]);
return 0;
}
cP3628 [APIO2010]特别行动队#
题目描述#
你有一支由 名预备役士兵组成的部队,士兵从 到 编号,你要将他们拆分成若干特别行动队调入战场。出于默契的考虑,同一支特别行动队中队员的编号应该连续,即为形如 的序列。所有的队员都应该属于且仅属于一支特别行动队。
编号为 的士兵的初始战斗力为 ,一支特别行动队的初始战斗力 为队内士兵初始战斗力之和,即 .
通过长期的观察,你总结出对于一支初始战斗力为 的特别行动队,其修正战斗力 ,其中 是已知的系数 . 作为部队统帅,现在你要为这支部队进行编队,使得所有特别行动队的修正战斗力之和最大。
试求出这个最大和。
.
DP#
与上一题类似。
令 表示前 个物品,分若干段的最小代价,有状态转移方程:
其中 是 数列的前缀和,也就是 .
优化#
设 是能使 最大化的继承状态,那么整理状态转移方程:
考虑一次函数斜截式 或 ,将状态转移方程变换为这个形式。
其中变量 与 有关, 与 无关。
所以:
要最大化 ,也就是最大化截距 ,把 看作平面直角坐标系上的点。
那么由于本题中 ,是单调递减的,与上凸包单调性相同。所以与上一题类似,选取的点一定只在上凸包上,并且是第一个斜率小于 的点。
可以用单调队列维护:
设单调队列(递增)为 ,队首为 ,队尾为 ,当前要转移 号点,那么具体步骤:
- 当 时,++,(保证最优解)
- 此时的 就是最优转移点,根据它和转移方程计算出 ,
- 当 时,—,(维护上凸包)
- 在队尾插入 .
代码#
#include <bits/stdc++.h>
#define int long long
#define x(X) (s[X])
#define y(X) (f[X] + a * s[X] * s[X] - b * s[X] + c)
using namespace std;
const int MAXN = 1e6 + 10;
int n;
int a, b, c;
int s[MAXN], f[MAXN];
int head, tail, q[MAXN];
double slope(int i, int j) {
return 1.0 * (y(j) - y(i)) / (x(j) - x(i));
}
signed main() {
scanf("%lld%lld%lld%lld", &n, &a, &b, &c);
for(int i = 1; i <= n; i++) {
int tmp; scanf("%lld", &tmp);
s[i] = s[i - 1] + tmp;
}
head = tail = 1;
for(int i = 1; i <= n; i++) {
while(head < tail && slope(q[head], q[head + 1]) >= 2ll * a * s[i]) {
head++;
}
int j = q[head]; f[i] = f[j] + a * (s[i]-s[j])*(s[i]-s[j]) + b * (s[i]-s[j]) + c;
while(head < tail && slope(q[tail - 1], q[tail]) <= slope(q[tail], i)) {
tail--;
}
q[++tail] = i;
}
cout << f[n] << endl;
return 0;
}
cP3648 [APIO2014]序列分割#
题目描述#
你正在玩一个关于长度为 的非负整数序列的游戏,序列中第 个元素的值为 . 这个游戏中,你需要把序列分为 个非空的块。为了得到 块,你需要重复下面的操作 次:
- 选择一个有超过一个元素的块(初始时你只有一块,即整个序列)
- 选择两个相邻元素,把这个块从中间分开,得到两个非空的块。
每次操作后,你将获得那两个新产生的块的元素和的乘积的分数。
求总得分最大值,并输出任意一种分割方案。
.
分析#
首先可以发现,最终的分数与切的顺序无关。
证明:对于长度为 的序列 ,将其分为三部分,只有两种方法:
- 先分割 ,再分割 ,贡献为 ,
- 先分割 ,再分割 ,贡献为 .
显然可以推广到更大的长度,于是结论得证。
DP#
由于上面的结论,假设所有切割是按照从左到右的顺序进行的。
定义 表示前 个数,进行了 次切割的最大得分,那么有状态转移方程:
其中 是题目序列中的元素前缀和,即 .
优化#
考虑到斜率优化的形式,需要 变成一维,又发现状态转移方程中, 只和 有关,所以可以滚动数组。
设 ,,那么方程可以写为:
这就很容易想到斜率优化。
设 是能使 最大化的继承状态,那么状态转移方程化为:
考虑一次函数斜截式 ,要把方程转化为这个形式。
其中变量 与 相关,常量 与 无关。
于是:
所以:
\left\{ \begin{array}{**lr**} y = g_j-{s_j}^2\\ k = -s_i\\ x = s_j\\ b = f_i \end{array} \right.这里斜率 是单调下降的,并且最优决策点 是第一个满足 的点。所以最优决策点一定在上凸包上。
#include <bits/stdc++.h>
#define int long long
#define X(i) (s[i])
#define Y(i) (g[i] - s[i] * s[i])
using namespace std;
const long double inf = -1e18;
const int MAXN = 1e5 + 10;
int n, k;
int s[MAXN], f[MAXN], g[MAXN];
double slope(int i, int j) {
if(s[j] == s[i]) return inf;
return (Y(j) - Y(i)) / (double)(X(j) - X(i));
}
int head, tail, q[MAXN];
int to[210][MAXN];
signed main() {
scanf("%lld%lld", &n, &k);
for(int i = 1; i <= n; i++) {
int tmp; scanf("%lld", &tmp);
s[i] = s[i - 1] + tmp;
}
for(int l = 1; l <= k; l++) {
head = tail = 1; q[1] = 0;
for(int i = 1; i <= n; i++) {
while(head < tail && slope(q[head], q[head + 1]) >= -1 * s[i]) {
++head;
}
int j = q[head]; to[l][i] = q[head];
f[i] = g[j] + s[j] * (s[i] - s[j]);
while(head < tail && slope(q[tail - 1], q[tail]) <= slope(q[tail], i)) {
tail--;
}
q[++tail] = i;
}
memcpy(g, f, sizeof(f));//把f复制到g
}
printf("%lld\n", f[n]);
for(int i = k, j = n; i >= 1; i--) {
j = to[i][j];
printf("%lld ", j);
}
return 0;
}
cP4360 [CEOI2004]锯木厂选址#
题目描述#
从山顶到山底下沿着一条直线种植了 颗老树。当地的政府决定把它们砍下来。为了不浪费任何一颗木材,树被砍倒后要运送到锯木厂。
木材只能朝山下运。山脚下有一个锯木厂。另外两个锯木厂将新修建在山路上。你必须决定在哪里修建这两个锯木厂,使得运输的费用总和最小。假定运输每公斤木材,移动每米需要一分钱。
题目给定第 颗树的重量 ,以及第 颗树与第 颗树之间的距离 .
请计算最小运输费用。
,保证在 long long 范围内。
DP#
https://blog.csdn.net/forever_dreams/article/details/97973474 ↗
这题难点可能在这部分…)
定义 为以 作为第二个锯木厂的最小花费, 为 的前缀和, 为 的后缀和, 为 的总和。
转移就是通过枚举第一个锯木厂,然后减去省掉的花费。有下面的转移方程:
斜率优化#
这部分和前面都一样,所以字越来越少……
设 最优,化简:
所以:
\left\{ \begin{array}{**lr**} y = s_j\times dis_j\\ k = dis_i\\ x = s_j\\ b = cost-f_i-s_i\times dis_i \end{array} \right.是后缀和,单调递减。又因为要最小化 ,所以要最大化 。
所以最优决策点 是第一个满足 的点,单调队列维护上凸包即可。
代码#
#include <bits/stdc++.h>
#define int long long
#define X(i) (s[i])
#define Y(i) (s[i] * dis[i])
using namespace std;
const int MAXN = 2e4 + 10;
int n;
int w[MAXN], d[MAXN];
int cost, s[MAXN], dis[MAXN], f[MAXN];
double slope(int i, int j) {
return (Y(j) - Y(i)) / (double)(X(j) - X(i));
}
int head, tail, q[MAXN];
int ans = 1e18;
signed main() {
scanf("%lld", &n);
for(int i = 1; i <= n; i++) scanf("%lld%lld", &w[i], &d[i]);
for(int i = 1; i <= n; i++) s[i] = s[i - 1] + w[i];
for(int i = n; i >= 1; i--) dis[i] = dis[i + 1] + d[i];
for(int i = 1; i <= n; i++) cost += w[i] * dis[i];
head = tail = 1;
for(int i = 1; i <= n; i++) {
while(head < tail && slope(q[head], q[head + 1]) >= dis[i]) ++head;
int j = q[head];
f[i] = cost - s[j] * dis[j] - (s[i] - s[j]) * dis[i];
ans = min(ans, f[i]);
while(head < tail && slope(q[tail - 1], q[tail]) <= slope(q[tail], i)) tail--;
q[++tail] = i;
}
printf("%lld\n", ans);
return 0;
}
cP2900 [USACO08MAR]Land Acquisition G#
题目描述#
翻译的不是很好,所以我简化一下题面。
给定 个矩阵的宽 和高 ,每次选一个矩阵集合,代价为集合中的 。
任意两次之间没有重复的矩阵,求取完所有矩阵的最小代价。
分析#
注意到对于两个矩阵 ,如果 并且 ,那么 和 一定可以放到一组, 对答案没有贡献,不用考虑,可以直接删去。
比如下图当中的 和 . 矩阵 能完全被 包含,所以 没有贡献,可以删去。
于是考虑先删除所有 这样的矩阵。
具体做法就是:按照 为第一关键字, 为第二关键字从大到小排序,之后双指针扫一遍即可。
//a已经降序排序
int t = 0;
for(int i = 1; i <= n; i++) {
if(a[i].w > a[t].w) a[++t] = a[i];
}
cpp如上图,剩下的只有 号矩阵。
删完之后剩下的就是一个, 严格单调递减, 严格单调递增的序列。考虑 解决。
DP#
设 为新序列中,取走前 个矩阵的最小花费, 为宽(单调递增), 为高(单调递减),那么有转移方程:
相当于,把 到 这一段合为一组取走。由于单调性,所以这一组的 ,.
复杂度 。这显然可以斜率优化。
斜率优化#
我们目前优化的目的,是尽快地找到 到底是由哪个 转移过来的,也就是尽快找到最小化 的继承状态。
为了表示方便,设 为能使 最小化的继承状态,所以有:
考虑一次函数斜截式 或 ,将状态转移方程变换为这个形式:
其中变量 与 有关, 与 无关。且要求 随 单调递增, 中包含 (斜率优化基本操作)。
所以可以构造出直线方程为:
\left\{ \begin{array}{**lr**} y = f_j\\ k = w_i\\ x = -h_{j+1}\\ b = f_i \end{array} \right.随 单调递增, 随 单调递增,要使截距 最小,最优解 就是从左往右枚举到的第一个 的点。
所以单调队列维护下凸包即可。
代码#
#include <bits/stdc++.h>
#define X(i) (-a[i+1].h)
#define Y(i) (f[i])
#define int long long
using namespace std;
const int MAXN = 5e4 + 10;
int n;
struct Node{
int w, h;
} a[MAXN];
int f[MAXN];
bool cmp(Node i, Node j) {
return (i.h > j.h) || (i.h == j.h && i.w > j.w);
}
double slope(int i, int j) {
return (Y(j) - Y(i)) / (double)(X(j) - X(i));
}
int head, tail, q[MAXN];
signed main() {
scanf("%lld", &n);
for(int i = 1; i <= n; i++) {
scanf("%lld%lld", &a[i].w, &a[i].h);
}
sort(a + 1, a + n + 1, cmp);
int t = 0;
for(int i = 1; i <= n; i++) {
if(a[i].w > a[t].w) a[++t] = a[i];
}
n = t;
head = tail = 1;
for(int i = 1; i <= n; i++) {
while(head < tail && slope(q[head], q[head + 1]) <= a[i].w) {
head++;
}
int j = q[head]; f[i] = f[j] + a[i].w * a[j + 1].h;
while(head < tail && slope(q[tail - 1], q[tail]) >= slope(q[tail], i)) {
tail--;
}
q[++tail] = i;
}
cout << f[n] << endl;
return 0;
}
c总结?#
如果转移方程能表示成: 的形式(或 ),并且 关于 有单调性,就都可以用斜率优化。
斜率优化能把 降成 .
数形结合。
代码短。