TonyYin's Blog

Back

斜率优化DP#

五道题,套路基本相同,期望2h讲完.

P3195 - 玩具装箱#

题意#

nn 个玩具,第 ii 个玩具价值为 cic_i。要求将这 nn 个玩具排成一排,分成若干段。对于一段 [l,r][l,r],它的代价为:

(rl+i=lrciL)2(r-l+\sum_{i=l}^r c_i-L)^2

求分段的最小代价。

1n5×104,1L,0ci1071\le n\le 5\times 10^4,1\le L,0\le c_i\le 10^7.

DP#

fif_i 表示前 ii 个物品,分若干段的最小代价。

状态转移方程:

fi=minj<i{fj+(preiprej+ij1L)2}f_i=\min_{j<i}{\{f_j+(pre_i-pre_j+i-j-1-L)^2\}}

其中 preipre_i 表示 cc 数列中,前 ii 个数的和,即 j=1icj\sum_{j=1}^i c_j

时间复杂度为 O(n2)O(n^2),无法解决本题。

优化#

简化状态转移方程式:令 si=prei+i,L=L+1s_i=pre_i+i,L'=L+1,则

fi=minj<ifj+(sisjL)2f_i=\min_{j<i}{f_j+(s_i-s_j-L')^2}

我们目前优化的目的,是尽快地找到上面式子中的 jj,也就是令 fif_i 最小化的继承状态。

为了表示方便,设 jj 为能使 fif_i 最小化的继承状态,所以有:

fi=fj+(sisjL)2f_i=f_j+(s_i-s_j-L')^2

考虑一次函数的斜截式 y=kx+by=kx+b,将方程转化为这个形式。

其中变量 x,yx, yjj 有关,b,kb, kjj 无关,且要求 xxjj 单调递增,bb 中包含 fif_i(建模需要)。

按照上面的规则,对方程进行整理:

fi=fj+[si(sj+L)]2fi=fj+si22si(sj+L)+(sj+L)2fisi2+2si(sj+L)=fj+(sj+L)2\begin{aligned} f_i&=f_j+[s_i-(s_j+L')]^2\\ f_i&=f_j+{s_i}^2-2s_i(s_j+L')+(s_j+L')^2\\ f_i-{s_i}^2+2s_i(s_j+L')&=f_j+(s_j+L')^2\\ \end{aligned}

也就是:

fj+(sj+L)2=2si(sj+L)+fisi2f_j+(s_j+L')^2=2s_i(s_j+L)+f_i-{s_i}^2

与一次函数 y=kx+by=kx+b 对照地看,其中:

\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.

我们把 (x,y)(x, y) 看成平面直角坐标系上的点。那么现在 (xj,yj)(x_j, y_j) 的意义就是:直线 y=kx+by=kx+b 上的一个点

又因为我们的目的是最小化 fif_i,在上面的表示当中,fif_i 只与直线的截距 bb 有关。所以显然地,问题转化为:如何选取 jj ,才能使得过点 (xj,yj)(x_j, y_j) 的直线的截距 bb 最小。

注意到:直线的斜率不变。于是相当于平移直线 y=kxy=kx ,直到其经过图中的一个点。

20190701195640154

如图,为了最小化 bb,显然除了折线上面的点,都不可能是最优解。(这条折线上的点集就是凸包的一部分。

在折线上任取三个点 A,B,CA, B, C 满足 xA<xB<xCx_A<x_B<x_C,一定满足 slope(A,B)<slope(B,C)\operatorname{slope}(A,B)<\operatorname{slope}(B, C).

由图像又易知,最优解 jj 就是第一个 slope(j,j+1)>k\operatorname{slope}(j, j+1)>k 的点

于是可以得到 O(nlogn)\mathcal{O}(n\log n) 的算法。(处理凸包后,每次利用单调性二分)

由于随着 ii 的增长,k=2sik=2s_i 也是单调递增的,不难发现可以用单调队列得到 O(n)\mathcal{O}(n) 的算法。

设单调队列(递增)为 qq,队首为 headhead,队尾为 tailtail,当前要转移 ii 号点,那么具体步骤:

  1. slope(qhead,qhead+1)2si\operatorname{slope}(q_{head}, q_{head+1})\leq2 s_i 时,headhead++,(保证最优解)
  2. 此时的 qheadq_{head} 就是最优转移点,根据它和转移方程计算出 fif_i
  3. slope(qtail1,qtail)slope(qtail,i)\operatorname{slope}(q_{tail-1}, q_{tail})\geq\operatorname{slope}(q_{tail}, i) 时,tailtail—,(维护下凸包
  4. 在队尾插入 ii.

代码#

#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;
}
c

P3628 [APIO2010]特别行动队#

题目描述#

你有一支由 nn 名预备役士兵组成的部队,士兵从 11nn 编号,你要将他们拆分成若干特别行动队调入战场。出于默契的考虑,同一支特别行动队中队员的编号应该连续,即为形如 (i,i+1,,i+k)(i, i + 1,\cdots, i+k) 的序列。所有的队员都应该属于且仅属于一支特别行动队。

编号为 ii 的士兵的初始战斗力为 xix_i,一支特别行动队的初始战斗力 XX 为队内士兵初始战斗力之和,即 xi+xi+1++xi+kx_i + x_{i+1} + \cdots + x_{i + k}.

通过长期的观察,你总结出对于一支初始战斗力为 XX 的特别行动队,其修正战斗力 X=aX2+bX+cX'=aX^2+bX+c,其中 a,b,ca, b, c 是已知的系数 a<0a < 0. 作为部队统帅,现在你要为这支部队进行编队,使得所有特别行动队的修正战斗力之和最大。

试求出这个最大和。

1n106,5a1,107b,c107,1xi1001\leq n\leq 10^6, -5\leq a\leq -1, -10^7\leq b, c\leq 10^7, 1\leq x_i\leq 100.

DP#

与上一题类似。

fif_i 表示前 ii 个物品,分若干段的最小代价,有状态转移方程:

fi=maxj<i{fj+a(sisj)2+b(sisj)+c}f_i=\max_{j<i}{\{f_j+a(s_i-s_j)^2+b(s_i-s_j)+c\}}

其中 sis_ixx 数列的前缀和,也就是 si=j=1ixjs_i=\sum_{j=1}^i{x_j}.

优化#

jj 是能使 fif_i 最大化的继承状态,那么整理状态转移方程:

fi=fj+a(sisj)2+b(sisj)+cfi=fj+a(si2+sj22sisj)+b(sisj)+cfi=fj+asi2+asj22asisj+bsibsj+c\begin{aligned} f_i&=f_j+a(s_i-s_j)^2+b_(s_i-s_j)+c\\ f_i&=f_j+a({s_i}^2+{s_j}^2-2s_is_j)+b(s_i-s_j)+c\\ f_i&=f_j+a\cdot {s_i}^2+a\cdot {s_j}^2-2a\cdot s_is_j+b\cdot s_i-b\cdot s_j+c \end{aligned}

考虑一次函数斜截式 y=kx+by=kx+bb=ykxb=y-kx,将状态转移方程变换为这个形式。

其中变量 x,yx, yjj 有关,b,kb, kjj 无关

fibsiasi2=(fj+asj2bsj+c)2asisj\textcolor{red}{f_i-b\cdot s_i-a\cdot {s_i}^2}=\textcolor{purple}{(f_j+a\cdot {s_j}^2-b\cdot s_j+c)}-\textcolor{green}{2a\cdot s_i}\textcolor{blue}{s_j}

所以:

{y=fj+asj2bsj+ck=2asix=sjb=fibsiasi2\left\{ \begin{array}{lr} y = f_j+a\cdot {s_j}^2-b\cdot s_j+c\\ k = 2a\cdot s_i\\ x = s_j\\ b = f_i-b\cdot s_i-a\cdot {s_i}^2 \end{array} \right.

要最大化 fif_i,也就是最大化截距 bb,把 (xj,yj)(x_j, y_j) 看作平面直角坐标系上的点。

那么由于本题中 k=2asik=2as_i ,是单调递减的,与上凸包单调性相同。所以与上一题类似,选取的点一定只在上凸包上,并且是第一个斜率小于 kk 的点

可以用单调队列维护:

设单调队列(递增)为 qq,队首为 headhead,队尾为 tailtail,当前要转移 ii 号点,那么具体步骤:

  1. slope(qhead,qhead+1)2asi\operatorname{slope}(q_{head}, q_{head+1})\textcolor{red}{\geq}2a\cdot s_i 时,headhead++,(保证最优解)
  2. 此时的 qheadq_{head} 就是最优转移点,根据它和转移方程计算出 fif_i
  3. slope(qtail1,qtail)slope(qtail,i)\operatorname{slope}(q_{tail-1}, q_{tail})\textcolor{red}{\leq}\operatorname{slope}(q_{tail}, i) 时,tailtail—,(维护上凸包
  4. 在队尾插入 ii.

代码#

#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;
}
c

P3648 [APIO2014]序列分割#

题目描述#

你正在玩一个关于长度为 nn 的非负整数序列的游戏,序列中第 ii 个元素的值为 aia_i. 这个游戏中,你需要把序列分为 k+1k+1 个非空的块。为了得到 k+1k+1 块,你需要重复下面的操作 kk 次:

  • 选择一个有超过一个元素的块(初始时你只有一块,即整个序列)
  • 选择两个相邻元素,把这个块从中间分开,得到两个非空的块。

每次操作后,你将获得那两个新产生的块的元素和的乘积的分数。

求总得分最大值,并输出任意一种分割方案

2n105,1kmin{n1,200}2\leq n\leq 10^5, 1\leq k\leq \min{\{n-1, 200\}}.

分析#

首先可以发现,最终的分数与切的顺序无关

证明:对于长度为 33 的序列 xyzxyz,将其分为三部分,只有两种方法:

  1. 先分割 xyxy,再分割 yzyz,贡献为 x(y+z)+yz=xy+yz+xzx(y+z)+yz=xy + yz + xz
  2. 先分割 yzyz,再分割 xyxy,贡献为 (x+y)z+xy=xy+yz+xz(x+y)z + xy=xy+yz+xz.

显然可以推广到更大的长度,于是结论得证。

DP#

由于上面的结论,假设所有切割是按照从左到右的顺序进行的。

定义 DPi,j\operatorname{DP}_{i, j} 表示前 ii 个数,进行了 jj 次切割的最大得分,那么有状态转移方程:

DPi,k=maxj<i{DPj,k1+sj(sisj)}\operatorname{DP}_{i, k}=\max_{j<i}{\{\operatorname{DP}_{j, k-1}+s_j(s_i-s_j)\}}

其中 ss 是题目序列中的元素前缀和,即 si=j=1iajs_i=\sum_{j=1}^{i}a_j.

优化#

考虑到斜率优化的形式,需要 DP\operatorname{DP} 变成一维,又发现状态转移方程中,DPi\operatorname{DP}_i 只和 DPi1\operatorname{DP}_{i-1} 有关,所以可以滚动数组

fi=DPi,kf_i=\operatorname{DP}_{i, k}gj=DPj,k1g_j=\operatorname{DP}_{j, k-1},那么方程可以写为:

fi=maxj<i{gj+sj(sisj)}f_i=\max_{j<i}{\{g_j+s_j(s_i-s_j)\}}

这就很容易想到斜率优化。

jj 是能使 fif_i 最大化的继承状态,那么状态转移方程化为:

fi=gj+sj(sisj)f_i=g_j+s_j(s_i-s_j)

考虑一次函数斜截式 y=kx+by=kx+b,要把方程转化为这个形式。

其中变量 x,yx, yjj 相关,常量 b,kb, kjj 无关

于是:

fi=gj+sj(sisj)fi=gj+sisjsj2gjsj2=sisj+fi\begin{aligned} f_i&=g_j+s_j(s_i-s_j)\\ f_i&=g_j+s_is_j-{s_j}^2\\ g_j-{s_j}^2&=-s_is_j+f_i \end{aligned}

所以:

\left\{ \begin{array}{**lr**} y = g_j-{s_j}^2\\ k = -s_i\\ x = s_j\\ b = f_i \end{array} \right.

这里斜率 kk单调下降的,并且最优决策点 (xj,yj)(x_j, y_j)第一个满足 slope(j,j+1)<k\operatorname{slope}(j, j+1) < k 的点。所以最优决策点一定在上凸包上。

#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;
}
c

P4360 [CEOI2004]锯木厂选址#

题目描述#

从山顶到山底下沿着一条直线种植了 nn 颗老树。当地的政府决定把它们砍下来。为了不浪费任何一颗木材,树被砍倒后要运送到锯木厂。

木材只能朝山下运。山脚下有一个锯木厂。另外两个锯木厂将新修建在山路上。你必须决定在哪里修建这两个锯木厂,使得运输的费用总和最小。假定运输每公斤木材,移动每米需要一分钱。

题目给定第 ii 颗树的重量 wiw_i ,以及第 ii 颗树与第 i+1i+1 颗树之间的距离 did_i.

请计算最小运输费用

2n20000,1wi,di100002\leq n\leq 20000, 1\leq w_i, d_i\leq 10000,保证在 long long 范围内。

DP#

https://blog.csdn.net/forever_dreams/article/details/97973474

这题难点可能在这部分…)

定义 fif_i 为以 ii 作为第二个锯木厂的最小花费,sis_iwiw_i 的前缀和,disidis_idid_i 的后缀和,costcostwi×disiw_i\times dis_i 的总和。

转移就是通过枚举第一个锯木厂,然后减去省掉的花费。有下面的转移方程:

fi=minj<i{costsj×disj(sisj)×disi}f_i=\min_{j<i}{\{cost - s_j\times dis_j-(s_i-s_j)\times dis_i\}}

斜率优化#

这部分和前面都一样,所以字越来越少……

jj 最优,化简:

fi=costsj×disjsi×disi+sj×disisj×disj=sj×disi+(costfisi×disi)\begin{aligned} f_i&=cost-s_j\times dis_j-s_i\times dis_i+s_j\times dis_i\\ s_j\times dis_j&=s_j\times dis_i+(cost-f_i-s_i\times dis_i) \end{aligned}

所以:

\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.

k=disik=dis_i 是后缀和,单调递减。又因为要最小化 fif_i,所以要最大化 bb

所以最优决策点 (xj,yj)(x_j, y_j) 是第一个满足 slope(j,j+1)<k\operatorname{slope}(j, j+1)<k 的点,单调队列维护上凸包即可。

代码#

#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;
}
c

P2900 [USACO08MAR]Land Acquisition G#

题目描述#

翻译的不是很好,所以我简化一下题面。

给定 nn 个矩阵的宽 ww 和高 hh,每次选一个矩阵集合,代价为集合中的 max{w}×max{h}\max{\{w\}}\times \max{\{h\}}

任意两次之间没有重复的矩阵,求取完所有矩阵的最小代价

分析#

注意到对于两个矩阵 i,ji, j,如果 wiwjw_i\leq w_j 并且 hihjh_i\leq h_j,那么 iijj 一定可以放到一组,ii 对答案没有贡献,不用考虑,可以直接删去。

比如下图当中的 1122. 矩阵 22 能完全被 11 包含,所以 22 没有贡献,可以删去。

P2900-1

于是考虑先删除所有 ii 这样的矩阵。

具体做法就是:按照 hh 为第一关键字,ww 为第二关键字从大到小排序,之后双指针扫一遍即可。

//a已经降序排序
int t = 0;
for(int i = 1; i <= n; i++) {
	if(a[i].w > a[t].w) a[++t] = a[i];
}
cpp

如上图,剩下的只有 1,31, 3 号矩阵。

删完之后剩下的就是一个,hh 严格单调递减ww 严格单调递增的序列。考虑 DPDP 解决。

DP#

fif_i 为新序列中,取走前 ii 个矩阵的最小花费,ww 为宽(单调递增),hh 为高(单调递减),那么有转移方程:

fi=minj=0i{fj+wihj+1}f_i=\min_{j=0}^{i}{\{f_{j}+w_ih_{j+1}\}}

相当于,把 j+1j+1ii 这一段合为一组取走。由于单调性,所以这一组的 max{w}=wi\max{\{w\}}=w_imax{h}=hj+1\max{\{h\}}=h_{j+1}.

复杂度 O(n2)\mathcal{O}(n^2)。这显然可以斜率优化。

斜率优化#

我们目前优化的目的,是尽快地找到 fif_i 到底是由哪个 jj 转移过来的,也就是尽快找到最小化 fif_i 的继承状态。

为了表示方便,设 jj 为能使 fif_i 最小化的继承状态,所以有:

fi=fj+wihj+1f_i=f_{j}+w_ih_{j+1}

考虑一次函数斜截式 y=kx+by=kx+bb=ykxb=y-kx,将状态转移方程变换为这个形式:

fj=wihj+1+fif_{j}=-w_ih_{j+1}+f_i

其中变量 x,yx, yjj 有关,b,kb, kjj 无关。且要求 xxjj 单调递增,bb 中包含 fif_i(斜率优化基本操作)。

所以可以构造出直线方程为:

\left\{ \begin{array}{**lr**} y = f_j\\ k = w_i\\ x = -h_{j+1}\\ b = f_i \end{array} \right.

xxjj 单调递增,kkii 单调递增,要使截距 b=fib=f_i 最小,最优解 jj 就是从左往右枚举到的第一个 slope(j,j+1)>k\operatorname{slope}(j, j+1)>k 的点

所以单调队列维护下凸包即可。

代码#

#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

总结?#

如果转移方程能表示成:fi=maxj<i{aibj+cj+di}f_i=\max_{j<i}{\{a_ib_j+c_j+d_i\}} 的形式(或 min\min),并且 aia_i 关于 ii 有单调性,就都可以用斜率优化。

斜率优化能把 O(n2)\mathcal{O}(n^2) 降成 O(n)\mathcal{O}(n).

数形结合。

代码短。

斜率优化
https://www.tonyyin.top/blog/oi-notes/dp-opt-slope
Author TonyYin
Published at February 26, 2021
Comment seems to stuck. Try to refresh?✨