动态规划的优化
一、空间优化
说明
动态规划空间优化为滚动数组优化,即对于一个多维数组,转移时均是由上一阶段转移来的,则可以将这一维省略,以降低空间复杂度,但要注意转移时的顺序;
例题
0 - 1 背包问题
题目
有一个最多能装 m m m 千克的背包,有 n n n 件物品,它们的重量分别是 W 1 , W 2 , . . . , W n W_1,W_2,..., W_n W1,W2,...,Wn ,它们的价值分别是 C 1 , C 2 , . . . , C n C_1,C_2,... ,C_n C1,C2,...,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 \begin{cases} dp[i- 1][j] \; (j < w[i]) \\ dp[i - 1][j - w[i]] + c[i] \end{cases} dp[i][j]=max{dp[i−1][j](j<w[i])dp[i−1][j−w[i]]+c[i]
优化
在 d p [ i ] [ j ] dp[i][j] dp[i][j] 转移时,对于枚举物品这一维只用了 i i i 与 i − 1 i - 1 i−1 两个状态,在数组中即为,
dp | j − w [ i ] j - w[i] j−w[i] | … | j j j |
---|---|---|---|
i − 1 i - 1 i−1 | d p [ i − 1 ] [ j − w [ i ] ] dp[i - 1][j - w[i]] dp[i−1][j−w[i]] | … | d p [ i − 1 ] [ j ] dp[i - 1][j] dp[i−1][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[i−1] 这一维的状态转移过来的,则转移时只保存上一阶段的所有状态和当前阶段的所有状态即可,再利用这两个数组不断滚动计算,则可以得到最后一阶段的最后一个状态;
既可以优化为,
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[j−w[i]] ,如果 j j j 从小到大枚举,则 d p [ j − w [ i ] ] dp[j - w[i]] dp[j−w[i]] 则为当前状态 i i i 的,不是 i − 1 i - 1 i−1 的状态,所以 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)i−1{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(1≤N≤105) 只奶牛,已知每只奶牛的效率 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 i−1 只中第 i − 1 i - 1 i−1 只选与不选的最大值即可;
若选第 i i i 只,则枚举 j ( i − k ≤ j < i ) j (i - k \leq j < i) j(i−k≤j<i) ,选取 j ∼ i j \sim i j∼i 这一区间的奶牛;
状态转移方程即为,
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[i−1][0],dp[i−1][1]}dp[i][1]=j=i−kmaxi−1{dp[j][0]−sum[j]+sum[i]}
时间复杂度为
O
(
N
∗
N
)
O(N * N)
O(N∗N) ,考虑优化;
对于 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=i−ki−1{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
)
\begin{aligned} dp[i] &= dp[j] + q(i) + p(j) + a(i) * b(j) \\ dp[j] + p(j) &= -a(i) * b(j) + dp[i] + q(i) \\ \end{aligned}
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 i∼j ,其的费用为 ( j − i + ∑ k − i j C k − L ) 2 (j - i + \sum_{k - i}^{j} C_k - L)^2 (j−i+∑k−ijCk−L)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=1mini−1{dp[j]+(i−j−1+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
\begin{aligned} dp[i] & = dp[j] + (i - j - 1 + sum[i] − sum[j] − L)^2 \\ dp[i] & = dp[j] + ((i - sum[i]) - (sum[j] + j − L - 1))^2 \\ \end{aligned}
dp[i]dp[i]=dp[j]+(i−j−1+sum[i]−sum[j]−L)2=dp[j]+((i−sum[i])−(sum[j]+j−L−1))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]−L−1 ,即
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
\begin{aligned} dp[i] & = dp[j] + (A[i] - B[j])^2 \\ dp[i] & = dp[j] + A[i]^2 + B[j]^2 - 2 * A[i] * B[j] \\ dp[j] + B[j]^2 &= 2 * A[i] * B[j] + dp[i] - A[i]^2 \end{aligned}
dp[i]dp[i]dp[j]+B[j]2=dp[j]+(A[i]−B[j])2=dp[j]+A[i]2+B[j]2−2∗A[i]∗B[j]=2∗A[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]
2∗A[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] 2∗A[i] 的直线的截距的最小值;
即可通过单调队列维护一个下凸包;
先预处理出 A , B , s u m A, B, sum A,B,sum 数组;
再依次遍历每一个数,
先将队头两个点组成直线的斜率与 2 ∗ A [ i ] 2 * A[i] 2∗A[i] 比较,若队头两个点组成直线的斜率比 2 ∗ A [ i ] 2 * A[i] 2∗A[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;
}