题目:给定一个包含非负整数的 m × n 网格,请找出一条从左上角到右下角的路径,使得路径上的数字总和为最小。
说明:每次只能向下或者向右移动一步。
示例:
输入:
[
[1,3,1],
[1,5,1],
[4,2,1]
]
输出: 7
解释: 因为路径 1→3→1→1→1 的总和最小。
这道题可以利用动态规划进行求解。
定义:
min[i][j]
从左上角到第 i 行第 j 列的点最短路径值
以最终终点为例,要找到到达第 m 行第 n 列(右下角)的最短路径,我们只需要知道 min[m-1][n]
(到终点正上方的点的最短路径)以及min[m][n-1]
(到终点左侧的点的最短路径)的值谁更小,用更小的值,加上最终终点的值,就是我们需要的min[m][n]
的值。
因此我们可以写出如下表达式(grid表示输入的m×n二维数组):
min[m][n] = Min(min[m-1][n],min[m][n-1]) + grid[m-1][n-1]
然而有两种特殊情况需要注意:
1.到达矩形中第一行某点的最短路径一定是其左侧点min
值加上其本身:
min[1][j] = min[1][j-1] + grid[0][j-1]
2.到达矩形中第一列某点的最短路径一定是其上侧点min
值加上其本身:
min[i][1] = min[i-1][1] + grid[i-1][0]
实现代码(java):
public class answer11 {// m * n 矩形最短路径
public int minPathSum(int[][] grid) {
int a = grid.length;
int b = grid[0].length;
int[][] min = new int[a+1][b+1];//因为索引要取到a和b,因此这里声明a+1以及b+1
min[1][1] = grid[0][0];//设置边界条件,隐形边界条件为 min[0][j]以及min[i][0]都为0,它们没有实际意义
for (int i = 1; i <a+1 ; i++) {//迭代计算min数组的值
for (int j = 1; j < b+1; j++) {
if (i == 1){
min[1][j] = min[1][j-1] + grid[0][j-1];
}
else if (j == 1){
min[i][1] = min[i-1][1] + grid[i-1][0];
}
else{
min[i][j] = Math.min(min[i][j-1],min[i-1][j]) + grid[i-1][j-1];
}
}
}
return min[a][b];//运算完成后输出最终结果
}
}
实现代码 (Go):
func minPathSum(grid [][]int) int {
a := len(grid)
b := len(grid[0])
min := make([][]int,a+1)
for i:= 0;i<len(min);i++ {
min[i] = make([]int,b+1)
}
min[1][1] = grid[0][0]
for i:=1;i<a+1;i++ {
for j:=1;j<b+1;j++ {
if i==1 {
min[1][j] = min[1][j-1] + grid[0][j-1]
} else if j==1 {
min[i][1] = min[i-1][1] + grid[i-1][0]
} else {
min[i][j] = min2(min[i][j-1],min[i-1][j]) + grid[i-1][j-1]
}
}
}
return min[a][b]
}
func min2(a int,b int) int{
if a<b {
return a
}
return b
}
还有另外一种递归的思路:
我现在知道矩形左上角的值。
如果我知道大矩形中除去左上角定点那一行或者那一列形成的两个附属小矩形对应的最短路径中最小的值,那么我让其加上左上角的值 a,即可得出结果。
如下图所示,我想求黑色大矩形的最短路径,我只需要知道红色矩形以及黄色矩形他们俩产生的最短路径谁更小,用更小的值加上 a,就是最终答案。
同理,红色矩形则要用紫色和蓝色矩形判断,黄色矩形要用蓝色和绿色矩形进行判断,以此类推。
为此可以写出递归第一版代码:
class Solution {
public int minPathSum(int[][] grid){
return minPathSum(grid,0,0);
}
public int minPathSum(int[][]grid,int top,int left){//多态,输入初始矩阵,以及左上角顶点值索引
int result = 0;
//if (top<=grid.length-1 || left<=grid[0].length-1){
//上面这行本来是用于判断是否越界,然后进行越界处理,然而我下面的判断保证了索引不会越界,因此多于了
if (top+1<=grid.length-1 && left+1<=grid[0].length-1){
result = grid[top][left] + Math.min(minPathSum(grid,top,left+1),minPathSum(grid,top+1,left));
//新矩形左上角顶点值的下面和右边都存在值,均不越界
}
else if (top+1<=grid.length-1 && left+1>grid[0].length-1){
result = grid[top][left] + minPathSum(grid,top+1,left);
//新矩形左上角顶点值只有下面存在值,右边越界
}
else if (top+1>grid.length-1 && left+1<=grid[0].length-1){
result = grid[top][left] + minPathSum(grid,top,left+1);
//新矩形左上角顶点值下面越界,只有右边存在值
}
else if (top == grid.length-1 && left == grid[0].length-1){
result = grid[top][left];
//新矩形左上角顶点值,下面和右边均越界,只有单独一块
}//注意,情况必定是上面其中之一
}
return result;
//}
}
这样做完之后运行会发现超时,主要原因在于有太多重复的运算,时间复杂度很大。
就像上面画的图一样,我在判断红色矩阵值的时候计算了一遍蓝色,在判断黄色矩阵值的时候也计算了一边蓝色,类似的重复非常多,十分浪费时间,因此可以想到利用记忆优化,算完一次就把它存储起来,下次用的时候直接调用,这样就能大大节约时间。
第二版代码:
public class answer11_2 {
public int minPathSum(int[][] grid){
int[][] rem = new int[grid.length][grid[0].length];
for (int i = 0; i < grid.length; i++) {
Arrays.fill(rem[i],-1);//新创立记忆化搜索用的数组,并初始化。
}
return minPathSum(grid,0,0,rem);//这里方法发生了变化,要将记忆数组传入方法中
}
public int minPathSum(int[][]grid,int top,int left,int[][]rem){
if (rem[top][left]!= -1){//如果记忆数组有值,则返回值,不进行下面的递归运算
return rem[top][left];
}
int result = grid[top][left];
//这行本来在下面,现在提前了,逻辑不变,意义发生了稍许变化
//清楚result在下面必定会出现的某一种情况中,一定含有grid[top][left],只是不清楚会不会有后面多出来的表达式,因此进行下面的判断
if (top+1<=grid.length-1 && left+1<=grid[0].length-1){
result += Math.min(minPathSum(grid,top,left+1,rem),minPathSum(grid,top+1,left,rem));
}
else if (top+1<=grid.length-1 && left+1>grid[0].length-1){
result += minPathSum(grid,top+1,left,rem);
}
else if (top+1>grid.length-1 && left+1<=grid[0].length-1){
result += minPathSum(grid,top,left+1,rem);
}
/*else if (top == grid.length-1 && left == grid[0].length-1){
result = grid[top][left];
}*/
rem[top][left] = result;//计算完毕,要把值存入记忆数组
return result;
}
}
这种递归方法用的思想是准确的判断各种情况,保证输入不会越界,对不同情况进行不同处理。
还有另外一种思路:允许数组越界,但是会对越界情况进行特殊处理,保证程序运行正确:
以下代码由作者:LASTiMP提供
允许越界第一版,无记忆优化:
class Solution {
public int minPathSum(int[][] grid) {
return minPathSum(grid, 0, 0);
}
private int minPathSum(int[][] grid, int top, int left) {
if (top >= grid.length || left >= grid[0].length) return -1;
return grid[top][left] + min(minPathSum(grid, top + 1, left), minPathSum(grid, top, left + 1));
}
private int min(int a, int b) {
if (a == -1 && b == -1) return 0;
else if (a == -1) return b;
else if (b == -1) return a;
else return Math.min(a, b);
}
}
允许越界第二版,有记忆优化:
class Solution {
public int minPathSum(int[][] grid) {
int[][] saves = new int[grid.length][grid[0].length];
for (int i = 0; i < saves.length; i++) {
for (int j = 0; j < saves[0].length; j++) {
saves[i][j] = -1;
}
}
return minPathSum(grid, 0, 0, saves);
}
private int minPathSum(int[][] grid, int top, int left, int[][] saves) {
if (top >= grid.length || left >= grid[0].length) return -1;
if (saves[top][left] == -1)
saves[top][left] = grid[top][left] + min(minPathSum(grid, top + 1, left, saves), minPathSum(grid, top, left + 1, saves));
return saves[top][left];
}
private int min(int a, int b) {
if (a == -1 && b == -1) return 0;
else if (a == -1) return b;
else if (b == -1) return a;
else return Math.min(a, b);
}
}
个人认为,这个越界处理巧妙之处就在于新写的min
方法,越界之后minPathSum
方法会返回 -1,然而我们通常用的Math.min()
方法并不支持负数比较,通过重写方法,让越界处理变的十分简单。