斜率优化DP

斜率优化DP

例题

HNOI2008 玩具装箱

朴素dp

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

状态转移方程为:

dpi=minj<i{dpj+(i(j+1)+sisjL)2}=minj<i{dpj+(sisj+ij1L)2}

其中 prei 表示前 i 个数的和,即 j=1icj

该做法时间复杂度为 O(n2),直接爆炸。

优化

为方便表示,令 L=L+1

我们将式子改写为如下形式:

dpi=dpj+(sisj+ijL)2dpi=dpj+((si+iL)+(sjj))2dpi=dpj+(si+iL)2+2(si+iL)(sjj)+(sjj)2dpi(si+iL)2=dpj+(sj+j)22(si+iL)(sj+j)

考虑一次函数式子 y=kx+b,将其移项得到 b=ykx。我们将只与 j 有关的信息表示为 y,把同时与 i,j 有关的信息表示为 kx(其中与 i 相关的作 k,与 j 相关的作 x),把只与 i 有关的信息(要求的信息)表示为 b具体地,设

bi=dpi(si+iL)2ki=2(si+iL)yj=dpj+(sj+j)2xj=sj+j

则转移方程 dpi(si+iL)2=dpj+(sj+j)22(si+iL)(sj+j) 就可以写作

bi=minj<i{yjkixj}

我们把 (xj,yj) 看作二维平面上的点,ki 表示直线斜率, bi 表示一条经过 (xj,yj) 的斜率ki 的直线的截距。问题转化为了:选择合适的 j (1j<i),最小化直线的截距(即 bi

如图,我们将这个斜率为 ki 的直线从下往上平移,直到有一个点 (xp,yp) 在这条直线上,则有 bi=ypkixp,这时 bi 取到最小值。算完 fi,我们就把这个点 (xi,yi) 这个点加入点集中,以做为新的 dp 决策。那么,我们该如何维护点集?

容易发现,可能让 bi 取到最小值的点一定在凸壳上。因此在寻找 p 的时候我们就不需要枚举所有 i1 个点,只需要考虑凸包上的点。在本题中 ki 随着 i 的增加而增加,因此我们可以单调队列维护凸包

具体地,设 K(a,b) 表示过 (xa,ya)(xb,yb) 的直线的斜率。考虑单调队列 ql,ql+1,,qr,维护的时下凸包上的点。也就是说,对于 l<i<r始终有 K(qi1,qi)<K(qi,qi+1) 成立。

我们维护一个指针 e 来计算 bi 的最小值。我们需要找到一个 K(qe1,qe)ki<K(qe,qe+1)e,这时就有 p=qe,即 qei 的最优决策点。

在插入一个点 (xi,yi) 时,我们要判断是否 K(qr1,qr)<K(qr,i),如果不等式不成立就将 qr 弹出,直到等式满足。然后将 i 插到 q 队尾。

代码实现

  1. 初始状态入队
  2. 每次使用一条和 i 相关的直线 f(i) 去切维护的凸包,找到最优决策,更新 dpi
  3. 加入状态 dpi。如果一个状态(即凸包上的一个点)在 dpi 加入后不再是凸包上的点,需要在 dpi 加入前将其剔除。

时间复杂度 O(n)

#include <bits/stdc++.h>
#define int long long

using namespace std;

const int INF = 0x3f3f3f3f;
const int N = 5e4 + 10;
int n, L;
int c[N], s[N];
int dp[N], q[N], head = 1, tail = 0;
int b(int i) {return dp[i] - (s[i] + i - L - 1) * (s[i] + i - L - 1); } // 定义 b
int k(int i) {return 2 * (s[i] + i - L - 1); }							// 定义 k
int y(int j) {return dp[j] + (s[j] + j) * (s[j] + j); }					// 定义 y
int x(int j) {return s[j] + j; }										// 定义 x
double slope(int i, int j) {											// 定义 K(a, b)
	return 1.0 * (y(i) - y(j)) / (x(i) - x(j));
}
signed main() {
	scanf("%lld%lld", &n, &L);
	for (int i = 1; i <= n; i++) {
		scanf("%lld", &c[i]);
		s[i] = s[i - 1] + c[i];
	}
	memset(dp, INF, sizeof(dp));
	dp[0] = 0;
	q[++tail] = 0;
	for (int i = 1; i <= n; i++) {
		while (head < tail && slope(q[head], q[head + 1]) < k(i)) { // 改点不可能是最优决策点,出队
			head++;
		}
		dp[i] = y(q[head]) - k(i) * x(q[head]) + (s[i] + i - L - 1) * (s[i] + i - L - 1);
		while (head < tail && slope(q[tail - 1], q[tail]) > slope(q[tail], i)) { // 若该点不再是凸壳上的点,则将其踢出队列
			tail--;
		}
		q[++tail] = i;
	}
	printf("%lld\n", dp[n]);
	
	return 0;
}

小结

具体的代码实现因题就题,比较抽象,要考虑是上凸包还是下凸包,事踢对头还是踢队尾之类的。注意上凸包斜率越来越小,下凸包斜率越来越大

CDQ优化

一般用不上,但是放在这里。

对于有些问题,斜率并不是单调的。这时我们需要维护凸包上的每一个节点,然后每次用当前的直线去切这个凸包。

本题与「玩具装箱」问题唯一的区别是,玩具的价值可以为负。延续之前的思路,设 dpi 表示前 i 个物品,分若干段的最小代价。

状态转移方程为:

dpi=minj<i{dpj+(sisj+ij1L)2}

其中 prei 表示前 i 个数的和,即 j=1icj

将方程做相同的变换:

dpi(si+iL)2=minj<i{dpj+(sj+j)22(si+iL)(sj+j)}

然而这时有两个条件不成立了:

  1. 直线的斜率不再单调;
  2. 每次加入的决策点的横坐标不再单调。

仍然考虑凸壳的维护。

在寻找最优决策点,也就是用直线切凸壳的时候,我们将单调队列找队首改为:凸壳上二分。我们二分出斜率最接近直线斜率的那条凸壳边,就可以找到最优决策。

在加入决策点,也就是凸壳上加一个点的时候,我们有两种方法维护。

第一种方法是直接用平衡树维护凸壳。那么寻找决策点的二分操作就转化为在平衡树上二分,插入决策点就转化为在平衡树上插入一个结点,并删除若干个被踢出凸壳的点。此方法思路简洁但实现繁琐。

来说一种基于 CDQ 分治的做法。

CDQ(l,r) 表示计算 fi,i[l,r]。考虑 CDQ(1,N)

  • 我们先调用 CDQ(1,mid) 算出 fi,i[1,mid]。然后我们对 [1,mid] 这个区间内的决策点建凸包,然后用这个凸包去更新 fi,i[mid+1,n]。这时我们决策点集是固定的,不像之前那样边计算 DP 值边加入决策点,那么我们就可以把 i[mid+1,n]fi 先按照直线的斜率 ki 排序,然后就可以使用单调队列来计算 DP 值了。
  • 对于 [mid+1,n] 中的每个点,如果它的最优决策的位置是在 [1,mid] 这个区间,在这一步操作中他就会被更新成最优答案。当执行完这一步操作时,我们发现 [1,mid] 中的所有点已经发挥了全部的作用,凸壳中他们存不存在已经不影响之后的答案更新。因此我们可以直接舍弃这个区间的决策点,并使用 CDQ(mid+1,n) 解决右区间剩下的问题。

时间复杂度 O(nlog2n)

小结

对比「玩具装箱」和「玩具装箱 改」,可以总结出以下两点:

  • 二分/CDQ/平衡树等能够优化 DP 方程的计算,于一定程度上降低复杂度,但不能改变这个方程本身。
  • DP 方程的性质会取决于数据的特征,但 DP 方程本身取决于题目中的数学模型。

  1. 一个点 (x,y) 的斜率为 yx↩︎

posted @   Zctf1088  阅读(57)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 单线程的Redis速度为什么快?
· 展开说说关于C#中ORM框架的用法!
· Pantheons:用 TypeScript 打造主流大模型对话的一站式集成库
· SQL Server 2025 AI相关能力初探
· 为什么 退出登录 或 修改密码 无法使 token 失效
点击右上角即可分享
微信分享提示