[Lintcode] triangle && 动态规划总结
Triangle
Given a triangle, find the minimum path sum from top to bottom. Each step you may move to adjacent numbers on the row below.
For example, given the following triangle
[
[2],
[3,4],
[6,5,7],
[4,1,8,3]
]
The minimum path sum from top to bottom is 11 (i.e., 2 + 3 + 5 + 1 = 11).
Bonus point if you are able to do this using only O(n) extra space, where n is the total number of rows in the triangle.
SOLUTION 1:
这个题特别适合总结DP方法,所以总结就在这题了。首先我们要知道为什么要动态规划,为啥不直接递归?很简单,递归时间复杂度太高了。为什么时间复杂度高?因为重复计算,实际上三角形里一共有(n^2)/2个点,实际算(n^2)/2次,O(n^2) 就够了,但是递归要计算多少次呢,递归要计算(2^n)/2,次,指数级别,太差了。怎么优化呢?cache一下就行了,现把所有计算结果放到hash里,然后重复计算时候就直接用,这样就只计算一次,就可以O(n^2)解决问题了。DP第一步就是这样,用记忆化搜索优化递归,达到DP效果。
记忆化搜索:这个方法我个人认为比DP好,不需要考虑转移方程,不需要考虑复杂的初始化,直接硬写,时间复杂度一样,多几行代码,少死几个脑细胞。
下面就是记忆化搜索模版:
核心就是先递归,做一个递归辅助函数,或者用divide & conquer也可以,写起来应该更简单。然后把所有结果存起来,如果flag标记一下,没标记过的存进去,标记过说明以前计算过,直接用,找到目标值就行了。
// 循环求所有状态 For I = 1 ->n dp[i] = search (i) // 搜索 int search(int i) { if(visit[i] == 1){ return dp[i]; } if(smallest state){ set smallest state } else { // to update (i,) , we might need other state // such as (i-1), (i+1) for other state update dp[i] = max(search(i-1) , search(i+1)) } visit[i] = 1; return dp[i]; }
看代码:
代码里用了几个全局变量,但是其实都是可以为了线程安全,放到search函数里。
public class Solution { /** * @param triangle: a list of lists of integers. * @return: An integer, minimum path sum. */ private int n; private int[][] minSum; private int[][] triangle; private int search(int x, int y){ if (x >= n){ return 0; } if (minSum[x][y] != Integer.MAX_VALUE){ return minSum[x][y]; } minSum[x][y] = triangle[x][y] + Math.min(search(x + 1, y), search(x + 1, y + 1)); return minSum[x][y]; } public int minimumTotal(int[][] triangle) { if (triangle == null || triangle.length == 0){ return -1; } if (triangle[0] == null || triangle[0].length == 0){ return -1; } this.n = triangle.length; this.triangle = triangle; this.minSum = new int[n][n]; for (int i = 0; i < n; i++){ for (int j = 0; j < n; j++){ minSum[i][j] = Integer.MAX_VALUE; } } return search(0, 0); } }
SOLUTION 2:
再来说正经的动态规划,先来说自底向上的DP。
先复习下动归四大要素:
1,状态,就是比如说f(x)或者f(x,y)代表了什么含义。在这题里面,首先是二维的,所以是f(x,y), 它代表从底到(x,y)坐标点的最小距离。
2,状态转移方程,就是说由哪个位置的状态可以得到现在位置的状态。f(x,y) = Math.min( f(x + 1, y), f(x + 1, y + 1) ).
3,初始化。初始化就是说,最开始定义的几个状态,而这几个状态是用上面方程求不出来的。本题要初始化的三角形最后一行的所有值。
4,结果。就是返回值,最后返回的是什么,有的是返回最后一项,有的是返回结果数组里面最大或最小的,要看实际定义。本题要找的就是f(0 ,0)也就是顶端的值。
public class Solution { /** * @param triangle: a list of lists of integers. * @return: An integer, minimum path sum. */ public int minimumTotal(int[][] triangle) { if (triangle == null || triangle.length == 0) { return -1; } if (triangle[0] == null || triangle[0].length == 0) { return -1; } int n = triangle.length; int[][] result = new int[n][n]; // from result[m - 1][0....n - 1] to result[0][0] //initial for (int i = 0; i < n; i++){ result[n - 1][i] = triangle[n - 1][i]; } //function for (int i = n - 2; i >= 0; i--){ for (int j = 0; j <= i; j++){ result[i][j] = triangle[i][j] + Math.min(result[i + 1][j], result[i + 1][j + 1]); } } //ANS return result[0][0]; } }
SOLUTION 3:
自顶向下的动态规划。
自顶向下要比较麻烦一点,转移方程一样,但是初始化要初始化对角线所有点以及左侧边界所有点,结果要在最后一行里找最大值。
代码如下:
// version 0: top-down public class Solution { /** * @param triangle: a list of lists of integers. * @return: An integer, minimum path sum. */ public int minimumTotal(int[][] triangle) { if (triangle == null || triangle.length == 0) { return -1; } if (triangle[0] == null || triangle[0].length == 0) { return -1; } // state: f[x][y] = minimum path value from 0,0 to x,y int n = triangle.length; int[][] f = new int[n][n]; // initialize f[0][0] = triangle[0][0]; for (int i = 1; i < n; i++) { f[i][0] = f[i - 1][0] + triangle[i][0]; f[i][i] = f[i - 1][i - 1] + triangle[i][i]; } // top down for (int i = 1; i < n; i++) { for (int j = 1; j < i; j++) { f[i][j] = Math.min(f[i - 1][j], f[i - 1][j - 1]) + triangle[i][j]; } } // answer int best = f[n - 1][0]; for (int i = 1; i < n; i++) { best = Math.min(best, f[n - 1][i]); } return best; } }
总结:
首先,在一个题里,看到问题是
**最大值/最小值**最大路径/最小路径**结果总数**true/false**=====> 基本可以先去试试动态规划思路。
其次,在动态规划过程中,可以优化的就是空间,用滚动数组优化二维DP,循环指针优化一维DP。(其实就是取个 %)。
再然后就是动态规划中,如果转移方程不好计算,并且初始化不好找,果断使用记忆化搜索,开一个额外空间做flag,标记这个位置是否访问过,再纪录所有计算过程,可以做到跟DP一样的时间复杂度。(我认为这个方法可以说是最好用的吧,不需要太多思考,但是一定要画一个搜索树,画图,确定方程)。
最后注意,比如说word ladder这种题,单词中字母位置跟DP数组中对应位置是否一致,需要看自己定义状态跟初始化。
浙公网安备 33010602011771号