动态规划学习整理


动态规划问题整理

问题思考

背包类问题的求解误区

在学动态规划思想之前求解装包类问题时,很容易想到根据性价比排序优先装高性价比物品的贪心算法,这就有点像线性规划,连续型变量我们可以通过求导来计算,但涉及到整型就会很头疼了:

想要举反例很简单,比如只有两个物品:物品A:价值5,体积5,物品B:价值8:体积7,背包容量为10,物品B的性价比显然要比物品A高,那么用贪心算法必然会选择放入一个物品B,此时,剩余的空间已无法装下A或者B,所以得到的最高价值为8,而实际上,选择放入两个物品A即可得到更高的价值10。所以这里贪心算法并不适用。
完全背包问题

再比如零钱兑换问题也会陷入这样的误区。

哪些问题适用dp

  1. 动态规划理论部分
    1.1. 最优化原理
    全局最优策略的子策略也必须是子问题的最优解。
    1.2. 无后效性
    当下决策不看过去(过去对当下的选择没有影响),马尔科夫决策。

两个反例参考01背包问题

  1. 相关问题整理
    首先是最优解问题,最优解问题通常都可以先考虑下是否可以用dp来求解(找子问题,要满足最优化原理和无后效性的特点),通常会用min、max来在子问题解的基础上进行选择。
    比较典型的就是背包问题(各种求和/填充问题)、子序列问题,通常都会有分阶段选择决策的步骤。
    目标和问题虽然不是最优化问题,但也是个背包问题,可以用dp来求解,它其实不是决策,而是在用dp存储方法数并传递,所以用的不是max/min,而是加法。

怎么dp

dp的关键在于找到当前问题与子问题的递推关系式,并且要正确地设置边界值。
使用递推关系式,假设子问题已经求出最优解,当前问题的决策只需在子问题最优解的基础上选取当下的动作,然后考虑怎么将当下动作和子问题的最优解相结合(即,dp用来存什么以及怎么算)。
通过以下几个问题练习一下问题分解以及递推关系式(可以在leetcode中搜索题目):

以上几类算是比较经典的简单dp问题,初次见到时建议自己手动算,在计算的过程中发现重叠的子问题,然后找出递推公式,熟悉了后记住这几类问题的子问题切入点,面试经常会问到。像背包、投资、路径选择问题,子问题还是比较显而易见的,但最长子序列和连续子序列(字串)问题,就要记住子问题怎么切入:

  • 最长子序列的dp记录以nums[i]结束的最长子序列,从i=0开始++,每一步决策是找j<i的、能拼接上的最大dp[j]来拼接相加作为dp[i];
  • 连续子序列的dp记录以nums[i]结束的连续子序列,从i=0开始++,每一步决策是决定当前的nums[i]是拼接相加到dp[i-1]上,还是另起炉灶。
    对于新的最优求解问题,基于已知子问题最优解的方式找不到递推关系式时,可以举例手算求解,在求的过程中发现哪些地方存在重复计算,这里就是子问题的方向;然后将问题分解到边界子问题,在向上(原问题方向)计算的过程中便可观察出递推关系式。
    值得注意的是,dp数组(维度根据问题而定)元素的“值”不一定就是个整数或者浮点数,也可以是String、List、或者结构体/类对象等(可以保存路径)。
//背包问题的求解
public int binPack(int[] w, int[] v, int capacity, int n_items) {
	int[][] profit_max = new int[n_items][capacity];
	for (int j = 0; j < capacity; j++)
		profit_max[0][j] = w[0] < j ? v[0] : 0;
	for (int i = 1; i < n_items; i++) {
		for (int j = 0; j < capacity; j++) {
			// profit_max[i][j] = max{[k * v[i] + profit_max[i-1][j - k*w[i]] for k in options[i]]}	
			// |
			// V
			//	for (int k = 0; k*w[i] <= j && k <= n[i]; k++) {
			// 		profit_max[i][j] = Math.max(profit_max[i][j], k * v[i] + profit_max[i-1][j - k*w[i]])
			//	}
			// 0-1背包问题的options[i]就是0/1(也是特殊的完全背包/多重背包问题),完全背包问题的options[i]就是满足k*w[i]<=j的k,多重背包问题的options[i]就是满足k<=n[i]&&k*w[i]<=j的k。
			if (w[i] < j) {
				profit_max[i][j] = Math.max(v[i] + profit_max[i-1][j-w[i]], profit_max[i-1][j]);
			} else {
				profit_max[i][j] = profit_max[i-1][j];
			}
		}
	}				
}
//也有一维的背包dp,但个人认为二维的更好理解(也可能是我刷题太少了),在遇到新问题时,这种多一维的更容易想到。

递归、有记忆的递归(自上而下记忆法)、自下而上填表法的区别

参考01背包问题可以更清楚地区别回溯法、递归、动态规划之间的区别与联系。
不管是递归还是动态规划(这里说自上而下记忆法,也就是有记忆的递归),都需要找出问题与子问题之间的递推关系式,至于需不需要保存记忆,就看子问题有没有重叠部分了(比如斐波那契数列),保存记忆可以避免重复求解,这就是递归不同于动态规划的地方。
而回溯、DFS是遍历解空间的思想,即使有剪枝,其遍历空间还是比dp要大很多,因为dp使用最优解遍历时避免了对差解的遍历。
以0-1背包问题为例,递归的时间复杂度为capacity * n_items * 2;而DFS的时间复杂度为2^n_items。

posted @ 2020-08-26 16:25  汉尼拔草  阅读(366)  评论(0编辑  收藏  举报