题目描述:给定一个 N*M 的矩阵arr,从左上角开始每次只能向下或者向右走,最后到达右下角。路径上所有点的数字和为 路径和,求最小的路径和。
典型的动态规划。状态方程为: dp[i][j] = getMin( dp[i – 1][j] ,dp[i][j – 1] ) + arr[i][i] 。dp[i][j] 表示 达到点 arr[i][j] 是的最小路径和,因为每次只能向下或者向右,所以要达到 arr[i][j] 必须先经过 arr[i – 1][j – 1] 或者 arr[i][j – 1] 其中一个点,找出路径最小的即可。
因为每次只能向下和向右,所以第一行只能从左一直往右走,而第一列只能从上一直往下走,并且将经过的点累加起来。
所以我们可以先对第一行、第一列的 dp[i][j] 进行初始化。
具体代码:
import java.util.Scanner;
/**
* 输入一个 N*M 的矩阵,从左上角开始,每次只能向下或者向右走,最后到达右下角的位置
* 将路径上所以数字加起来就是路径和,求最小路径和
*
* @author luzi
*
*/
public class minPathSumOfArr {
public static void main(String args[]){
Scanner scan = new Scanner(System.in);
while(scan.hasNext()){
int n = scan.nextInt(); //行数
int m = scan.nextInt(); //列数
int[][] arr = new int[n][m];
for(int i = 0; i < n; i++){
for(int j = 0; j < m; j++){
arr[i][j] = scan.nextInt();
}
}
System.out.println(minPath(arr,n,m));
}
}
public static int minPath(int[][] arr,int n,int m){
if(arr == null)
return 0;
int[][] dp = new int[n][m];
dp[0][0] = arr[0][0];
for(int i = 1; i < n;i++){
dp[i][0] = dp[i - 1][0] + arr[i][0];
}
for(int i = 1; i < m; i++){
dp[0][i] = dp[0][i - 1] + arr[0][i];
}
for(int i = 1; i < n; i++){
for(int j = 1; j < m; j++){
dp[i][j] = Math.min(dp[i - 1][j], dp[i][j - 1]) + arr[i][j];
}
}
return dp[n - 1][m - 1];
}
}
/*******************2017-5-31 修改********************/
上面的方法中,时间复杂度为 O(n*m), 但是辅助空间也为 O(n*m)。通过压缩空间的方法来减小辅助空间,使得辅助空间只需要 O(min(n,m)),
状态转移方程为: dp[ i ] = min ( dp[ i – 1] , dp[ i ] ) + arr[ i ] [ j ] .
我们用一个实际的例子来分析说明,比如现在有一个 5*4 的矩阵:
1 1 1 1
2 3 5 4
1 7 2 3
1 3 3 4
2 3 4 1
明显 n > m,我们设置一个辅助的数组 int[ ] dp = new int [ m ]。dp[ 0 ] = arr[ 0 ][ 0 ];
我们从第一行开始(如果是 m > n,那么就从 第一列开始),因为第一行只能从左到右(第一列则只能从上到下),
所以我们初始化 dp[ 1 ] = 2,dp[ 2 ] = 3, dp[ 3 ] = 4。接下来看第二行,因为第二行的第一个只能是从上到下,所以此时应该为 1 + 2 = 3,即 dp[ 0 ] = dp[ 0 ] + arr[ 1 ][ 0 ] = 3, 而第二行的第二个,有两种方法可以到达,一种是从左边的2 到达,一种是从上面的 1 到达,我们要的是更小的路径,所以我们选择左边跟上边中更小的那个即:
min( dp[ i – 1],dp[ i ] ) 。式子中红色 的 dp[ i ] ,表示的是我们能到达上一行的 第 i 个数的 最小路径值,也就是我们当前这一行的第 i 的数上面数值,跟当前行左边的数值进行比较取更小的 。最终我们遍历到最后一行最后一个数字,得出的dp [ m – 1 ] 就是最小的路径。
具体的代码如下:
//压缩空间的方法,只需要额外的 O(min(n,m)) 空间 ,代码过于冗余,精益求精!再优化
public static int minPath2(int[][] arr,int n,int m){
if(arr == null)
return 0;
if(n <= m){
int[] dp = new int[n];
dp[0] = arr[0][0];
for(int i = 1; i < n; i++){
dp[i] = dp[i - 1] + arr[i][0];
}
for(int i = 1; i < m; i++){
dp[0] = dp[0] + arr[0][i];
for(int j = 1; j < n; j++){
dp[j] = Math.min(dp[j], dp[j - 1]) + arr[j][i];
}
}
return dp[n - 1];
}
else{
int[] dp = new int[m];
dp[0] = arr[0][0];
for(int i = 1; i < m; i++){
dp[i] = dp[i - 1] + arr[0][i];
}
for(int i = 1; i < n; i++){
dp[0] = dp[0] + arr[i][0];
for(int j = 1; j < m; j++){
dp[j] = Math.min(dp[j], dp[j - 1]) + arr[i][j];
}
}
return dp[m - 1];
}
}
//根据上面的 minPath2 方法进行优化,参考了《程序员代码面试指南》
public static int minPath3(int[][] arr,int n,int m){
if(arr == null)
return 0;
//比较行数 和 列数大小
int more = Math.max(n, m);
int less = Math.min(n, m);
boolean rowmore = (more == n); //标志变量,行数是否大于列数
int[] dp = new int[less];
dp[0] = arr[0][0];
for(int i = 1; i < less; i++){
dp[i] = dp[i - 1] + (rowmore ? arr[0][i] : arr[i][0]); //如果行数大于列数,则加上arr[0][i]
}
for(int i = 1; i < more; i++){
dp[0] = dp[0] + (rowmore ? arr[i][0] : arr[0][i]);
for(int j = 1; j < less; j++){
dp[j] = Math.min(dp[j - 1], dp[j]) + (rowmore ? arr[i][j] : arr[j][i]);
}
}
return dp[less - 1];
}
minPath2 的方法容易理解,但是代码太冗余
minPath3 方法的精髓在于设置了一个标志标量 rowmore ,用于选择我们应该先遍历每一行还是先遍历每一列,细细体会。