2.3 动态规划1
一、基本概念
1.状态
状态定义:完全表示当前状态,无冗余信息
性质:最优子结构,无后效性
状态转移:利用前面的状态通过某一转移决策推出当前状态的过程
状态转移方程:转移决策
2.实现方式
(1)记忆化搜索
主要思想:在函数递归时如果出现了已经得到过的函数值,不妨开辟新数组把它存储下来,再次传入相同参数时,就可以直接获取数组中的值,减少不必要的时间消耗(用空间换时间)
使用条件:
- 函数返回结果只与参数有关
- 同样参数的函数会被多次调用
一般使用函数的递归调用进行求解
(2)递推
主要思想:把复杂问题转化为简单问题的多次重复,一般要对问题推导递推关系式求解
一般使用数组进行求解
3.典例
(1)数字三角形 ( I O I 1994 ) (IOI1994) (IOI1994)
定义 f ( i , j ) f(i,j) f(i,j)为走到 ( i , j ) (i,j) (i,j)的最大值
状态转移方程:
f ( i , j ) = m a x ( f ( i − 1 , j ) , f ( i − 1 , j − 1 ) ) + a ( i , j ) f(i,j)=max(f(i-1,j),f(i-1,j-1))+a(i,j) f(i,j)=max(f(i−1,j),f(i−1,j−1))+a(i,j)
(2)数字三角形 拓展 1 1 1
在原题基础上,变为求和 m o d 100 mod\space 100 mod 100的最大值
若按原来的状态定义,无法满足最优子结构
思考:如何将 m o d 100 mod\space 100 mod 100定义在状态中?
定义 f ( i , j , k ) f(i,j,k) f(i,j,k)表示从起点走到 ( i , j ) (i,j) (i,j),能否满足结果为 k k k,状态转移方程:
f ( i , j , k ) = f ( i − 1 , j , k − a ( i , j ) ) ∣ f ( i − 1 , j − 1 , k − a ( i , j ) ) f(i,j,k)=f(i-1,j,k-a(i,j))|f(i-1,j-1,k-a(i,j)) f(i,j,k)=f(i−1,j,k−a(i,j))∣f(i−1,j−1,k−a(i,j))
注意这里 k − a ( i , j ) k-a(i,j) k−a(i,j)的计算结果要对 100 100 100取模
(3)数字三角形 拓展 2 2 2
在原题基础上,必须经过某个点,求最大值
两种做法:
-
将经过点所在行的其他点权值设为 − ∞ -\infty −∞
-
从经过点分别往上下两个方向进行DP,得到的结果相加
(4)最长上升子序列 ( L I S ) (LIS) (LIS)
定义 f ( i ) f(i) f(i)表示以 i i i结尾的 L I S LIS LIS长度
状态转移方程:
f ( i ) = { f ( i − 1 ) + 1 , a ( i ) > a ( i − 1 ) m a x ( f ( i − 1 ) , f ( j ) + 1 ) , a ( i ) ≤ a ( i − 1 ) , j < i , a ( j ) < a ( i ) f(i)= \begin{cases} f(i-1)+1,&a(i)>a(i-1)\\ max(f(i-1),f(j)+1),&a(i)\leq a(i-1),j<i,a(j)<a(i) \end{cases} f(i)={f(i−1)+1,max(f(i−1),f(j)+1),a(i)>a(i−1)a(i)≤a(i−1),j<i,a(j)<a(i)
(5)最长公共子序列 ( L C S ) (LCS) (LCS)
定义 f ( i , j ) f(i,j) f(i,j)表示 a 1 , a 2 , ⋯ , a i a_1,a_2,\cdots,a_i a1,a2,⋯,ai和 b 1 , b 2 , ⋯ , b j b_1,b_2,\cdots,b_j b1,b2,⋯,bj的 L C S LCS LCS长度
状态转移方程:
f ( i , j ) = { f ( i − 1 , j − 1 ) + 1 , a ( i ) = a ( j ) m a x ( f ( i − 1 , j ) , f ( i , j − 1 ) ) , a ( i ) ≠ a ( j ) f(i,j)= \begin{cases} f(i-1,j-1)+1,&a(i)=a(j)\\ max(f(i-1,j),f(i,j-1)),&a(i)\neq a(j) \end{cases} f(i,j)={f(i−1,j−1)+1,max(f(i−1,j),f(i,j−1)),a(i)=a(j)a(i)=a(j)
二、区间 D P DP DP
1.朴素(线形)
例题:石子合并
- 在一条公路上摆放 n n n堆石子,现要将石子有次序地合并成一堆,规定每次只能选相邻的 2 2 2堆合并成新的一堆,并将新的一堆的石子数,记为该次合并的得分,求将 n n n堆石子合并为 1 1 1堆后,得分的最小值。
定义 f ( l , r ) f(l,r) f(l,r)表示从 l l l合并到 r r r的最小代价
首先预处理出前缀和数组 s u m sum sum
对于区间 ( l , r ) (l,r) (l,r),只有可能是由两个区间 ( l , k ) (l,k) (l,k)和 ( k + 1 , r ) (k+1,r) (k+1,r)合并而来
状态转移方程:
f ( l , r ) = m i n { f ( l , k ) + f ( k + 1 , r ) + s u m [ r ] − s u m [ l − 1 ] } , l ≤ k < r f(l,r)=min\{f(l,k)+f(k+1,r)+sum[r]-sum[l-1]\},l\leq k<r f(l,r)=min{f(l,k)+f(k+1,r)+sum[r]−sum[l−1]},l≤k<r
注意:在 D P DP DP时,需要先枚举区间长度,再根据每一个 l l l,求出对应的 r r r
2.环形
在原题基础上,结构由线形变为环形(即第 1 1 1堆能与第 n n n堆合并)
首先通过分析,不难发现进行 n − 1 n-1 n−1次合并后就能合并为一堆,而环形中间存在 n n n个合并位置,我们进行如下操作:
以 5 5 5个数 a , b , c , d , e a,b,c,d,e a,b,c,d,e为例,首先将其加倍,变为 a , b , c , d , e , a , b , c , d , e a,b,c,d,e,a,b,c,d,e a,b,c,d,e,a,b,c,d,e
对于每种合并方式,不难发现就是如下五种情况中的朴素做法的最小值
a b c d e a b c d e 1. a b c d e 2. b c d e a 3. c d e a b 4. d e a b c 5. e a b c d \begin{matrix} &a&b&c&d&e&a&b&c&d&e&\\ 1.&a&b&c&d&e&&&&&&\\ 2.&&b&c&d&e&a&&&&&\\ 3.&&&c&d&e&a&b&&&&\\ 4.&&&&d&e&a&b&c&&&\\ 5.&&&&&e&a&b&c&d&&\\ \end{matrix} 1.2.3.4.5.aabbbccccdddddeeeeeeaaaaabbbbcccdde
三、背包 D P DP DP
有 n n n件物品和一个容量为 m m m的背包。物品 i i i的体积是 v i v_i vi,价值是 w i w_i wi。求解将哪些物品装入背包可使价值总和最大
1. 01 01 01背包问题
01 01 01背包问题特点:每种物品仅有一件
状态表示:定义 f ( i , j ) f(i,j) f(i,j)表示只从前 i i i个物品中选择,总体积不超过 j j j的最大价值,最终答案为 f ( n , m ) f(n,m) f(n,m)
状态转移: f ( i , j ) f(i,j) f(i,j)可以分为不选物品 i i i和选物品 i i i两种情况
状态转移方程:
f
(
i
,
j
)
=
m
a
x
(
f
(
i
−
1
,
j
)
,
f
(
i
−
1
,
j
−
v
i
)
+
w
i
)
f(i,j)=max(f(i-1,j),f(i-1,j-v_i)+w_i)
f(i,j)=max(f(i−1,j),f(i−1,j−vi)+wi)
int f[N][N];
for(int i=1;i<=n;i++){
for(int j=1;j<=m;j++){
f[i][j]=f[i-1][j];
if(j>=v[i]) f[i][j]=max(f[i][j],f[i-1][j-v[i]]+w[i]);
}
}
优化:从二维变成一维
注意:这里 j j j从大到小迭代,保证 f ( j ) f(j) f(j)在 f ( j − v i ) f(j-v_i) f(j−vi)改变前改变,从而使当前的f[j-v[i]]表示i-1时的状态
int f[N];
for(int i=1;i<=n;i++)
for(int j=m;j>=v[i];j++)
f[j]=max(f[j],f[j-v[i]]+w[i]);
2.完全背包问题
完全背包问题特点:每种物品有无数件
思路:类比 01 01 01背包
状态表示:定义 f ( i , j ) f(i,j) f(i,j)表示只从前 i i i个物品中选择,总体积不超过 j j j的最大价值
状态转移: f ( i , j ) f(i,j) f(i,j)可以分为不选物品 i i i和选 1 1 1 ~ ⌊ j v i ⌋ \lfloor\frac{j}{v_i}\rfloor ⌊vij⌋个物品 i i i两种情况
等价变形: f ( i , j ) f(i,j) f(i,j)可以分为选 0 0 0 ~ ⌊ j v i ⌋ \lfloor\frac{j}{v_i}\rfloor ⌊vij⌋个物品 i i i的情况
状态转移方程:
f
(
i
,
j
)
=
m
a
x
(
f
(
i
,
j
)
,
f
(
i
−
1
,
j
−
k
v
i
)
+
k
w
i
)
(
k
∈
[
0
,
⌊
j
v
i
⌋
]
,
k
∈
Z
)
f(i,j)=max(f(i,j),f(i-1,j-kv_i)+kw_i)\quad(k\in [0,\lfloor\frac{j}{v_i}\rfloor],k\in Z)
f(i,j)=max(f(i,j),f(i−1,j−kvi)+kwi)(k∈[0,⌊vij⌋],k∈Z)
int f[N][N];
for(int i=1;i<=n;i++)
for(int j=0;j<=m;j++)
for(int k=0;k*v[i]<=j;k++)
f[i][j]=max(f[i][j],f[i-1][j-k*v[i]]+k*w[i]);
优化:对于 f ( i , j ) f(i,j) f(i,j)和 f ( i , j − v i ) f(i,j-v_i) f(i,j−vi)而言:
f ( i , j ) = m a x ( f ( i − 1 , j ) , f ( i − 1 , j − v i ) + w i , f ( i − 1 , j − 2 v i ) + 2 w i , ⋯ ) f(i,j)=max(f(i-1,j),f(i-1,j-v_i)+w_i,f(i-1,j-2v_i)+2w_i,\cdots) f(i,j)=max(f(i−1,j),f(i−1,j−vi)+wi,f(i−1,j−2vi)+2wi,⋯)
f ( i , j − v i ) = m a x ( f ( i − 1 , j − v i ) , f ( i − 1 , j − 2 v i ) + w i , f ( i − 1 , j − 3 v i ) + 2 w i , ⋯ ) f(i,j-v_i)=max(f(i-1,j-v_i),f(i-1,j-2v_i)+w_i,f(i-1,j-3v_i)+2w_i,\cdots) f(i,j−vi)=max(f(i−1,j−vi),f(i−1,j−2vi)+wi,f(i−1,j−3vi)+2wi,⋯)
我们惊奇的发现: f ( i , j − v i ) f(i,j-v_i) f(i,j−vi)的表达式与 f ( i , j ) f(i,j) f(i,j)的表达式除第一项以外只差 w i w_i wi
于是将状态转移方程改写为:
f
(
i
,
j
)
=
m
a
x
(
f
(
i
−
1
,
j
)
,
f
(
i
,
j
−
v
i
)
+
w
i
)
f(i,j)=max(f(i-1,j),f(i,j-v_i)+w_i)
f(i,j)=max(f(i−1,j),f(i,j−vi)+wi)
int f[N][N];
for(int i=1;i<=n;i++)
for(int j=0;j<=m;j++){
f[i][j]=f[i-1][j];
if(j>=w[i]) f[i][j]=max(f[i][j],f[i][j-v[i]]+w[i]);
}
再优化:从二维变成一维
注意:这里 j j j不需要从大到小迭代,因为要让 f ( j − v i ) f(j-v_i) f(j−vi)在 f ( j ) f(j) f(j)改变前改变,从而保证物品 i i i可以购买多件
int f[N];
for(int i=1;i<=n;i++)
for(int j=v[i];j<=m;j++)
f[j]=max(f[j],f[j-v[i]]+w[i]);
3.多重背包问题
多重背包问题特点:每种物品有件数限制
额外定义物品 i i i有 s i s_i si件
状态表示:定义 f ( i , j ) f(i,j) f(i,j)表示只从前 i i i个物品中选择,总体积不超过 j j j的最大价值
状态转移: f ( i , j ) f(i,j) f(i,j)可以分为选 0 0 0 ~ m i n ( s i , ⌊ j v i ⌋ ) min(s_i,\lfloor\frac{j}{v_i}\rfloor) min(si,⌊vij⌋)个物品 i i i的情况
状态转移方程:
f
(
i
,
j
)
=
m
a
x
(
f
(
i
,
j
)
,
f
(
i
−
1
,
j
−
k
v
i
)
+
k
w
i
)
(
k
∈
[
0
,
m
i
n
(
s
i
,
⌊
j
v
i
⌋
)
]
,
k
∈
Z
)
f(i,j)=max(f(i,j),f(i-1,j-kv_i)+kw_i)\quad(k\in [0,min(s_i,\lfloor\frac{j}{v_i}\rfloor)],k\in Z)
f(i,j)=max(f(i,j),f(i−1,j−kvi)+kwi)(k∈[0,min(si,⌊vij⌋)],k∈Z)
时间复杂度: O ( n 3 ) O(n^3) O(n3)
for(int i=1;i<=n;i++)
for(int j=0;j<=m;j++)
for(int k=0;k<=s[i]&&k*v[i]<=j;k++)
f[i][j]=max(f[i][j],f[i-1][j-k*v[i]]+k*w[i]);
优化:转化为 01 01 01背包问题
方法:二进制拆分
假设物品 i i i有 1000 1000 1000件,则可以选择的数量有 0 , 1 , 2 , ⋯ , 1000 0,1,2,\cdots,1000 0,1,2,⋯,1000,我们将物品按 1 , 2 , 4 , ⋯ , 256 , 489 ( 1,2,4,\cdots,256,489( 1,2,4,⋯,256,489(此处为 1000 − 511 ) 1000-511) 1000−511)件进行“打包”,则可将问题转化为“打包”后新 ⌈ log n ⌉ \lceil \log n\rceil ⌈logn⌉个物品的01背包问题,时间复杂度 O ( n 3 ) O(n^3) O(n3)变为 O ( n 2 log n ) O(n^2\log n) O(n2logn)
//这里注意N至少要开到n*log(s)
int v[N],w[N];
int f[N];
int n,m,cnt;
//边读边打包
for(int i=1;i<=n;i++){
int a,b,s;
scanf("%d%d%d",&a,&b,&s);
//打包存储,cnt记录打包后物品总数量
int k=1;
while(k<=s){
cnt++;
v[cnt]=a*k;
w[cnt]=b*k;
s-=k;
k*=2;
}
//剩余不足2^(k+1)个物品额外打包
if(s>0){
cnt++;
v[cnt]=a*s;
w[cnt]=b*s;
}
}
//物品个数为cnt,背包容量为m的01背包问题
for(int i=1;i<=cnt;i++)
for(int j=m;j>=v[i];j--)
f[j]=max(f[j],f[j-v[i]]+w[i]);
利用单调队列优化多重背包,时间复杂度可以达到 O ( n 2 ) O(n^2) O(n2)
4.分组背包问题
分组背包问题特点:物品分为多组,每组内有若干个,每组只能选一个
状态表示:定义 f ( i , j ) f(i,j) f(i,j)表示只从前 i i i组物品中选择,总体积不超过 j j j的最大价值
状态转移: f ( i , j ) f(i,j) f(i,j)可以分为不从第 i i i组物品中选和从第 i i i组物品中选第 k k k个物品两种情况
状态转移方程:
f
(
i
,
j
)
=
m
a
x
(
f
(
i
−
1
,
j
)
,
f
(
i
−
1
,
j
−
v
(
i
,
k
)
)
+
w
(
i
,
k
)
)
f(i,j)=max(f(i-1,j),f(i-1,j-v_{(i,k)})+w_{(i,k)})
f(i,j)=max(f(i−1,j),f(i−1,j−v(i,k))+w(i,k))
优化:从二维变成一维(类比 01 01 01背包, j j j从大到小遍历)
int n,m;//n组物品,容量为m
int v[N][N],w[N][N],s[N];//s[i]表示第i组物品的件数
int f[N];
for(int i=1;i<=n;i++)
for(int j=m;j>=0;j--)
for(int k=1;k<=s[i];k++)
if(v[i][k]<=j)
f[j]=max(f[j],f[j-v[i][k]]+w[i][k]);
四、树形 D P DP DP
1.树的相关知识
树的直径:树上的最长链
求解方法:
( 1 ) (1) (1)两次 D F S DFS DFS
- 任取树上一点 P P P,进行一次 D F S DFS DFS,找到离 P P P最远的点 Q Q Q
- 再以点 Q Q Q为起点进行一次 D F S DFS DFS,找到离 Q Q Q最远的点 W W W,此时 Q Q Q与 W W W间的距离就是树的直径
( 2 ) (2) (2)树形 D P DP DP
对于每个节点我们需要记录两个值:
- f 1 ( i ) f1(i) f1(i)表示以 i i i为根的子树中 i i i到叶子节点的最大值
- f 2 ( i ) f2(i) f2(i)表示以 i i i为根的子树中 i i i到叶子节点的次大值
更新时,考虑贪心,先看是否能更新 f 1 f1 f1的值,再看是否能更新 f 2 f2 f2的值
- 若 f 1 ( i ) < f 1 ( j ) + w ( i , j ) f1(i)< f1(j)+w(i,j) f1(i)<f1(j)+w(i,j),则 f 2 ( i ) = f 1 ( i ) , f 1 ( i ) = f 1 ( j ) + w ( i , j ) f2(i)= f1(i),f1(i)= f1(j)+ w(i,j) f2(i)=f1(i),f1(i)=f1(j)+w(i,j)
- 否则,若 f 2 ( i ) < f 1 ( j ) + w ( i , j ) f2(i)< f1(j)+w(i,j) f2(i)<f1(j)+w(i,j),则 f 2 ( i ) = f 1 ( j ) + w ( i , j ) f2(i)=f1(j)+w(i,j) f2(i)=f1(j)+w(i,j)
最终的结果: m a x { f 1 ( i ) + f 2 ( i ) } max\{f1(i)+f2(i)\} max{f1(i)+f2(i)}
树的重心:删掉某个点后剩下的最大连通块大小最小
对于删掉某个节点,剩下的连通块大小分别是:
- 以删掉点的儿子为根的子树大小
- 整棵树大小减去以删掉点为根节点的子树大小