斜率优化动态规划

原文链接

前言

斜率优化通常使用单调队列辅助进行实现,用于优化 D P DP DP 的时间复杂度,比较抽象,需要读者有较高的数学素养。

本文例题链接

适用范围

使用单调队列优化 D P DP DP ,通常可以解决型如: d p [ i ] = m i n ( f ( j ) ) + g ( i ) dp[i]=min(f(j))+g(i) dp[i]=min(f(j))+g(i) 的状态转移方程。其中 f ( i ) f(i) f(i) 是只关于 i i i 的函数, g ( j ) g(j) g(j) 是只关于 j j j 的函数。朴素的解决方法是在第二层循环中枚举 j j j 来实现最小值,时间复杂度为 O ( n 2 ) O(n^2) O(n2) 。可以使用单调队列来维护这个最小值实现 O ( n ) O(n) O(n) 的时间复杂度。

而斜率优化利用上述方法进行改进,实现对于型如: d p [ i ] = m i n ( f ( i , j ) ) + g ( i ) dp[i]=min(f(i,j))+g(i) dp[i]=min(f(i,j))+g(i) 的状态转移方程。对比第一种情况,可以发现函数 f f f 函数与两个值 i , j i,j i,j 都有关,简单地使用单调队列是无法优化的。这时候就开始引入主题斜率优化了。

下面结合一道例题来具体详解。题目来自于 H N O I 2008 HNOI2008 HNOI2008 省选题目。

题目大意

n n n 个数字 C C C,把它分为若干组,给出另一个数 L L L ,每组的花费为 ( i − j + ∑ k = i j C k − L ) 2 (i-j+\sum_{k=i}^jC_k-L)^2 (ij+k=ijCkL)2,总花费为所有组的花费之和。求最小总花费。

思路

先考虑朴素的 d p dp dp 做法。

d p [ i ] dp[i] dp[i] 为将前 i i i 个数字分组后的最小花费。求和可以考虑使用前缀和来优化,设前缀和数组为 p r e pre pre 。则状态转移方程可以写为:

d p [ i ] = M i n ( d p [ j ] + ( s u m [ i ] − s u m [ j ] ) + ( i − ( j + 1 ) ) − L ) 2 , 0 ≤ j < i ) dp[i]=Min(dp[j]+(sum[i]-sum[j])+(i-(j+1))-L)^2,0≤j<i) dp[i]=Min(dp[j]+(sum[i]sum[j])+(i(j+1))L)2,0ji)

即是:

d p [ i ] = M i n ( d p [ j ] + ( s u m [ i ] − s u m [ j ] + i − j − L − 1 ) 2 , 0 ≤ j < i ) dp[i]=Min(dp[j]+(sum[i]-sum[j]+i-j-L-1)^2,0≤j<i) dp[i]=Min(dp[j]+(sum[i]sum[j]+ijL1)2,0ji)

那么 s u m sum sum 数组可以初始化为:

for(int i = 1; i <= n; i++) {
	Quick_Read(val[i]);
	sum[i] = sum[i - 1] + val[i];
}

p r e [ i ] = s u m [ i ] + i pre[i]=sum[i]+i pre[i]=sum[i]+i ,再进一步设 l = L + 1 l=L+1 l=L+1 那么状态转移方程可以写为:

d p [ i ] = M i n ( d p [ j ] + ( p r e [ i ] − p r e [ j ] − l ) 2 , 0 ≤ j < i ) dp[i]=Min(dp[j]+(pre[i]-pre[j]-l)^2,0≤j<i) dp[i]=Min(dp[j]+(pre[i]pre[j]l)2,0j<i)

状态转移

int Get_Dp(int i, int j) {
	return dp[j] + (pre[i] - pre[j] - l) * (pre[i] - pre[j] - l);
}

若枚举 j j j ,则时间复杂度为 O ( n ) 2 O(n)^2 O(n)2 ,时间复杂度不优。使用斜率优化可以对其进行优化。

假设当前枚举到 i i i ,需要得到 i i i 的状态。假设有两个决策点 j j j k k k ,满足决策点 j j j 优于决策点 k k k 。用符号语言可以表达为:

d p [ j ] + ( p r e [ i ] − p r e [ j ] − l ) 2 < d p [ k ] + ( p r e [ i ] − p r e [ k ] − l ) 2 dp[j]+(pre[i]-pre[j]-l)^2<dp[k]+(pre[i]-pre[k]-l)^2 dp[j]+(pre[i]pre[j]l)2<dp[k]+(pre[i]pre[k]l)2

展开得:

d p [ j ] + p r e [ i ] 2 + p r e [ j ] 2 + l 2 − 2 × p r e [ i ] × p r e [ j ] − 2 × l × p r e [ i ] + 2 × l × p r e [ j ] < d p [ k ] + p r e [ i ] 2 + p r e [ k ] 2 + l 2 − 2 × p r e [ i ] × p r e [ k ] − 2 × l × p r e [ i ] + 2 × l × p r e [ k ] dp[j]+pre[i]^2+pre[j]^2+l^2-2\times pre[i]\times pre[j]-2\times l\times pre[i]+2\times l\times pre[j]<dp[k]+pre[i]^2+pre[k]^2+l^2-2\times pre[i]\times pre[k]-2\times l\times pre[i]+2\times l\times pre[k] dp[j]+pre[i]2+pre[j]2+l22×pre[i]×pre[j]2×l×pre[i]+2×l×pre[j]<dp[k]+pre[i]2+pre[k]2+l22×pre[i]×pre[k]2×l×pre[i]+2×l×pre[k]

进一步整理得 :

d p [ j ] + p r e [ j ] 2 − d p [ k ] − p r e [ k ] 2 < ( p r e [ i ] − l ) × 2 × ( p r e [ j ] − p r e [ k ] ) dp[j]+pre[j]^2-dp[k]-pre[k]^2<(pre[i]-l)\times 2\times (pre[j] - pre[k]) dp[j]+pre[j]2dp[k]pre[k]2<(pre[i]l)×2×(pre[j]pre[k])

观察可得:左边的式子只与 j j j k k k 有关,但右边的式子还与 i i i 有关。也可以发现若满足上述式子,则会有 j j j 优于 k k k 。再分类讨论:

  1. j > k j>k j>k ,则 p r e [ j ] > p r e [ k ] pre[j]>pre[k] pre[j]>pre[k],移项得 d p [ j ] + p r e [ j ] 2 − ( d p [ k ] + p r e [ k ] 2 ) p r e [ j ] − p r e [ k ] < p r e [ i ] − l \frac{dp[j]+pre[j]^2-(dp[k]+pre[k]^2)}{pre[j] - pre[k]}<pre[i]-l pre[j]pre[k]dp[j]+pre[j]2(dp[k]+pre[k]2)<pre[i]l p r e [ i ] − l pre[i]-l pre[i]l 可以 看为一个常数。那么意味着点 j ( d p [ j ] + p r e [ j ] 2 , p r e [ j ] ) j(dp[j]+pre[j]^2,pre[j]) j(dp[j]+pre[j]2,pre[j]) 与点 k ( d p [ k ] + p r e [ k ] 2 , p r e [ k ] ) k(dp[k]+pre[k]^2,pre[k]) k(dp[k]+pre[k]2,pre[k]) 所构成的直线的斜率小于 p r e [ i ] − l pre[i]-l pre[i]l 这个常数。
  2. j < k j<k j<k ,则 p r e [ j ] < p r e [ k ] pre[j]<pre[k] pre[j]<pre[k],移项得 d p [ j ] + p r e [ j ] 2 − ( d p [ k ] + p r e [ k ] 2 ) p r e [ j ] − p r e [ k ] > p r e [ i ] − l \frac{dp[j]+pre[j]^2-(dp[k]+pre[k]^2)}{pre[j] - pre[k]}>pre[i]-l pre[j]pre[k]dp[j]+pre[j]2(dp[k]+pre[k]2)>pre[i]l p r e [ i ] − l pre[i]-l pre[i]l 可以 看为一个常数。那么意味着点 j ( d p [ j ] + p r e [ j ] 2 , p r e [ j ] ) j(dp[j]+pre[j]^2,pre[j]) j(dp[j]+pre[j]2,pre[j]) 与点 k ( d p [ k ] + p r e [ k ] 2 , p r e [ k ] ) k(dp[k]+pre[k]^2,pre[k]) k(dp[k]+pre[k]2,pre[k]) 所构成的直线的斜率大于 p r e [ i ] − l pre[i]-l pre[i]l 这个常数。

获得分子的函数:

int Get_Up(int j, int k) {
	return dp[j] + pre[j] * pre[j] - dp[k] - pre[k] * pre[k];
}

获得分母的函数:

int Get_Down(int j, int k) {
	return pre[j] - pre[k];
}

有了上述的一级结论,可以进一步推导出二级结论:
在这里插入图片描述
x , y x,y x,y 的斜率表示为 k ( x , y ) k(x,y) k(x,y) 。若存在三点 a , b , c a,b,c a,b,c ,有 k ( a , b ) > k ( b , c ) k(a,b)>k(b,c) k(a,b)>k(b,c) ,即是图像形成上凸的形状时,那么点 b b b 绝对不是最优的。

分类讨论:

  1. k ( a , b ) > k ( b , c ) > p r e [ i ] − l k(a,b)>k(b,c)>pre[i]-l k(a,b)>k(b,c)>pre[i]l ,则对于上述结论可以得出 a a a b b b 更优,舍去 b b b
  2. p r e [ i ] − l > k ( a , b ) > k ( b , c ) pre[i]-l>k(a,b)>k(b,c) pre[i]l>k(a,b)>k(b,c) ,则对于上述结论可以得出 c c c b b b 更优,舍去 b b b
  3. p r e [ i ] − l < k ( a , b ) pre[i]-l<k(a,b) pre[i]l<k(a,b) p r e [ i ] − l > k ( b , c ) pre[i]-l>k(b,c) pre[i]l>k(b,c) ,则对于上述结论可以得出 a a a c c c 都比 b b b 更优,舍去 b b b

那么就可以得出答案的点必须满足 k ( a 1 , a 2 ) < k ( a 2 , a 3 ) < . . . < k ( a m − 1 , a m ) k(a_1,a_2)<k(a_2,a_3)<...<k(a_{m-1},a_m) k(a1,a2)<k(a2,a3)<...<k(am1,am) 。全部呈现出下凸状态,如下图。
在这里插入图片描述
这样下标递增,斜率递增的点集可以使用单调队列来维护。

找出当前最优的点为 q u e [ h e a d ] que[head] que[head] ,即队头元素。

while(Get_Up(que[head + 1], que[head]) <= 2 * (pre[i] - l) * Get_Down(que[head + 1], que[head]) && head < tail)
	head++;

用当前点 i i i 来更新队列,使得该队列呈下凸之势。

while(Get_Up(que[tail], que[tail - 1]) * Get_Down(i, que[tail]) >= Get_Up(i, que[tail]) * Get_Down(que[tail], que[tail - 1]) && head < tail)
	tail--;

按照上述方法进行状态转移,得到的 d p [ n ] dp[n] dp[n] 就是当前的最优解。

C++代码

代码比较短,一气呵成。(注意要开 l o n g long long l o n g long long

#include <cstdio>
#define int long long
void Quick_Read(int &N) {
	N = 0;
	int op = 1;
	char c = getchar();
	while(c < '0' || c > '9') {
		if(c == '-')
			op = -1;
		c = getchar();
	}
	while(c >= '0' && c <= '9') {
		N = (N << 1) + (N << 3) + (c ^ 48);
		c = getchar();
	}
	N *= op;
}
void Quick_Write(int N) {
	if(N < 0) {
		putchar('-');
		N = -N;
	}
	if(N >= 10)
		Quick_Write(N / 10);
	putchar(N % 10 + 48);
}
const int MAXN = 5e5 + 5;
int dp[MAXN];
int pre[MAXN], val[MAXN];
int n, l;
int que[MAXN];
int head, tail;
int Get_Dp(int i, int j) {
	return dp[j] + (pre[i] - pre[j] - l) * (pre[i] - pre[j] - l);
}
int Get_Up(int j, int k) {
	return dp[j] + pre[j] * pre[j] - dp[k] - pre[k] * pre[k];
}
int Get_Down(int j, int k) {
	return pre[j] - pre[k];
}
void Line_Dp() {
	head = 1;
	tail = 1;
	for(int i = 1; i <= n; i++) {
		while(Get_Up(que[head + 1], que[head]) <= 2 * (pre[i] - l) * Get_Down(que[head + 1], que[head]) && head < tail)
			head++;
		dp[i] = Get_Dp(i, que[head]);
		while(Get_Up(que[tail], que[tail - 1]) * Get_Down(i, que[tail]) >= Get_Up(i, que[tail]) * Get_Down(que[tail], que[tail - 1]) && head < tail)
			tail--;
		que[++tail] = i;
	}
	Quick_Write(dp[n]);
}
void Read() {
	Quick_Read(n);
	Quick_Read(l);
	l++;
	for(int i = 1; i <= n; i++) {
		Quick_Read(val[i]);
		pre[i] = pre[i - 1] + val[i] + 1;
	}
}
signed main() {
	Read();
	Line_Dp();
	return 0;
}
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值