动态规划题主要的思路步骤就是: 将问题做拆解,找到问题之间具体的联系;状态定义;递推方程推导;实现。
这里还是拿 Quora 上面的例子来讲解,“1+1+1+1+1+1+1+1” 得出答案是 8,那么如何快速计算 “1+ 1+1+1+1+1+1+1+1”,我们首先可以对这个大的问题进行拆解,这里我说的大问题是 9 个 1 相加,这个问题可以拆解成 1 + “8 个 1 相加的答案”,8 个 1 相加继续拆,可以拆解成 1 + “7 个 1 相加的答案”,… 1 + “0 个 1 相加的答案”,到这里,第一个步骤 已经完成。【初始化定义为1,后面的都是+1】
定义好了状态,递推方程就变得非常简单,就是 dp[i] = dp[i - 1] + 1
,这里的 dp[i]
记录的是当前问题的答案,也就是当前的状态,dp[i - 1]
记录的是之前相邻的问题的答案,也就是之前的状态,它们之间通过 +1 来实现状态的变更。
最后一步就是实现了,有了状态表示和递推方程,实现这一步上需要重点考虑的其实是初始化,就是用什么样的数据结构,根据问题的要求需要做那些初始值的设定。
public int dpExample(int n) { |
你可以看到,动态规划这四个步骤其实是相互递进的,状态的定义离不开问题的拆解,递推方程的推导离不开状态的定义,最后的实现代码的核心其实就是递推方程,这中间如果有一个步骤卡壳了则会导致问题无法解决,当问题的复杂程度增加的时候,这里面的思维复杂程度会上升。
Leetcode70:爬楼梯:
假设你正在爬楼梯。需要 n 阶你才能到达楼顶。每次你可以爬 1 或 2 个台阶。你有多少种不同的方法可以爬到楼顶呢?注意:给定 n 是一个正整数。
输入:2 |
输入:3 |
问题拆解:
我们到达第 n 个楼梯可以从第 n – 1 个楼梯和第 n – 2 个楼梯到达,因此第 n 个问题可以拆解成第 n – 1 个问题和第 n – 2 个问题,第 n – 1 个问题和第 n – 2 个问题又可以继续往下拆,直到第 0 个问题,也就是第 0 个楼梯 (起点)
状态定义
“问题拆解” 中已经提到了,第 n 个楼梯会和第 n – 1 和第 n – 2 个楼梯有关联,那么具体的联系是什么呢?你可以这样思考,第 n – 1 个问题里面的答案其实是从起点到达第 n – 1 个楼梯的路径总数,n – 2 同理,从第 n – 1 个楼梯可以到达第 n 个楼梯,从第 n – 2 也可以,并且路径没有重复,因此我们可以把第 i 个状态定义为 “从起点到达第 i 个楼梯的路径总数”,状态之间的联系其实是相加的关系。
递推方程
“状态定义” 中我们已经定义好了状态,也知道第 i 个状态可以由第 i – 1 个状态和第 i – 2 个状态通过相加得到,因此递推方程就出来了 dp[i] = dp[i - 1] + dp[i - 2]
实现
你其实可以从递推方程看到,我们需要有一个初始值来方便我们计算,起始位置不需要移动 dp[0] = 0
,第 1 层楼梯只能从起始位置到达,因此 dp[1] = 1
,第 2 层楼梯可以从起始位置和第 1 层楼梯到达,因此 dp[2] = 2
,有了这些初始值,后面就可以通过这几个初始值进行递推得到。
LeetCode 第 120 号问题:三角形最小路径和。
给定一个三角形,找出自顶向下的最小路径和。每一步只能移动到下一行中相邻的结点上。
例如,给定三角形:
[ |
- 问题拆解:
这里的总问题是求出最小的路径和,路径是这里的分析重点,路径是由一个个元素组成的,和之前爬楼梯那道题目类似,[i][j]
位置的元素,经过这个元素的路径肯定也会经过 [i - 1][j]
或者 [i - 1][j - 1]
,因此经过一个元素的路径和可以通过这个元素上面的一个或者两个元素的路径和得到。
状态定义
状态的定义一般会和问题需要求解的答案联系在一起,这里其实有两种方式,一种是考虑路径从上到下,另外一种是考虑路径从下到上,因为元素的值是不变的,所以路径的方向不同也不会影响最后求得的路径和,如果是从上到下,你会发现,在考虑下面元素的时候,起始元素的路径只会从
[i - 1][j]
获得,每行当中的最后一个元素的路径只会从[i - 1][j - 1]
获得,中间二者都可,这样不太好实现,因此这里考虑从下到上的方式,状态的定义就变成了 “最后一行元素到当前元素的最小路径和”,对于[0][0]
这个元素来说,最后状态表示的就是我们的最终答案。递推方程
“状态定义” 中我们已经定义好了状态,递推方程就出来了
dp[i][j] = Math.min(dp[i + 1][j], dp[i + 1][j + 1]) + triangle[i][j]
实现
这里初始化时,我们需要将最后一行的元素填入状态数组中,然后就是按照前面分析的策略,从下到上计算即可
public int minimumTotal(List<List<Integer>> triangle) { |
LeetCode 第 53 号问题:最大子序和。
给定一个整数数组 nums ,找到一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。
输入: [-2,1,-3,4,-1,2,1,-5,4], |
问题拆解:
问题的核心是子数组,子数组可以看作是一段区间,因此可以由起始点和终止点确定一个子数组,两个点中,我们先确定一个点,然后去找另一个点,比如说,如果我们确定一个子数组的截止元素在 i 这个位置,这个时候我们需要思考的问题是 “以 i 结尾的所有子数组中,和最大的是多少?”,然后我们去试着拆解,这里其实只有两种情况:
i 这个位置的元素自成一个子数组;
i 位置的元素的值 + 以 i – 1 结尾的所有子数组中的子数组和最大的值
你可以看到,我们把第 i 个问题拆成了第 i – 1 个问题,之间的联系也变得清晰
状态定义
通过上面的分析,其实状态已经有了,
dp[i]
就是 “以 i 结尾的所有子数组的最大值”递推方程
拆解问题的时候也提到了,有两种情况,即当前元素自成一个子数组,另外可以考虑前一个状态的答案,于是就有了。
实现
题目要求子数组不能为空,因此一开始需要初始化,也就是 dp[0] = array[0]
,保证最后答案的可靠性,另外我们需要用一个变量记录最后的答案,因为子数组有可能以数组中任意一个元素结尾
public int maxSubArray(int[] nums) {
if (nums == null || nums.length == 0) {
return 0;
}
int n = nums.length;
int[] dp = new int[n];
dp[0] = nums[0];
int result = dp[0];
for (int i = 1; i < n; ++i) {
dp[i] = Math.max(dp[i - 1], 0) + nums[i];
result = Math.max(result, dp[i]);
}
return result;
}