小言_互联网的博客

动态规划的优化

269人阅读  评论(0)

动态规划的优化

一、空间优化

说明

动态规划空间优化为滚动数组优化,即对于一个多维数组,转移时均是由上一阶段转移来的,则可以将这一维省略,以降低空间复杂度,但要注意转移时的顺序;

例题

0 - 1 背包问题

题目

有一个最多能装 m m m 千克的背包,有 n n n 件物品,它们的重量分别是 W 1 , W 2 , . . . , W n W_1,W_2,..., W_n W1W2...,Wn ,它们的价值分别是 C 1 , C 2 , . . . , C n C_1,C_2,... ,C_n C1C2...Cn

若每种物品只有一件,问能装入的最大总价值;

分析

状态

由于最终价值与物品与重量有关,所以可定义状态, d p [ i ] [ j ] dp[i][j] dp[i][j] 表示前 i i i 件物品装在容量为 j j j 的背包中所能装入的最大总价值;

转移

依次遍历每一个物品与背包容量,比较取与不取当前物品的价值;

状态转移方程

d p [ i ] [ j ] = max ⁡ { d p [ i − 1 ] [ j ]    ( j < w [ i ] ) d p [ i − 1 ] [ j − w [ i ] ] + c [ i ] dp[i][j] = \max {dp[i1][j](j<w[i])dp[i1][jw[i]]+c[i] dp[i][j]=max{ dp[i1][j](j<w[i])dp[i1][jw[i]]+c[i]

优化

d p [ i ] [ j ] dp[i][j] dp[i][j] 转移时,对于枚举物品这一维只用了 i i i i − 1 i - 1 i1 两个状态,在数组中即为,

dp j − w [ i ] j - w[i] jw[i] j j j
i − 1 i - 1 i1 d p [ i − 1 ] [ j − w [ i ] ] dp[i - 1][j - w[i]] dp[i1][jw[i]] d p [ i − 1 ] [ j ] dp[i - 1][j] dp[i1][j]
i i i d p [ i ] [ j ] dp[i][j] dp[i][j]

即在 d p [ i ] [ j ] dp[i][j] dp[i][j] 的转移中,均是通过 d p [ i − 1 ] dp[i - 1] dp[i1] 这一维的状态转移过来的,则转移时只保存上一阶段的所有状态和当前阶段的所有状态即可,再利用这两个数组不断滚动计算,则可以得到最后一阶段的最后一个状态;

既可以优化为,

d p [ j ] dp[j] dp[j]:容量为 j j j 的背包装下物品的价值的最大值;

但在计算 d p [ j ] dp[j] dp[j] 时,需要用到 d p [ j − w [ i ] ] dp[j - w[i]] dp[jw[i]] ,如果 j j j 从小到大枚举,则 d p [ j − w [ i ] ] dp[j - w[i]] dp[jw[i]] 则为当前状态 i i i 的,不是 i − 1 i - 1 i1 的状态,所以 j j j 应从大到小枚举;

依次遍历每一个物品与背包容量,比较取与不取当前物品的价值;

状态转移方程

$ dp[j] = \max {dp[j], dp[j - w[i]] + c[i] } $

代码

#include <cstdio>
#include <algorithm>
#define MAXN 20
#define MAXX 105
using namespace std;
int n, v, w[MAXN], c[MAXN], dp[MAXX];
int main() {
	scanf("%d %d", &n, &v);
	for (int i = 1; i <= n; i++) {
		scanf("%d %d", &w[i], &c[i]);
	}
	for (int i = 1; i <= n; i++) { // 遍历物品
		for (int j = v; j >= w[i]; j--) { // 反向枚举
			dp[j] = max(dp[j], dp[j - w[i]] + c[i]); // 状态转移
		}
	}
	printf("%d", dp[v]);
	return 0;
} 

 

二、单调队列优化

说明

单调队列就是队内元素保持一定单调性的队列,即从队首到队尾单调递增或递减的队列;

可借助单调队列的单调性及时排除不可能的决策,保持候选集合的有效性与秩序性;

即对于形如 d p [ i ] = max ⁡ j = a ( a < i ) i − 1 { d p [ j ] + x ( i , j ) } dp[i] = \max_{j = a(a < i)}^{i - 1}\{dp[j] + x(i, j)\} dp[i]=maxj=a(a<i)i1{ dp[j]+x(i,j)} 的状态转移方程,可使用单调队列优化,维护一个单调递增或递减的单调队列,将 d p dp dp 数组全部存入单调队列中;

但为了方便判断单调队列中的状态是否满足 j j j 的范围,一般在单调队列中存储下标;

对于 d p [ i ] dp[i] dp[i] 更新时,先将单调队列中不满足 j j j 的范围的状态从队列中删除,再利用单调队列的单调性选出对于 d p [ i ] dp[i] dp[i] 的最优 d p [ j ] dp[j] dp[j] ,用其更新 d p [ i ] dp[i] dp[i] ,再将 d p [ i ] dp[i] dp[i] 存入队列中继续更新;

例题

Mowing the Lawn G

题目

N ( 1 ≤ N ≤ 1 0 5 ) N (1 \leq N \leq 10^5) N(1N105) 只奶牛,已知每只奶牛的效率 E i E_i Ei ,选取一种方案,使该方案中没有连续的超过 K 只奶牛且奶牛的效率总和最大;

分析

状态

由于要求不能有连续 K K K 只奶牛存在,所以定义状态为,

d p [ i ] [ 0 / 1 ] dp[i][0/1] dp[i][0/1] 表示前 i i i 只奶牛选 (1) 与不选 (0) ;

转移

对于第 i i i 只奶牛,

若不选第 i i i 只,则比较前 i − 1 i - 1 i1 只中第 i − 1 i - 1 i1 只选与不选的最大值即可;

若选第 i i i 只,则枚举 j ( i − k ≤ j < i ) j (i - k \leq j < i) j(ikj<i) ,选取 j ∼ i j \sim i ji 这一区间的奶牛;

状态转移方程即为,
d p [ i ] [ 0 ] = max ⁡ { d p [ i − 1 ] [ 0 ] , d p [ i − 1 ] [ 1 ] } d p [ i ] [ 1 ] = max ⁡ j = i − k i − 1 { d p [ j ] [ 0 ] − s u m [ j ] + s u m [ i ] } dp[i][0] = \max\{dp[i - 1][0], dp[i - 1][1]\} \\ dp[i][1] = \max_{j = i - k}^{i - 1}\{dp[j][0] − sum[j] + sum[i]\} dp[i][0]=max{ dp[i1][0],dp[i1][1]}dp[i][1]=j=ikmaxi1{ dp[j][0]sum[j]+sum[i]}
时间复杂度为 O ( N ∗ N ) O(N * N) O(NN) ,考虑优化;

对于 d p [ i ] [ 1 ] dp[i][1] dp[i][1] 的方程,可将 s u m [ i ] sum[i] sum[i] 提出,得 d p [ i ] [ 1 ] = max ⁡ j = i − k i − 1 { d p [ j ] [ 0 ] − s u m [ j ] } + s u m [ i ] } dp[i][1] = \max_{j = i - k}^{i - 1}\{dp[j][0] − sum[j]\} + sum[i]\} dp[i][1]=maxj=iki1{ dp[j][0]sum[j]}+sum[i]} ,则可使用单调队列优化,维护一个递减单调队列;

依次遍历 n n n 个物品,若当前物品编号为 i i i 则,

先更新 d p [ i ] [ 0 ] dp[i][0] dp[i][0] 的值;

将单调队列存储的候选优解中下标不满足 j j j 范围的从单调队列中删除;

用单调队列的队首元素更新 d p [ i ] dp[i] dp[i] 的值;

将下标 i i i 存入单调队列中,但为维护单调队列的单调性,应先将单调队列队尾元素中 d p dp dp 值小于 d p [ i ] dp[i] dp[i] 的元素从队列中删除,再将下标 i i i 存入单调队列;

代码

#include <cstdio>
#include <algorithm>
#define MAXN 100005
using namespace std;
int n, k;
long long a[MAXN], sum[MAXN], dp[MAXN][2];
int q[MAXN], head = 1, tail = 1; // 单调队列
int main() {
	scanf("%d %d", &n, &k);
	for (int i = 1; i <= n; i++) {
		scanf("%lld", &a[i]);
		sum[i] = sum[i - 1] + a[i]; // 计算前缀和
	}
	for (int i = 1; i <= n; i++) {
		dp[i][0] = max(dp[i - 1][0], dp[i - 1][1]); // 更新 DP
		while (head <= tail && q[head] < i - k)	head++; // 去除单调队列中已无效的状态
		dp[i][1] = dp[q[head]][0] - sum[q[head]] + sum[i]; // 更新 DP
		while (head <= tail && dp[q[tail]][0] - sum[q[tail]] < (dp[i][0] - sum[i])) tail--; //维持点调性
		q[++tail] = i; // 存入新状态
	}
	printf("%lld", max(dp[n][0], dp[n][1]));
	return 0;
}

 

总结

更为一般地,对于可以单调队列优化的状态转移式均可写为以下形式,
d p [ i ] = min ⁡ j = L ( i ) R ( i ) { d p [ j ] + v a l ( i , j ) } dp[i] = \min_{j = L(i)}^{R(i)}\{dp[j] + val(i, j)\} dp[i]=j=L(i)minR(i){ dp[j]+val(i,j)}
此为动态规划的一类模型,被称为 1D/1D 的动态规划;

其中,

L ( i ) , R ( i ) L(i), R(i) L(i),R(i) 为关于 i i i 的一次函数,且有 R ( i ) ≤ i R(i) \leq i R(i)i ,限制了 j j j 的决策范围,保证其上下界变化具有单调性;

v a l ( i , j ) val(i, j) val(i,j) 为关于 i , j i, j i,j 的多项式函数;

回顾上题,将 v a l ( i , j ) = q ( i ) + p ( j ) val(i, j) = q(i) + p(j) val(i,j)=q(i)+p(j) 拆为了两个部分,一部分 q ( i ) q(i) q(i) 仅与 i i i 有关,另一部分 p ( j ) p(j) p(j) 仅与 j j j 有关,将 q ( i ) q(i) q(i) 提出 min ⁡ j = L ( i ) R ( i ) { } \min_{j = L(i)}^{R(i)}\{\} minj=L(i)R(i){ } 中,用单调队列维护 d p [ j ] + p ( j ) dp[j] + p(j) dp[j]+p(j) 的值;

所以在上述 1D/1D 的动态规划中,多项式 v a l ( i , j ) val(i, j) val(i,j) 是否能且仅能拆分为存在 q ( i ) , p ( j ) q(i), p(j) q(i),p(j) 两个部分是能否使用单调队列优化的基本条件;

d p dp dp 状态转移方程形如 d p [ i ] = min ⁡ j = L ( i ) R ( i ) { d p [ j ] + q ( i ) + p ( j ) } dp[i] = \min_{j = L(i)}^{R(i)}\{dp[j] + q(i) + p(j)\} dp[i]=minj=L(i)R(i){ dp[j]+q(i)+p(j)} 可用单调队列优化;

三、斜率优化

说明

在 1D/1D 的动态规划中,多项式 v a l ( i , j ) val(i, j) val(i,j) 若能拆分成 q ( i ) + p ( j ) + a ( i ) ∗ b ( j ) q(i) + p(j) + a(i) * b(j) q(i)+p(j)+a(i)b(j) 即可以使用动态规划的斜率优化;

其中, q ( i ) , a ( i ) q(i), a(i) q(i),a(i) 仅与 i i i 有关, p ( j ) , b ( j ) p(j), b(j) p(j),b(j) 仅与 j j j 有关;

d p dp dp 状态转移方程形如 d p [ i ] = min ⁡ j = L ( i ) R ( i ) { d p [ j ] + q ( i ) + p ( j ) + a ( i ) ∗ b ( j ) } dp[i] = \min_{j = L(i)}^{R(i)}\{dp[j] + q(i) + p(j) + a(i) * b(j)\} dp[i]=minj=L(i)R(i){ dp[j]+q(i)+p(j)+a(i)b(j)} 可用斜率优化;

即可继续方程式变形,
d p [ i ] = d p [ j ] + q ( i ) + p ( j ) + a ( i ) ∗ b ( j ) d p [ j ] + p ( j ) = − a ( i ) ∗ b ( j ) + d p [ i ] + q ( i ) dp[i]=dp[j]+q(i)+p(j)+a(i)b(j)dp[j]+p(j)=a(i)b(j)+dp[i]+q(i) dp[i]dp[j]+p(j)=dp[j]+q(i)+p(j)+a(i)b(j)=a(i)b(j)+dp[i]+q(i)
变形为 y = k x + b y = kx + b y=kx+b 的样式,其中 d p [ j ] + p ( j ) dp[j] + p(j) dp[j]+p(j) 即为 y y y b ( j ) b(j) b(j) 即为 x x x − a ( i ) -a(i) a(i) 即为 k k k d p [ i ] + q ( i ) dp[i] + q(i) dp[i]+q(i) 即为 b b b

则平面直角坐标系的一个点 ( b ( j ) , d p [ j ] + p ( j ) ) (b(j), dp[j] + p(j)) (b(j),dp[j]+p(j)) 仅与 j j j 有关,整个 d p dp dp 是一条直线,其斜率为 − a ( i ) -a(i) a(i) ,截距为 d p [ i ] + q ( i ) dp[i] + q(i) dp[i]+q(i) ,由于要求 d p [ i ] dp[i] dp[i] 最小,则应让一条过点 ( b ( j ) , d p [ j ] + p ( j ) ) (b(j), dp[j] + p(j)) (b(j),dp[j]+p(j)) 的直线截距 d p [ i ] + q ( i ) dp[i] + q(i) dp[i]+q(i) 最小;

由于在求解 d p [ i ] dp[i] dp[i] 值时, − a ( i ) -a(i) a(i) 仅与 i i i 有关,所以可以将其看作一个定值;

即求过点 ( b ( j ) , d p [ j ] + p ( j ) ) (b(j), dp[j] + p(j)) (b(j),dp[j]+p(j)) 的斜率为 − a ( i ) -a(i) a(i) 的直线的截距 d p [ i ] + q ( i ) dp[i] + q(i) dp[i]+q(i) 最小;

若图上的每个点均为一个 j j j 所对应的点,直线为 l l l ,则此时情况为,

则点 C C C 即为 d p [ i ] dp[i] dp[i] 对应的最优的 d p [ j ] dp[j] dp[j]

则此时可以发现, A , B , C A, B, C A,B,C 三个点共同组成了一个下凸包,即 K l ( A , C ) < K l ( C , B ) K_{l(A, C)} < K_{l(C, B)} Kl(A,C)<Kl(C,B) ,则中间点 C C C 即为最优解;

对于剩下的两个点,

对于 A A A 点,由于斜率不断增大, A A A点不可能用来转移后面的状态,所以将它删除;
对于 B B B 点,当斜率到达一定大小,例如下图,

此时最优解为 B B B ,而 C C C 又要删除;

所以只要维护一个下凸包,使得下凸包中相邻两个点连的斜率要大于当前这条线的斜率,正如上例;

可以使用单调队列维护,

一旦最左端的一个点和次左端的点的连线要小于当前的斜率了,就把最左端的点删除,每次遇到新的直线,直接拿最左端的点来转移;

加入一个新点 i i i 就加到最右边,由于横坐标也是递增的,在加入点 i i i 之前,一定要保证下凸的性质,先将 i i i 点与单调队列队尾元素组成直线 l 1 l_1 l1 的斜率与单调队列队尾两个元素组成的直线 l 2 l_2 l2 的斜率相比较;

如下图所示,

K l 1 K_{l_1} Kl1 大于 K l 2 K_{l_2} Kl2 ,则队尾元素一定不会成为最优解,则将队尾元素删除 ,再将下标 i i i 存入单调队列;

注意,比较斜率时,为防止卡精度可十字相乘转化为乘积形式,但若分母为负数,应该要变号;

例题

玩具装箱

题目

n n n 个数,每个数有一个值 C i C_i Ci ,将这 n n n 个数分为若干段,对于一段 i ∼ j i \sim j ij ,其的费用为 ( j − i + ∑ k − i j C k − L ) 2 (j - i + \sum_{k - i}^{j} C_k - L)^2 (ji+kijCkL)2 ,求最小的总费用;

分析

状态

d p [ i ] dp[i] dp[i] 表示前 i i i 个数总费用的最小值;

转移

通过题目费用计算方法可知状态转移方程为,
d p [ i ] = min ⁡ j = 1 i − 1 { d p [ j ] + ( i − j − 1 + s u m [ i ] − s u m [ j ] − L ) 2 } dp[i] = \min_{j = 1}^{i - 1}\{dp[j] + (i - j - 1 + sum[i] − sum[j] − L)^2\} dp[i]=j=1mini1{ dp[j]+(ij1+sum[i]sum[j]L)2}

优化

先转化状态转移方程,
d p [ i ] = d p [ j ] + ( i − j − 1 + s u m [ i ] − s u m [ j ] − L ) 2 d p [ i ] = d p [ j ] + ( ( i − s u m [ i ] ) − ( s u m [ j ] + j − L − 1 ) ) 2 dp[i]=dp[j]+(ij1+sum[i]sum[j]L)2dp[i]=dp[j]+((isum[i])(sum[j]+jL1))2 dp[i]dp[i]=dp[j]+(ij1+sum[i]sum[j]L)2=dp[j]+((isum[i])(sum[j]+jL1))2
为方便表示,令 A [ i ] = i + s u m [ i ] , B [ j ] = j + s u m [ j ] − L − 1 A[i] = i + sum[i], B[j] = j + sum[j] - L - 1 A[i]=i+sum[i],B[j]=j+sum[j]L1 ,即
d p [ i ] = d p [ j ] + ( A [ i ] − B [ j ] ) 2 d p [ i ] = d p [ j ] + A [ i ] 2 + B [ j ] 2 − 2 ∗ A [ i ] ∗ B [ j ] d p [ j ] + B [ j ] 2 = 2 ∗ A [ i ] ∗ B [ j ] + d p [ i ] − A [ i ] 2 dp[i]=dp[j]+(A[i]B[j])2dp[i]=dp[j]+A[i]2+B[j]22A[i]B[j]dp[j]+B[j]2=2A[i]B[j]+dp[i]A[i]2 dp[i]dp[i]dp[j]+B[j]2=dp[j]+(A[i]B[j])2=dp[j]+A[i]2+B[j]22A[i]B[j]=2A[i]B[j]+dp[i]A[i]2
其中 d p [ j ] + B [ j ] 2 dp[j] + B[j]^2 dp[j]+B[j]2 即为 y y y 2 ∗ A [ i ] 2 * A[i] 2A[i] 即为 k k k B [ j ] B[j] B[j] 即为 x x x d p [ i ] + A [ i ] 2 dp[i] + A[i]^2 dp[i]+A[i]2 即为 b b b

即求过 ( B [ j ] , d p [ j ] + B [ j ] 2 ) (B[j], dp[j] + B[j]^2) (B[j],dp[j]+B[j]2) ,斜率为 2 ∗ A [ i ] 2 * A[i] 2A[i] 的直线的截距的最小值;

即可通过单调队列维护一个下凸包;

先预处理出 A , B , s u m A, B, sum A,B,sum 数组;

再依次遍历每一个数,

先将队头两个点组成直线的斜率与 2 ∗ A [ i ] 2 * A[i] 2A[i] 比较,若队头两个点组成直线的斜率比 2 ∗ A [ i ] 2 * A[i] 2A[i] 小,则不能去,将其从对头删除;

用单调队列的队首元素更新 d p [ i ] dp[i] dp[i] 的值;

将下标 i i i 存入单调队列中,但为维护下凸包斜率的单调性,先将 i i i 点与单调队列队尾元素组成直线 l 1 l_1 l1 的斜率与单调队列队尾两个元素组成的直线 l 2 l_2 l2 的斜率相比较;

K l 1 K_{l_1} Kl1 应大于 K l 2 K_{l_2} Kl2 ,否则则 i i i 一定不会成为最优解则将队尾元素删除 ,再将下标 i i i 存入单调队列;

代码

#include <cstdio>
#include <algorithm>
#define MAXN 500005
using namespace std;
int n;
long long l, c[MAXN], sum[MAXN], dp[MAXN], a[MAXN], b[MAXN];
int head, tail, q[MAXN];
long long get_up(int x, int y) { // y 坐标差值
	return dp[x] + b[x] * b[x] - dp[y] - b[y] * b[y];
}
long long get_down(int x, int y) { // x 坐标差值
	return b[x] - b[y];
}
long long get_dp(int i, int j) { // 获取 dp 值
	return dp[j] + (a[i] - b[j]) * (a[i] - b[j]);
}
int main() {
	scanf("%d %lld", &n, &l);
	for (int i = 1; i <= n; i++) {
		scanf("%lld", &c[i]);
		sum[i] = sum[i - 1] + c[i]; // 预处理
		a[i] = sum[i] + i;
		b[i] = sum[i] + i + l + 1;
	}
	b[0] = l + 1;
	head = tail = 1; // 清空队列
	for (int i = 1; i <= n; i++) {
		while (head < tail && get_up(q[head + 1], q[head]) < 2 * a[i] * get_down(q[head + 1], q[head])) head++; // 通过斜率维护下凸
		dp[i] = get_dp(i, q[head]); // 更新 DP 值
		while (head < tail && get_up(i, q[tail]) * get_down(q[tail], q[tail - 1]) < get_down(i, q[tail]) * get_up(q[tail], q[tail - 1])) tail--; // 维护将 i 节点加入队列后的下凸
		q[++tail] = i; // 存入新的节点
	}
	printf("%lld\n", dp[n]);
	return 0;
}

 

转载:https://blog.csdn.net/ZhuRanCheng/article/details/128749175
查看评论
* 以上用户言论只代表其个人观点,不代表本网站的观点或立场