动态规划 之 背包问题 和 记忆功能 (Python and Java)

动态规划

动态规划(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 的背包,求这些物品中最有价值的一个子集,并且装到背包中。

动态规划分析

为了设计一个动态规划算法,需要分解问题,用部分物品组成的子问题的解来表示为背包问题的解:

  1. 首先,考虑一个由前 i i i 个物品( 1 ≤ i ≤ n 1 ≤ i ≤ n 1in)定义的子问题,物品的重量分别为 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 1jW);

  2. F ( i , j ) F(i, j) F(i,j) 是该子问题的最优解的物品总价值,也就是,能够放进承重为 j j j 的背包中的前 i i i 个物品中最有价值子集的总价值;

  3. 把前 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(i1,j)
    -b-
    在包括第 i i i 个物品的子集中( j − w i ≥ 0 j-w_i ≥ 0 jwi0),最优子集是由该物品和前 i − 1 i-1 i1 个物品中能够放入承重 为 j − w i j-w_i jwi 的背包的最优子集组成,这种最优子集的总价值等于 v i + F ( i − 1 , j − w i ) v_i+F(i-1, j-w_i) vi+F(i1,jwi)

  4. 因此,在前 i i i 个物品中最优解的总价值等于这两个价值中的较大值,当然,如果第 i i i 个物品不能够放入背包,则从前 i i i 个物品中选出的最优子集的总价值等于从前 i − 1 i-1 i1 个物品中选出最优子集的总价值,从而有状态的递推式:

 -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)
  1. 定义初始条件: 当 j ≥ 0 时 , F ( 0 , j ) = 0 ; 当 i ≥ 0 时 , F ( i , 0 ) = 0 当 j ≥ 0 时, F(0, j) = 0; 当 i ≥ 0 时, F(i, 0) = 0 j0F(0,j)=0;i0F(i,0)=0
    目标是:求 F ( n , W ) F(n, W) F(n,W),即 n n n 个物品中,能够放入 W W W 承重背包的子集的最大价值以及最优子集本身

  2. 如下表格所示:

0 j − w i j-w_i jwi j j j W W W
00000
i − 1 i-1 i10 F ( i − 1 , j − w i ) F(i-1, j-w_i) F(i1,jwi) F ( i − 1 , j ) F(i-1, j) F(i1,j)
i i i0 F ( i , j ) F(i, j ) F(i,j)
n n n0目标

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 i1 行第 j j j 列)的单元格 v i v_i vi 加上前一行左边 w i w_i wi 列(第 i − 1 i-1 i1 行第 j − w i j-w_i jwi 列)的单元格的和 进行比较,计算两者的较大值

问题的解决过程,就是填表格的过程,填表可以逐行填写,也可以逐列填写

该算法的时间效率和空间效率都是 Θ ( 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 52=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,31) 指定余下的组成部分;
因为 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 分钟的游玩时间,为了尽可能多地收获欣赏度,求可以获得的最多的欣赏度以及游玩的地点

游玩地点游玩时间欣赏度
124
2235
3143
4210

代码

  • 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--;
		}
	}
}
  • 4
    点赞
  • 26
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值