zoukankan      html  css  js  c++  java
  • 动态规划学习整理


    动态规划问题整理

    问题思考

    背包类问题的求解误区

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

    想要举反例很简单,比如只有两个物品:物品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。

  • 相关阅读:
    基于SOA分布式架构的dubbo框架基础学习篇
    项目管理 绩效考核
    性能测试晋级教程
    从页面走向单元实现真正的业务驱动
    微软的开源Sonar工具测试网站的性能和安全性
    2.动手实操Apache ZooKeeper
    1. Apache ZooKeeper快速课程入门
    开发人员的福音:微软、谷歌、Mozilla将他们所有的Web API文档放在同一个地方
    Happy Java:定义泛型参数的方法
    比较两个文件不同以及生成SQL插入语句
  • 原文地址:https://www.cnblogs.com/peanutk/p/13565912.html
Copyright © 2011-2022 走看看