Leetcode --- 矩阵路径问题(动态规划)

简介: Leetcode --- 矩阵路径问题(动态规划)

1.最小路径和(64 - 中)



题目描述:给定一个包含非负整数的 m x n 网格 grid ,请找出一条从左上角到右下角的路径,使得路径上的数字总和为最小。


说明:每次只能向下或者向右移动一步。


示例 :

输入:grid = [[1,3,1],[1,5,1],[4,2,1]]
输出:7
解释:因为路径 1→3→1→1→1 的总和最小。


思路:典型动态规划,定义dp数组记录累加值,状态转移方程为:dp[i][j] = Math.min(dp[i - 1][j], dp[i][j - 1]) + grid[i][j]


代码实现:

public int minPathSum(int[][] grid) {
    if (grid == null || grid.length == 0 || grid[0] == null || grid[0].length == 0) return 0;
    int m = grid.length, n = grid[0].length;
    int[][] dp = new int[m][n];
    dp[0][0] = grid[0][0];
    for(int i = 1; i < m; i++) {
        dp[i][0] = dp[i - 1][0] + grid[i][0];
    }
    for(int i = 1; i < n; i++) {
        dp[0][i] = dp[0][i - 1] + grid[0][i];
    }
    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][j];    
        }
    }
    return dp[m - 1][n - 1];
}


空间优化:空间复杂度O(n),滚动数组:因为i行依赖i行和i - 1行,所以我们计算完i行覆盖i - 1行的值,不会对最终结果。

public int minPathSum(int[][] grid) {
    int m = grid.length, n = grid[0].length;
    int[] dp = new int[n];
    for (int i = 0; i < m; ++i) {
        for (int j = 0; j < n; ++j) {
            if (j == 0) {  // 只能从上边界来
                dp[j] = dp[j];
            } else if (i == 0) { // 只能从左边界来
                dp[j] = dp[j - 1];
            } else {
                dp[j] = Math.min(dp[j - 1], dp[j]);
            } 
            dp[j] += grid[i][j];
        }
    } 
    return dp[n - 1];
}


2.不同路径(62 - 中)



题目描述:一个机器人位于一个 m x n 网格的左上角 (起始点在下图中标记为 “Start” )。机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为 “Finish” )。


问总共有多少条不同的路径?


示例 :

输入:m = 3, n = 7
输出:28


思路:典型动态规划问题,与上题不同的是dp数组记录的是到达该点的路径总数,状态转移方程为:dp[i][j] = dp[i - 1][j] + dp[i][j - 1]。注意dp数组初始值为1,即到达该位置的路径为1.


代码实现:

public int uniquePaths(int m, int n) {
    int[][] dp = new int[m][n];
    for (int i = 0; i < m; ++i) {
        dp[i][0] = 1;
    }
    for (int j = 0; j < n; ++j) {
        dp[0][j] = 1;
    }
    for (int i = 1; i < m; ++i) {
        for (int j = 1; j < n; ++j) {
            dp[i][j] = dp[i - 1][j] + dp[i][j - 1];
        }
    }
    return dp[m - 1][n - 1];
}


空间优化:空间复杂度O(min(m, n)),同理。

public int uniquePaths(int m, int n) {
    int less = Math.min(m, n), more = Math.max(m, n);
    int[] dp = new int[less];
    Arrays.fill(dp, 1);
    for (int i = 1; i < more; ++i) {
        for (int j = 1; j < less; ++j) {
            dp[j] = dp[j] + dp[j - 1];
        }
    }
    return dp[less - 1];
}


3.不同路径II(63 - 中)



题目描述:一个机器人位于一个 m x n 网格的左上角 (起始点在下图中标记为 “Start” )。机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为 “Finish” )。


现在考虑网格中有障碍物。问总共有多少条不同的路径?


示例 :

输入:obstacleGrid = [[0,0,0],[0,1,0],[0,0,0]]
输出:2
解释:
3x3 网格的正中间有一个障碍物。
从左上角到右下角一共有 2 条不同的路径:
1. 向右 -> 向右 -> 向下 -> 向下
2. 向下 -> 向下 -> 向右 -> 向右


思路:与上题类似,注意更新dp数组进行判断,该点如果是障碍则跳过。注意:本题不能进行空间压缩。


代码实现:

public int uniquePathsWithObstacles(int[][] obstacleGrid) {
    int m = obstacleGrid.length, n = obstacleGrid[0].length;
    int[][] dp = new int[m][n];
    // 处理边界条件
    for (int i = 0; i < m && obstacleGrid[i][0] == 0; ++i) dp[i][0] = 1;
    for (int j = 0; j < n && obstacleGrid[0][j] == 0; ++j) dp[0][j] = 1;
    // 顺序填表
    for (int i = 1; i < m; ++i) {
        for (int j = 1; j < n; ++j) {
            //当前位置为障碍物
            if (obstacleGrid[i][j] == 1) continue;
            dp[i][j] = dp[i][j - 1] + dp[i - 1][j];
        }
    }
    return dp[m - 1][n - 1];
}


拓展:三角形最小路径和(120 - 中)



题目描述:给定一个三角形 triangle ,找出自顶向下的最小路径和。


每一步只能移动到下一行中相邻的结点上。相邻的结点 在这里指的是 下标 与 上一层结点下标 相同或者等于 上一层结点下标 + 1 的两个结点。也就是说,如果正位于当前行的下标 i ,那么下一步可以移动到下一行的下标 i 或 i + 1 。


示例 :

输入:triangle = [[2],[3,4],[6,5,7],[4,1,8,3]]
输出:11
解释:如下面简图所示:
   2
  3 4
 6 5 7
4 1 8 3
自顶向下的最小路径和为 11(即,2 + 3 + 5 + 1 = 11)。


思路:动态规划,dp数组:到达某层的最小路径和,由题意的依赖关系,转移方程应为:dp[j] = Math.min(dp[j], dp[j + 1]) + triangle.get(i).get(j),即从下向上填表。注意:由于i依赖于i和i + 1,所以外层循环要倒叙,空间压缩时防止覆盖。


代码实现:

public int minimumTotal(List<List<Integer>> triangle) {
    int n = triangle.size();
    int[] dp = new int[n + 1]; 
    for (int i = n - 1; i >= 0; --i) {
        for (int j = 0; j <= i; ++j) {
            dp[j] = Math.min(dp[j], dp[j + 1]) + triangle.get(i).get(j);
        }
    }
    return dp[0];
}


拓展:圆环回原点问题(字节高频题)



题目描述:圆环上有10个点,编号为0~9。从0点出发,每次可以逆时针和顺时针走一步,问走n步回到0点共有多少种走法。举例:


  • 如果n=1,则从0出发只能到1或者9,不可能回到0,共0种走法
  • 如果n=2,则从0出发有4条路径:0->1->2, 0->1->0, 0->9->8, 0->9->0,其中有两条回到了0点,故一共有2种走法


示例

输入: 2
输出: 2
解释:有2种方案。分别是0->1->0和0->9->0


思路:本题考察点是动态规划,与Leetcode70爬楼梯相似。


  • dp[i][j] 为从0点出发走 i步j点 的方案数
  • 状态转移方程:走 n 步到 0 的方案数=走 n-1 步到 1 的方案数+走 n-1 步到 9 的方案数(j步达到i点的问题,转化为j-1步从相邻的两个节点到达目标节点的方法数之和)。即 dp[i][j] = dp[i - 1][(j - 1 + n) % n] + dp[i - 1][(j + 1) % n]
  • ps:公式之所以取余是因为 j-1或  j+1 可能会超过圆环0~9的范围


代码

import java.util.Scanner;
public class Solution005 {
    public static void main(String[] args) {
        System.out.println("请输入圆环节点个数和步数n:");
        Scanner sc = new Scanner(System.in);
        int length = sc.nextInt();
        int n = sc.nextInt();
        System.out.println(solution(length, n));
    }
    public static int solution(int n, int k) {
        if (n == 0) {
            return 1;
        }
        // 只有两个环,偶数步数有一种方案,奇数步数不能到达
        if (n == 2) {
            if (k % 2 == 0) {
                return 1;
            } else {
                return 0;
            }
        }
        // dp[i][j]为从0点出发走i步到j点的方案数
        int[][] dp = new int[k + 1][n];
        dp[0][0] = 1;
        // 0步到达任何位置(除0)的方法数为0,java可省略
        for (int j = 1; j < n; j++) {
            dp[0][j] = 0;
        }
        for (int i = 1; i <= k; i++) {
            for (int j = 0; j < n; j++) {
                dp[i][j] = dp[i - 1][(j - 1 + n) % n] + dp[i - 1][(j + 1) % n];
            }
        }
        return dp[k][0];
    }
}


拓展:如果本题不是返回原点,而是k步到达3这个点的方法总数,只需要返回 dp[k][3] 即可。


拓展:交错字符串(97-中)



题目描述:给定三个字符串 s1s2s3,请你帮忙验证 s3 是否是由 s1s2交错 组成的。每个字符串被分成长度若干的子串,验证这些子串是否可以交错拼接 s3


示例

输入:s1 = "aabcc", s2 = "dbbca", s3 = "aadbbcbcac"
输出:true
s3 = aa ddbc bc a c
输入:s1 = "aabcc", s2 = "dbbca", s3 = "aadbbbaccc"
输出:false
s3不能由s1和s2交错组成。


思路:参考@sage大佬,上图


image.png

问题转化为:每次只能向右或者向下选择字符,问是否存在target路径。注意当len(s1) + len(s2) != len(s3)时一定不能交错匹配,否则进行动态规划求解:


  • dp[i][j]代表 s1 前 i 个字符与 s2 前 j 个字符拼接成 s3 的 i+j 字符,也就是存在目标路径能够到达 i ,j ;
  • 状态转移方程:到达(i, j)可能由上边位置过来或者左边位置过来


ps:边界条件注意,字符串都为空,返回true;


代码

public boolean isInterleave(String s1, String s2, String s3) {
    int n1 = s1.length(), n2 = s2.length();
    if (s3.length() != n1 + n2) {
        return false;
    }
    // dp[i,j]表示s1前i字符能与s2前j字符组成s3前i+j个字符;
    boolean[][] dp = new boolean[n1 + 1][n2 + 1];
    dp[0][0] = true;
    for (int i = 1; i <= n1 && s1.charAt(i - 1) == s3.charAt(i - 1); i++) dp[i][0] = true;
    for (int j = 1; j <= n2 && s2.charAt(j - 1) == s3.charAt(j - 1); j++) dp[0][j] = true;
    for (int i = 1; i <= n1; i++) {
        for (int j = 1; j <= n2; j++) {
            dp[i][j] = dp[i - 1][j] && s1.charAt(i - 1) == s3.charAt(i + j - 1) 
            || dp[i][j - 1] && s2.charAt(j - 1) == s3.charAt(i + j - 1);
        }
    }
    return dp[n1][n2];
}


相关文章
【LeetCode 35】112.路径总和
【LeetCode 35】112.路径总和
196 0
|
存储 算法 NoSQL
LeetCode第73题矩阵置零
文章介绍了LeetCode第73题"矩阵置零"的解法,通过使用矩阵的第一行和第一列作为标记来记录哪些行或列需要置零,从而在不增加额外空间的情况下解决问题。
LeetCode第73题矩阵置零
【LeetCode 36】113.路径总和II
【LeetCode 36】113.路径总和II
170 0
|
Go 开发者 索引
【LeetCode 热题100】路径与祖先:二叉树中的深度追踪技巧(力扣33 / 81/ 153/154)(Go语言版)
本文深入探讨了LeetCode中四道关于「搜索旋转排序数组」的经典题目,涵盖了无重复和有重复元素的情况。通过二分查找的变形应用,文章详细解析了每道题的解题思路和Go语言实现代码。关键点包括判断有序区间、处理重复元素以及如何缩小搜索范围。文章还总结了各题的异同,并推荐了类似题目,帮助读者全面掌握二分查找在旋转数组中的应用。无论是初学者还是有经验的开发者,都能从中获得实用的解题技巧和代码实现方法。
543 14
|
算法 Go
【LeetCode 热题100】深入理解二叉树结构变化与路径特性(力扣104 / 226 / 114 / 543)(Go语言版)
本博客深入探讨二叉树的深度计算、结构变换与路径分析,涵盖四道经典题目:104(最大深度)、226(翻转二叉树)、114(展开为链表)和543(二叉树直径)。通过递归与遍历策略(前序、后序等),解析每题的核心思路与实现方法。结合代码示例(Go语言),帮助读者掌握二叉树相关算法的精髓。下一讲将聚焦二叉树构造问题,欢迎持续关注!
404 10
|
Go
【LeetCode 热题100】路径与祖先:二叉树中的深度追踪技巧(力扣437 / 236 )(Go语言版)
本文深入探讨二叉树中路径与祖先问题,涵盖两道经典题目:LeetCode 437(路径总和 III)和236(最近公共祖先)。对于路径总和 III,文章分析了双递归暴力解法与前缀和优化方法,后者通过哈希表记录路径和,将时间复杂度从O(n²)降至O(n)。在最近公共祖先问题中,采用后序遍历递归查找,利用“自底向上”的思路确定最近公共祖先节点。文中详细解析代码实现与核心要点,帮助读者掌握深度追踪技巧,理解树结构中路径与节点关系的本质。这类问题在面试中高频出现,掌握其解法意义重大。
331 4
|
算法 Go
【LeetCode 热题100】73:矩阵置零(详细解析)(Go语言版)
这篇文章详细解析了力扣热题 73——矩阵置零问题,提供两种解法:一是使用额外标记数组,时间复杂度为 O(m * n),空间复杂度为 O(m + n);二是优化后的原地标记方法,利用矩阵的第一行和第一列记录需要置零的信息,将空间复杂度降低到 O(1)。文章通过清晰的代码示例与复杂度分析,帮助理解“原地操作”及空间优化技巧,并推荐相关练习题以巩固矩阵操作能力。适合刷题提升算法思维!
423 9
|
机器学习/深度学习 算法 Go
【LeetCode 热题100】139:单词拆分(动态规划全解析+细节陷阱)(Go语言版)
本题是 LeetCode 热题 139:单词拆分(Word Break),需判断字符串 `s` 是否能由字典 `wordDict` 中的单词拼接而成。通过动态规划(DP)或记忆化搜索解决。DP 中定义布尔数组 `dp[i]` 表示前 `i` 个字符是否可拆分,状态转移方程为:若存在 `j` 使 `dp[j]=true` 且 `s[j:i]` 在字典中,则 `dp[i]=true`。初始条件 `dp[0]=true`。代码实现中用哈希集合优化查找效率。记忆化搜索则从起始位置递归尝试所有切割点。两种方法各有利弊,DP 更适合面试场景。思考扩展包括输出所有拆分方式及使用 Trie 优化大字典查找。
501 6
|
存储 算法 Linux
LeetCode第71题简化路径
文章讲述了LeetCode第71题"简化路径"的解题方法,利用栈的数据结构特性来处理路径中的"."和"..",实现路径的简化。
LeetCode第71题简化路径
LeetCode第64题最小路径和
LeetCode第64题"最小路径和"的解题方法,运用动态规划思想,通过构建一个dp数组来记录到达每个点的最小路径和,从而高效求解。
LeetCode第64题最小路径和