路径连环炮
第一炮 统计路径总数
问题描述
一个机器人位于一个 m x n 网格的左上角 (起始点在下图中标记为 “Start” )。机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为 “Finish” )。问总共有多少条不同的路径?详见leetcode62
问题分析
从起点开始,向下或者向右,走一次相当于少了一行或者一列,继续走,每走一次,都会少一行或者一列。我么可以通过递归来实现
代码实现
public int uniquePaths(int m, int n) {
if(m==1||n==1){
return 1;
}
return uniquePaths(m-1,n)+uniquePaths(m,n-1);
}
执行结果
大部分测试用例通过,少部分测试用例超出时间限制,说明我们的算法是正确的,但是仍然有待改进之处,以3✖️3的矩阵为例,在递归过程中,我们需要计算uniquePaths(m,n),包括带入m=3,n=3,需要计算(3,3),(2,3),(1,3),(2,2),(1,2),(2,1),(3,2),(2,2),(1,2),(2,1),(3,1)。可以看到其中包含多次的重复运算,如uniquePaths(1,2),uniquePaths(2,1)等。
第二炮 使用二维数组优化统计路径总数
优化分析
通过上面的分析,我们知道递归处理统计路径总数时存在大量重复计算。我们可以通过提前将运算结果保存起来,这样,下次运算时,不用运算,直接取结果即可,因为是存储m*n矩阵对应的路径总数,我们可以创建一个二维数组,数组的下标对应m,n,数组值表示从起点到当前位置的路径总数,这样我们最终统计的路径总数等于二维数组最后一个元素的值。很明显,数组的第一行和第一列对应的元素值为1,其他位置元素值等于其正上方元素与其正前方元素值和。
代码实现
public int uniquePaths(int m, int n) {
int[][] search = new int[m][n];
search[0][0] = 1;
for(int i=0;i<m;i++){
for(int j=0;j<n;j++){
if(j>0&&i>0){
search[i][j] = search[i-1][j]+search[i][j-1];
}else if(i>0){
search[i][j] = search[i-1][j];
}else if(j>0){
search[i][j] = search[i][j-1];
}
}
}
return search[m-1][n-1];
}
第三炮 使用滚动数组优化路径统计总数
优化分析
使用二维数组占用空间太大了。通过上面的计算,我们知道,二维数组的第一行和第一列对应的元素值为1,其他位置元素值等于其正上方元素与其正前方元素值和。我们可以初始化一个长度为n的一维数组,初始值为1。之后从头遍历数组,除了第一个元素,每个元素都等于本身与前一个元素之和。这样循环m次,相当于使用一维数组不断滚动代替二维数组。
代码实现
public int uniquePaths(int m, int n) {
int[] search = new int[n];
for(int i=0;i<n;i++){
search[i] = 1;
}
for(int i=1;i<m;i++){
for(int j=1;j<n;j++){
search[j] = search[j-1]+search[j];
}
}
return search[n-1];
}
第四炮 最小路径和
问题描述
给定一个包含非负整数的 m x n 网格 grid ,请找出一条从左上角到右下角的路径,使得路径上的数字总和为最小。说明:每次只能向下或者向右移动一步。详见leetcode64
问题分析
这个问题与统计路径总数比较相似,仍然是从左上角到右下角,每次只能向下或者向右移动,我们仍然可以使用二维数组优化重复计算,数组的下标对应m,n,数组值表示从起点到当前位置的最小路径和,这个最小路径和是等于从正上方或者正左方到当前位置的最小值加上当前位置的权值得到的。
代码实现
public int minPathSum(int[][] grid) {
int[][] search = new int[grid.length][grid[0].length];
for(int i=0;i<grid.length;i++){
for(int j=0;j<grid[0].length;j++){
if(i==0&&j==0){
search[i][j] = grid[i][j];
}else{
int top = i>0?search[i-1][j]:Integer.MAX_VALUE;
int left = j>0?search[i][j-1]:Integer.MAX_VALUE;
search[i][j] = Math.min(top,left)+grid[i][j];
}
}
}
return search[grid.length-1][grid[0].length-1];
}
第五炮 三角形最小路径和
问题描述
给定一个三角形 triangle ,找出自顶向下的最小路径和。每一步只能移动到下一行中相邻的结点上。相邻的结点 在这里指的是 下标 与 上一层结点下标 相同或者等于 上一层结点下标 + 1 的两个结点。也就是说,如果正位于当前行的下标 i ,那么下一步可以移动到下一行的下标 i 或 i + 1 。详见leetcode120
问题分析
首先我们要将三角形看成只有对角线及以下有元素的下三角形。我们同样创建一个二维数组来存储最小路径和。数组的下标对应m,n,数组值表示从起点到当前行当前位置的最小路径和。对于第一列,第一个元素为三角定点元素,之后的每一个最小路径和元素等于上一个元素与当前元素的和,对于对角线元素,第一个元素为三角定点元素,之后的每一个最小路径和元素等于左上一个元素与当前元素的和。对于其他元素,最小路径和等于左上和正上方到当前路径的最小值加上当前元素的和。最后遍历底层元素,找到最小路径和。
代码实现
public int minimumTotal(List<List<Integer>> triangle) {
int n = triangle.size();
int[][] search = new int[n][n];
search[0][0] = triangle.get(0).get(0);
for(int i=1;i<n;i++){
for(int j=0;j<=i;j++){
int val = triangle.get(i).get(j);
search[i][j] = Integer.MAX_VALUE;
if(j!=0){
search[i][j] = Math.min(search[i][j],search[i-1][j-1]+val);
}
if(j!=i){
search[i][j] = Math.min(search[i][j],search[i-1][j]+val);
}
}
}
int ans = Integer.MAX_VALUE;
for(int i=0;i<n;i++){
ans = Math.min(search[n-1][i],ans);
}
return ans;
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
理解动态规划
通过上面的例子,我们来理解动态规划的算法思想。动态规划的引入是为了消除冗余,加速计算。(例如,上面使用二维数组存储计算结果,避免重复计算)。使用动态规划,要满足有无后效性。即只管当下状态,不管之前状态。(如使用滚动数组,我们只保留了当前的路径总数,并没有将之前计算的所有路径总和都保存下来)。我们看到好多使用动态规划的问题使用递归或者回溯也可以解决(计算路径综合我们首先使用递归,之后使用动态规划进行优化)。回溯和动态规划的区别在于动态规划无后向性。因此很多问题回溯能解决,但是效率不高,而动态规划能解决的问题比回溯少,但是计算效率高。如,动态规划只能计算路径总和,不能得到所有路径,回溯既能得到路径总和,又能得到所有路径。动态规划具体能解决的问题类型,我们之后再详细说明。通过上面的例题,我们对动态规划有了一个基本模板。我们需要维护一个数组(一维或者二维),我们要明白每个数组表示的含义(状态),每个数组元素如何根据之前的元素来计算(状态转移方程),之后我们需要按照状态转移方程计算数组所有元素的值(实现记忆化搜索),最后,我们需要知道数组中的那个位置是我们最终需要的结果。