在刷LeetCode时,遇到了一些动态规划的题。个人觉得动态规划属于算法中比较难的一部分了,出去面试的话,经常会问到一些经典的动态规划题,比如青蛙跳台阶、最长回文子串等。在此对动态规划做一些简单的研究和总结。
动态规划的概念
动态规划(DP,Dynamic Programming)是运筹学的一个分支,是求解决策过程(decision process)最优化的数学方法。20世纪50年代初美国数学家R.E.Bellman等人在研究多阶段决策过程(multistep decision process)的优化问题时,提出了著名的最优化原理(principle of optimality),把多阶段过程转化为一系列单阶段问题,利用各阶段之间的关系,逐个求解,创立了解决这类过程优化问题的新方法——动态规划。1957年出版了他的名著《Dynamic Programming》,这是该领域的第一本著作。
从以上简单的摘要我们可以概括出一些要点, DP不是一个具体的算法,类似于冒泡或者快排一样,有特定的规章和套路,它是一种求解问题的优化途径和思路模式。
也就意味着,我们面对复杂的问题时,看问题是否可以分解为规模更小的字问题,并且原问题的最优解中包含了子问题的最优解,可以的话就可以使用动态规划。
因此DP的关键在于如何分解问题,而分解问题,依靠的就是问题的状态 和 状态之间的转移。
如何定义问题的状态
以一道leetcode题为例
最长上升子序列:给定一个无序的整数数组,找到其中最长上升子序列的长度。
示例:
输入: [10,9,2,5,3,7,101,18]
输出: 4
解释: 最长的上升子序列是 [2,3,7,101],它的长度是 4。
我们需要找到一个问题在一个状态的最优解。
我们先定义一个状态,假设以数组第i个元素结尾的满足条件的最长上升子序列的长度为dp(i),则最优解是dp(1),dp(2)…dp(n)中的最大值。
状态转移
转移方程: 设 j∈[0,i),在每轮计算dp[i]时,遍历区间[0,i),有以下判断。
1.当 nums[i] > nums[j],nums[i]可以连接在 nums[j]后构成符合要求的子序列,此次是最长上升子序列长度为 dp[j] + 1。
2.当 nums[i] <= nums[j]时,nums[i]不可以连接在 nums[j]后构成符合要求的子序列,跳过。
因此在上述1的场景下,计算出的dp[j] + 1的最大值为符合要求的最长上升子序列的长度。
因此,转移方程是dp[i] = max(dp[i], dp[j] + 1) for j in [0, i)。
代码如下:
/**
* @param {number[]} nums
* @return {number}
*/
var lengthOfLIS = function(nums) {
if (nums.length === 0) {
return 0;
}
if (nums.length === 1) {
return 1;
}
var resultArr = nums.concat();
resultArr.fill(1, 0, nums.length);
for (var i = 1; i < nums.length; i++) {
for (var j = 0; j < i; j++) {
if (nums[j] < nums [i]) {
resultArr[i] = Math.max(resultArr[i], resultArr[j] + 1);
}
}
}
return Math.max(...resultArr);
};
以上为动态规划的实践。
动态规划难点在于状态的确定以及状态的迁移。有以下总结:
一个问题是该用递推、贪心、搜索还是动态规划,完全是由这个问题本身阶段间状态的转移方式决定的
每个阶段只有一个状态->递推;
每个阶段的最优状态都是由上一个阶段的最优状态得到的->贪心;
每个阶段的最优状态是由之前所有阶段的状态的组合得到的->搜索;
每个阶段的最优状态可以从之前某个阶段的某个或某些状态直接得到而不管之前这个状态是如何得到的->动态规划。