算法-动态规划-最小路径和
1 题目概述
1.1 题目出处
https://leetcode-cn.com/problems/minimum-path-sum/
1.2 题目描述
给定一个无序的整数数组,找到其中最长上升子序列的长度。
示例:
输入: [10,9,2,5,3,7,101,18]
输出: 4
解释: 最长的上升子序列是 [2,3,7,101],它的长度是 4。
说明:
可能会有多种最长上升子序列的组合,你只需要输出对应的长度即可。
你算法的时间复杂度应该为 O(n2) 。
进阶: 你能将算法的时间复杂度降低到 O(n log n) 吗?
2 BFS
2.1 思路
从左上角开始,进行BFS,每次拿出队首元素,然后分别计算该元素的路径和加上右侧和下侧元素的数值之和
,看是否分别小于当前右和下侧元素的路径和,如果小于就更新,并且在这些元素未访问过时推入队列末尾进行BFS。
2.2 代码
class Solution {
public int minPathSum(int[][] grid) {
int result = 0;
if(grid == null || grid.length == 0){
return result;
}
int m = grid.length - 1;
int n = grid[0].length - 1;
// 用来存放某节点的最小路径和
int[][] record = new int[m + 1][n + 1];
for(int i = 0; i < record.length; i++){
for(int j = 0; j < record[0].length; j++){
record[i][j] = -1;
}
}
// 思路 从左上开始,分别计算右侧和下侧节点的最小路径和,并推入队列等待计算
LinkedList<int[]> queue = new LinkedList<>();
record[0][0] = grid[0][0];
int[] first = {0, 0, record[0][0]};
queue.add(first);
while(!queue.isEmpty()){
int[] ele = queue.poll();
if(ele[1] < n){
if(record[ele[0]][ele[1] + 1] == -1){
record[ele[0]][ele[1] + 1] = record[ele[0]][ele[1]] + grid[ele[0]][ele[1] + 1];
int[] right = {ele[0], ele[1] + 1, record[ele[0]][ele[1] + 1]};
queue.add(right);
}else{
record[ele[0]][ele[1] + 1] = Math.min(record[ele[0]][ele[1] + 1], record[ele[0]][ele[1]] + grid[ele[0]][ele[1] + 1]);
}
}
if(ele[0] < m){
if(record[ele[0] + 1][ele[1]] == -1){
record[ele[0] + 1][ele[1]] = record[ele[0]][ele[1]] + grid[ele[0] + 1][ele[1]];
int[] below = {ele[0] + 1, ele[1], record[ele[0] + 1][ele[1]]};
queue.add(below);
}else{
record[ele[0] + 1][ele[1]] = Math.min(record[ele[0] + 1][ele[1]], record[ele[0]][ele[1]] + grid[ele[0] + 1][ele[1]]);
}
}
}
return record[m][n];
}
}
2.3 时间复杂度
O(m*n)
2.4 空间复杂度
O(m*n)
3 DFS
3.1 思路
既然写了BFS,那为了练习我们可以来个DFS。
但有个问题,如果采用DFS,先遍历右侧,那可能导致有些节点因为已经被访问过而没有更新更小的值!
也就是说,我们不能像之前那样不管已经访问过的节点,需要再次访问!
3.2 代码
class Solution {
private int m = 0;
private int n = 0;
public int minPathSum(int[][] grid) {
int result = 0;
if(grid == null || grid.length == 0){
return result;
}
m = grid.length - 1;
n = grid[0].length - 1;
// 用来存放某节点的最小路径和
int[][] record = new int[m + 1][n + 1];
for(int i = 0; i < record.length; i++){
for(int j = 0; j < record[0].length; j++){
record[i][j] = -1;
}
}
record[0][0] = grid[0][0];
dfs(grid, record, 0, 0);
return record[m][n];
}
private void dfs(int[][] grid, int[][] record, int i, int j){
if(j < n){
if(record[i][j + 1] == -1){
record[i][j + 1] = record[i][j] + grid[i][j + 1];
}else{
record[i][j + 1] = Math.min(record[i][j + 1], record[i][j] + grid[i][j + 1]);
}
dfs(grid, record, i, j+1);
}
if(i < m){
if(record[i + 1][j] == -1){
record[i + 1][j] = record[i][j] + grid[i + 1][j];
}else{
record[i + 1][j] = Math.min(record[i + 1][j], record[i][j] + grid[i + 1][j]);
}
dfs(grid, record, i+1, j);
}
}
}
3.3 时间复杂度
果然,超出了时间限制!
4 DFS-优化
4.1 思路
从右下往左上进行DFS:
record[i][j] = grid[i][j] + Math.min(dfs(grid, record, i - 1, j), dfs(grid, record, i, j - 1));
还是采用record进行记录,但注意这里因为已经是最终的最小路径和,所以重复访问时可以直接返回该值,不需要再进行计算!
4.2 代码
class Solution {
private int m = 0;
private int n = 0;
public int minPathSum(int[][] grid) {
int result = 0;
if(grid == null || grid.length == 0){
return result;
}
m = grid.length - 1;
n = grid[0].length - 1;
// 1.缓存最小路径和
// 2.标识是否访问过
Integer[][] record = new Integer[m + 1][n + 1];
// 思路 从右下开始往左和上推
return dfs(grid, record, m, n);
}
private int dfs(int[][] grid, Integer[][] record, int i, int j){
if(i < 0 || j < 0){
// 越界
return Integer.MAX_VALUE;
}
if(i == 0 && j == 0){
return grid[0][0];
}
if(record[i][j] != null){
return record[i][j];
}
record[i][j] = grid[i][j] + Math.min(dfs(grid, record, i - 1, j), dfs(grid, record, i, j - 1));
return record[i][j];
}
}
4.3 时间复杂度
O(m*n)
4.4 空间复杂度
O(m*n)
5 动态规划1
5.1 思路
前面BFS看起来不错,但其实好像没有必要用队列,而且速度也很慢。我们改成动态规划试试。
因为我们遍历采用的是从左到右,从上到下,所以是可以保证每个节点都是最小路径和。
动态规划转移方程如下:
dp[i][j] = Math.min(dp[i - 1][j], dp[i][j-1]) + grid[i-1][j-1];
其中dp[i][j]表示该节点的最小路径和。
5.2 代码
class Solution {
public int minPathSum(int[][] grid) {
int result = 0;
if(grid == null || grid.length == 0){
return result;
}
int m = grid.length;
int n = grid[0].length;
// 用来存放某节点的最小路径和
int[][] dp = new int[m + 1][n + 1];
// 在外围虚拟一圈,除了grid[0][0]的左和上为0外,其他值都为Integer.MAX_VALUE;
// 这样做的目的是为了动态规划遍历中不用管边界值
int[] zeroRow = dp[0];
for(int i = 2; i < zeroRow.length; i++){
zeroRow[i] = Integer.MAX_VALUE;
}
for(int i = 2; i < m + 1; i++){
dp[i][0] = Integer.MAX_VALUE;
}
for(int i = 1; i <= m; i++){
for(int j = 1; j <= n; j++){
dp[i][j] = Math.min(dp[i - 1][j], dp[i][j-1]) + grid[i-1][j-1];
}
}
return dp[m][n];
}
}
5.3 时间复杂度
O(m*n)
这次速度快很多了!
5.4 空间复杂度
O(m*n)
6 动态规划2
6.1 思路
前面动态规划1看起来很妙,但还用了额外空间。
仔细观察,其实按上面方法遍历,每个元素的初始值只用了一次,那么可以复用原始数组grid,遍历过程中将计算的最小路径和放入里面。也就是说grid[i][j]最终表示该节点的最小路径和。
这样,最后直接返回grid[m-1][n-1]即可
6.2 代码
class Solution {
public int minPathSum(int[][] grid) {
int result = 0;
if(grid == null || grid.length == 0){
return result;
}
int m = grid.length;
int n = grid[0].length;
for(int i = 0; i < m; i++){
for(int j = 0; j < n; j++){
if(i + j == 0){
continue;
}else if (i == 0){
grid[i][j] = grid[i][j-1] + grid[i][j];
}else if (j == 0){
grid[i][j] = grid[i - 1][j] + grid[i][j];
}else{
grid[i][j] = Math.min(grid[i - 1][j], grid[i][j-1]) + grid[i][j];
}
}
}
return grid[m-1][n-1];
}
}
6.3 时间复杂度
O(m*n)
6.4 空间复杂度
O(1)
因为复用原数组grid,没有额外空间。