基本技巧
动态规划是指这样一类题目:
- 可通过穷举的办法解决
- 存在重叠子问题
- 具备最优子结构
- 需要明确的状态转移方程
动态规划解题流程无外乎:
- 确定状态
- 定义dp函数/数组含义
- 明确选择
- 明确base case
参照算法刷题思路小节笔记,因为动态规划的上述特性,决定了动态规划算法存在进步与优化的空间。 用最简单的斐波那契数列来看,暴力的方法是递归所有子问题,但因为存在重叠情况,所以采用了备忘录、自底向上等方法来优化。
熟练掌握解题流程,然后通过优化方式来带入思考,能够在时间空间上都有一定进步。
什么是最优子结构
量化子结构是动态规划的必要条件,目的是为了求解最值。 动态规划即根据base case往后推导,不断找到最优子结构,一定要证明状态转移方程。
dp数组的方向
观察可发现,在动态规划设计中各种遍历方式都有,但是一定要满足如下两点:
- 遍历的过程中,所需的状态必须是已经计算出来的。
- 遍历的终点必须是存储结果的那个位置。
简单来说就是新结果必须基于已有结果,要么递归再运算,要么自底向上。无论哪种方向,终点必须是求解结果。
base case 和 备忘录初始值
这一篇说实话,写的不好,不建议参考,可以根据给出的习题,自己思考解决。 只要认真执行动态规划四步走,在明确了状态选择时,先写出代码再逐步优化即可。
状态压缩技巧
状态压缩的核心,就是将二维数组压缩到一维数组即可。 如果之前阅读了基础知识相关部分,可以把这一篇和打家劫舍里的内容结合起来看,如下图。 打家劫舍中存在下一状态仅仅与上一状态两种取值有关,所以无需保存过程值,仅计算结束值即可。

需要注意,在求解动态规划的过程中,忌讳一上来就写的贼漂亮,当然这样很好,但是程序的可阅读性差,思考复杂度高,并不适合新手操作。
明确动态规划的本质还是可以穷举解决的问题,再通过解决重复子问题(备忘录or自底向上),状态压缩进而空间压缩,逐步优化。 【完成比完美重要】逐步思考而后再运用娴熟。
回溯算法与动态规划
这里采用了leetcode-494题,很好的比较了回溯算法与动态规划的差异性。 简单来说,回溯算法简单清晰,但是算法时间复杂度高,即使经过剪枝的情况下,仍然没有很大改进。
但是动态规划则不一样,通过划分子问题,再加之改造,容易得到非常魔性的解法…而且还会和初始思路偏差较大。 但是不用慌张,这里的动态规划套路是优先的,只要通过多刷题肯定能掌握。小熊肯定没问题√

class Leetcode494 {int res = 0;/*** 第一种写法,回溯实现* @param nums* @param target* @return*/public int findTargetSumWays(int[] nums, int target) {backtrack(nums, 0, 0, target);return res;}void backtrack(int[] nums, int sum, int i, int target) {if (i == nums.length && sum == target) {res += 1;}if (i == nums.length) {return;}backtrack(nums, sum + nums[i], i + 1, target);backtrack(nums, sum - nums[i], i + 1, target);}/*** 第二种写法,回溯实现,针对函数传参优化* @param nums* @param target* @return*/public int findTargetSumWays(int[] nums, int target) {backtrack(nums, 0, target);return res;}void backtrack(int[] nums, int i, int rest) {if (i == nums.length) {if (rest == 0) {res += 1;}return;}backtrack(nums, i + 1, rest + nums[i]);backtrack(nums, i + 1, rest - nums[i]);}/*** 【消除重叠子问题】* 第三种写法,回溯剪枝* @param nums* @param target* @return*/public int findTargetSumWays(int[] nums, int target) {return backtrack(nums, 0, target);}Map<String, Integer> memo = new HashMap<>();int backtrack(int[] nums, int i, int rest) {if (i == nums.length) {if (rest == 0) {return 1;}return 0;}if (memo.contains("" + (i + 1) + rest)) {return memo.get("" + (i + 1) + rest);}memo.put("" + (i + 1) + rest, backtrack(nums, i + 1, rest + nums[i]) + backtrack(nums, i + 1, rest - nums[i]));return memo.get("" + (i + 1) + rest);}/*** 【动态规划解法】** @param nums* @param target* @return*/public int findTargetSumWays(int[] nums, int target) {int sum = 0;for (int n : nums) {sum += n;}if (sum < target || (sum + target) % 2 == 1) {return 0;}return subsets(nums, (sum + target) / 2);}int subsets(int[] nums, int sum) {}}
动态规划解法推导过程如下:
- 该问题计算的是所有数字经过分配运算符,运算后是否等于给定值
- 那么问题其实是可以划分子集计算,我们将nums划分为两个子集A、B,分别代表分配+、-,那么可以得到如下运算关系: sum(A) - sum(B) = target sum(A) = target + sum(B)
sum(A) + sum(A) = target + sum(B) + sum(A)
2 * sum(A) = target + sum(nums)
sum(A) = (target + sum(nums)) / 2 - 得到了上面的函数,就可以转而定义出计算子集函数 int subsets(int[] nums, int sum){} 问题转而变成了背包问题的标准形式
