动态规划
动态规划(Dynamic Programming)算法的 核心思想 是:
- 将大问题划分为小问题进行解决,从而一步步获取最优解的处理算法
动态规划算法与分治算法类似,其基本思想也是将待求解问题分解成若干个子问题,先求解子问题,然后从这些子问题的解得到原问题的解
与分治法不同的是,适合于用动态规划求解的问题,经分解得到子问题往往不是互相独立的。 ( 即下一个子阶段的求解是建立在上一个子阶段的解的基础上,进行进一步的求解 )
动态规划可以通过填表的方式来逐步推进,得到最优解.
应用问题
-
待解决的原问题较难,但此问题可以被不断拆分成一个个小问题,而小问题的解是非常容易获得的;
-
如果单单只是利用递归的方法来解决原问题,那么采用的是分治法的思想,动态规划具有记忆性,将子问题的解都记录下来,以免在递归的过程中重复计算,从而减少了计算量
背包问题
问题
给定 n 个重量为 w 1 , w 2 , . . . , w n w_1, w_2, ..., w_n w1,w2,...,wn,价值为 v 1 , v 2 , . . . , v n v_1, v_2, ..., v_n v1,v2,...,vn 的物品和一个承重为 W 的背包,求这些物品中最有价值的一个子集,并且装到背包中。
动态规划分析
为了设计一个动态规划算法,需要分解问题,用部分物品组成的子问题的解来表示为背包问题的解:
-
首先,考虑一个由前 i i i 个物品( 1 ≤ i ≤ n 1 ≤ i ≤ n 1≤i≤n)定义的子问题,物品的重量分别为 w 1 , w 2 , . . . , w i w_1, w_2, ..., w_i w1,w2,...,wi,价值分别为 v 1 , v 2 , . . . , v i v_1, v_2, ..., v_i v1,v2,...,vi,背包的承重为 j j j( 1 ≤ j ≤ W 1 ≤ j ≤ W 1≤j≤W);
-
设 F ( i , j ) F(i, j) F(i,j) 是该子问题的最优解的物品总价值,也就是,能够放进承重为 j j j 的背包中的前 i i i 个物品中最有价值子集的总价值;
-
把前 i i i 个物品能够放入承重为 j j j 的背包中的子集分为两个类别:包括第 i i i 个物品的子集和不包括第 i i i 个物品的子集,从而有:
-a-
根据定义,在不包括第 i i i 个物品的子集中,最优子集的价值是 F ( i − 1 , j ) F(i-1, j) F(i−1,j)
-b-
在包括第 i i i 个物品的子集中( j − w i ≥ 0 j-w_i ≥ 0 j−wi≥0),最优子集是由该物品和前 i − 1 i-1 i−1 个物品中能够放入承重 为 j − w i j-w_i j−wi 的背包的最优子集组成,这种最优子集的总价值等于 v i + F ( i − 1 , j − w i ) v_i+F(i-1, j-w_i) vi+F(i−1,j−wi) -
因此,在前 i i i 个物品中最优解的总价值等于这两个价值中的较大值,当然,如果第 i i i 个物品不能够放入背包,则从前 i i i 个物品中选出的最优子集的总价值等于从前 i − 1 i-1 i−1 个物品中选出最优子集的总价值,从而有状态的递推式:
-a-
当 j-wi ≥ 0 时,F(i, j) = max{ F(i-1, j), vi+F(i-1, j-wi) }
-b-
当 j-wi < 0 时,F(i, j) = F(i-1, j)
-
定义初始条件: 当 j ≥ 0 时 , F ( 0 , j ) = 0 ; 当 i ≥ 0 时 , F ( i , 0 ) = 0 当 j ≥ 0 时, F(0, j) = 0; 当 i ≥ 0 时, F(i, 0) = 0 当j≥0时,F(0,j)=0;当i≥0时,F(i,0)=0
目标是:求 F ( n , W ) F(n, W) F(n,W),即 n n n 个物品中,能够放入 W W W 承重背包的子集的最大价值以及最优子集本身 -
如下表格所示:
0 | … | j − w i j-w_i j−wi | … | j j j | … | W W W | |
---|---|---|---|---|---|---|---|
0 | 0 | … | 0 | … | 0 | … | 0 |
… | … | … | … | … | … | … | … |
i − 1 i-1 i−1 | 0 | … | F ( i − 1 , j − w i ) F(i-1, j-w_i) F(i−1,j−wi) | … | F ( i − 1 , j ) F(i-1, j) F(i−1,j) | … | … |
i i i | 0 | … | … | … | F ( i , j ) F(i, j ) F(i,j) | … | … |
… | … | … | … | … | … | … | … |
n n n | 0 | … | … | … | … | … | 目标 |
当 i , j > 0 i, j > 0 i,j>0 时,为了计算第 i i i 行第 j j j 列的单元格 F ( i , j ) F(i, j) F(i,j),我们需要拿 前一行同一列(第 i − 1 i-1 i−1 行第 j j j 列)的单元格 与 v i v_i vi 加上前一行左边 w i w_i wi 列(第 i − 1 i-1 i−1 行第 j − w i j-w_i j−wi 列)的单元格的和 进行比较,计算两者的较大值
问题的解决过程,就是填表格的过程,填表可以逐行填写,也可以逐列填写
该算法的时间效率和空间效率都是 Θ ( n W ) Θ(nW) Θ(nW); 用来求最优解的具体组成的时间效率属于 O ( n ) O(n) O(n)
实例
问题描述:
填写表格:
总的最大价值是
F
(
4
,
5
)
=
37
F(4, 5)=37
F(4,5)=37
因为
F
(
4
,
5
)
>
F
(
3
,
5
)
F(4, 5)>F(3,5)
F(4,5)>F(3,5),所以物品 4 以及填满背包余下
5
−
2
=
3
5-2=3
5−2=3 个单位承重量的一个最优子集包括在最优解中(后者由
F
(
3
,
3
)
F(3, 3)
F(3,3) 表示 );
因为
F
(
3
,
3
)
=
F
(
2
,
3
)
F(3,3)=F(2,3)
F(3,3)=F(2,3),所以物品 3 不是最优子集的一部分;
因为
F
(
2
,
3
)
>
F
(
1
,
3
)
F(2,3)>F(1,3)
F(2,3)>F(1,3),所以物品 2 是最优选择的一部分,
F
(
1
,
3
−
1
)
F(1, 3-1)
F(1,3−1) 指定余下的组成部分;
因为
F
(
1
,
2
)
>
F
(
0
,
2
)
F(1,2)>F(0,2)
F(1,2)>F(0,2),所以最优为 {物品4, 物品2, 物品1}
记忆化
直接自顶而下的动态规划算法对递推关系式的求解,会导致算法不止一次地解公共的子问题,效率较低;而自底向上的动态规划,会对一些可能不需要的子问题求解,因此,可以使用记忆功能,将两者的优点进行结合
首先,需要维护一个类似自底向上的动态规划使用的表格,在为计算之前,统一初始化;之后,一旦计算一个新的值,先检查表中相应的单元格,如果已经计算过,则直接取值,否则计算,然后将结果记录在表格中
算法流程
对背包问题实现记忆功能方法
输入: 一个非负整数
i
i
i 表示先考虑的物品数量,一个非负整数
j
j
j 表示背包的承重量
输出: 前 i 个物品的最优子集的价值
注意: 处理 0 行和 0 列用 0 初始化外,F 中的其他单元格用 -1 初始化
# 伪代码
def mdp(i,j):
if F[i,j]<0:
if j<Weights[i]
value<--mdp(i-1,j)
else:
value<--max(mdp(i-1,j),Values[i]+mdp(i-1,j-Weights[i]))
F[i,j]<--value
return F[i,j]
案例与代码
问题描述:
某个景点,有 n 个小玩乐点,每个玩乐点需要 m i m_i mi 分钟游玩,游玩后获得欣赏度 r i r_i ri,已经游玩过的地点不能再去;现只有 m 分钟的游玩时间,为了尽可能多地收获欣赏度,求可以获得的最多的欣赏度以及游玩的地点
游玩地点 | 游玩时间 | 欣赏度 |
---|---|---|
1 | 2 | 4 |
2 | 2 | 35 |
3 | 1 | 43 |
4 | 2 | 10 |
代码
Python
# 总的游玩时间
con_min = 6
# 每个游玩点的游玩时间和对应欣赏度
minutes = [2, 2, 1, 2] # 与 rewards 一一对应
rewards = [4, 35, 43, 10]
# 游玩点的数量
pot_len = len(minutes)
F = [[0 for i in range(con_min)] for _ in range(pot_len)]
# item 初始化为 0,表示未游玩
item = [0 for _ in range(pot_len)]
# 找到最大欣赏点数
for i in range(pot_len):
for j in range(con_min):
# 如果该游玩项时间超出预定时间,则不游玩
if j < minutes[i]:
F[i][j] = F[i - 1][j]
# 否则查看是否游玩
else:
F[i][j] = max(F[i - 1][j], rewards[i] + F[i - 1][j - minutes[i]])
print('游玩欣赏度最高分: ')
print(F[pot_len - 1][con_min - 1])
# 找到获得最大游玩欣赏度的游玩点
def find(i, j):
if i >= 0:
# 相等说明没有游玩
if F[i][j] == F[i - 1][j]:
find(i - 1, j)
elif j - minutes[i] >= 0 and F[i][j] == F[i - 1][j - minutes[i]] + rewards[i]:
item[i] = 1 # 游玩标记
find(i - 1, j - minutes[i])
find(pot_len - 1, con_min - 1)
print('游玩点: ')
for i in range(pot_len):
if item[i] == 1:
print(i+1)
代码输出显示:
游玩欣赏度最高分:
88
游玩点:
2
3
4
Java
public class KnapsackProblem {
public static void main(String[] args) {
int[] weight = {1,4,3}; //物品重量 // [6, 5] [2,3][3,4]
int[] val = {1500,3000,2000}; //物品价值
int m = 4; //背包容量
int n = val.length; //物品个数
int[][] f = new int[n+1][m+1]; //f[i][j]表示前i个物品能装入容量为j的背包中的最大价值
int[][] path = new int[n+1][m+1];
//初始化第一列和第一行
for(int i=0;i<f.length;i++){
f[i][0] = 0;
}
for(int i=0;i<f[0].length;i++){
f[0][i] = 0;
}
//通过公式迭代计算
for(int i=1;i<f.length;i++){
for(int j=1;j<f[0].length;j++){
if(weight[i-1]>j)
f[i][j] = f[i-1][j];
else{
if(f[i-1][j]<f[i-1][j-weight[i-1]]+val[i-1]){
f[i][j] = f[i-1][j-weight[i-1]]+val[i-1];
path[i][j] = 1;
}else{
f[i][j] = f[i-1][j];
}
//f[i][j] = Math.max(f[i-1][j], f[i-1][j-weight[i-1]]+val[i-1]);
}
}
}
for(int i=0;i<f.length;i++){
for(int j=0;j<f[0].length;j++){
System.out.print(f[i][j]+" ");
}
System.out.println();
}
int i=f.length-1;
int j=f[0].length-1;
while(i>0&&j>0){
if(path[i][j] == 1){
System.out.print("第"+i+"个物品装入 ");
j -= weight[i-1];
}
i--;
}
}
}