【LeetCode 不同路径123】使用动态规划三步骤解决

一、动态规划三大步骤

之前在微信公众号【帅地玩编程】上看到有人分享,解决动态规划问题的三大步骤,今天使用这个解题套路,解决LeetCode中不同路径问题。

这个问题,LeetCode中共有三题,难度从由低到高,分别为【62题 不同路径】、【63题 不同路径2】、【980题 不同路径3】。

先上解题套路(下面是作者原话):

动态规划,无非就是利用历史记录,来避免我们的重复计算。而这些历史记录,我们得需要一些变量来保存,一般是用一维数组或者二维数组来保存。下面我们先来讲下做动态规划题很重要的三个步骤,

第一步骤:定义数组元素的含义,上面说了,我们会用一个数组,来保存历史数组,假设用一维数组 dp[] 吧。这个时候有一个非常非常重要的点,就是规定你这个数组元素的含义,例如你的 dp[i] 是代表什么意思?

第二步骤:找出数组元素之间的关系式,我觉得动态规划,还是有一点类似于我们高中学习时的归纳法的,当我们要计算 dp[n] 时,是可以利用 dp[n-1],dp[n-2]…..dp[1],来推出 dp[n] 的,也就是可以利用历史数据来推出新的元素值,所以我们要找出数组元素之间的关系式,例如 dp[n] = dp[n-1] + dp[n-2],这个就是他们的关系式了。而这一步,也是最难的一步,后面我会讲几种类型的题来说。

学过动态规划的可能都经常听到最优子结构,把大的问题拆分成小的问题,说时候,最开始的时候,我是对最优子结构一梦懵逼的。估计你们也听多了,所以这一次,我将换一种形式来讲,不再是各种子问题,各种最优子结构。所以大佬可别喷我再乱讲,因为我说了,这是我自己平时做题的套路。

第三步骤:找出初始值。学过数学归纳法的都知道,虽然我们知道了数组元素之间的关系式,例如 dp[n] = dp[n-1] + dp[n-2],我们可以通过 dp[n-1] 和 dp[n-2] 来计算 dp[n],但是,我们得知道初始值啊,例如一直推下去的话,会由 dp[3] = dp[2] + dp[1]。而 dp[2] 和 dp[1] 是不能再分解的了,所以我们必须要能够直接获得 dp[2] 和 dp[1] 的值,而这,就是所谓的初始值

不太理解,下面将其应用到题目中就能明白。

二、不同路径

题目(难度:中等):

一个机器人位于一个 m x n 网格的左上角 (起始点在下图中标记为“Start” )。

机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为“Finish”)。

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

步骤一、定义数组元素的含义

由于我们的目的是从左上角到右下角一共有多少种路径,那我们就定义 dp[i] [j]的含义为:当机器人从左上角走到(i, j) 这个位置时,一共有 dp[i] [j] 种路径。那么,dp[m-1] [n-1] 就是我们要的答案了(网格为m*n)。

步骤二:找出关系数组元素间的关系式

想象以下,机器人要怎么样才能到达 (i, j) 这个位置?由于机器人可以向下走或者向右走,所以有两种方式到达

一种是从 (i-1, j) 这个位置走一步到达

一种是从(i, j - 1) 这个位置走一步到达

因为是计算所有可能的步骤,所以是把所有可能走的路径都加起来,所以关系式是 dp[i] [j] = dp[i-1] [j] + dp[i] [j-1]。

步骤三、找出初始值

显然,当 dp[i] [j] 中,如果 i 或者 j 有一个为 0,那么还能使用关系式吗?答是不能的,因为这个时候把 i - 1 或者 j - 1,就变成负数了,数组就会出问题了,所以我们的初始值是计算出所有的 dp[0] [0….n-1] 和所有的 dp[0….m-1] [0]。这个还是非常容易计算的,相当于计算机图中的最上面一行和左边一列。因此初始值如下:

dp[0] [0….n-1] = 1; // 相当于最上面一行,机器人只能一直往左走

dp[0…m-1] [0] = 1; // 相当于最左面一列,机器人只能一直往下走

上代码

public static 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;
        }

        // 通过关系式推导出dp[m-1][n-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];
}

三、不同路径2

题目(难度:中等):

一个机器人位于一个 m x n 网格的左上角 (起始点在下图中标记为“Start” )。

机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为“Finish”)。

现在考虑网格中有障碍物。那么从左上角到右下角将会有多少条不同的路径?

步骤一、定义数组元素的含义

数组含义仍然跟上一题一样,当机器人从左上角走到(i, j) 这个位置时,一共有 dp[i] [j] 种路径。仍然是求dp[m-1] [n-1] 。

步骤二:找出关系数组元素间的关系式

跟上一题相同,机器人要怎么样才能到达 (i, j) 这个位置?由于机器人可以向下走或者向右走,所以有两种方式到达

一种是从 (i-1, j) 这个位置走一步到达

一种是从(i, j - 1) 这个位置走一步到达

因为是计算所有可能的步骤,所以是把所有可能走的路径都加起来,所以关系式是 dp[i] [j] = dp[i-1] [j] + dp[i] [j-1]。

因为有障碍了,如果(i,j)这个位置是障碍,那么不可能到达(i,j)这个位置,那么dp[i] [j] = 0。

步骤三、找出初始值

我们的初始值还是计算出所有的 dp[0] [0….n-1] 和所有的 dp[0….m-1] [0]。相当于计算机图中的最上面一行和左边一列。

dp[0] [0….n-1] = 1; // 相当于最上面一行,机器人只能一直往左走

dp[0…m-1] [0] = 1; // 相当于最左面一列,机器人只能一直往下走

但是跟上一题有个不同的地方,如果最上面一行或者最左边一列存在障碍,假设(0,j)存在障碍,dp[0][j] = 0,很好理解,因为肯定无法到达这个位置;值得注意,dp[0][j+1....n-1] = 0,因为(0,j)存在障碍,那就不可能一直往左走了,所以(0,j)右边都是0。

上代码

public static int UniquePathsWithObstacles(int[][] obstacleGrid) {
        int m= obstacleGrid.length;
        int n = obstacleGrid[0].length;

        int[][] dp = new int[m][n];

        if(obstacleGrid[0][0] == 1){
            return 0;
        }else {
            dp[0][0] = 1;
        }

        // 初始化,如果第一列存在障碍,第一列后面都是0
        for(int i=1; i<m; i++){
            if(obstacleGrid[i][0] == 1){
                dp[i][0] = 0;
            }else{
                dp[i][0] = dp[i-1][0];
            }
        }

        // 初始化,如果第一行存在障碍,第一行后面都是0
        for(int j=1; j<n; j++){
            if(obstacleGrid[0][j] == 1){
                dp[0][j] = 0;
            }else{
                dp[0][j] = dp[0][j-1];
            }
        }

        // 推导出dp[m-1][n-1]
        for(int i=1; i<m; i++){
            for(int j=1; j<n; j++){
                if(obstacleGrid[i][j] == 1){
                    dp[i][j] = 0;
                }else{
                    dp[i][j] = dp[i-1][j] + dp[i][j-1];
                }
            }
        }
        return dp[m-1][n-1];
    }

四、不同路径3

题目(难度:困难):

在二维网格 grid 上,有 4 种类型的方格:

1 表示起始方格。且只有一个起始方格。
2 表示结束方格,且只有一个结束方格。
0 表示我们可以走过的空方格。
-1 表示我们无法跨越的障碍。
返回在四个方向(上、下、左、右)上行走时,从起始方格到结束方格的不同路径的数目,每一个无障碍方格都要通过一次

本题不会用动态规划,实力不允许,找不到数组元素间的关系式,使用递归进行解答。

从起点1开始尝试遍历每一个 0 方格,并且将走过的方格放入一个map,用于标记走过了。当回溯时,必须将标记的方格从map中移除。

如果到达障碍点-1,则返回,此路不通。

如果到达终点2,并且经过所有无障碍方格(map的size等于所有除-1方格数),则表示map中是一条成功路径,成功路径+1

上代码

package dynamicProgram;

import java.util.LinkedHashMap;
import java.util.Map;

public class uniquePathsIII {

    public static void main(String[] args){

        int[][] obstacleGrid = new int[][]{{1,0,0,0},{0,0,0,0},{0,0,2,-1}};
        System.out.println("成功路径总数:" + uniquePathsIII(obstacleGrid));
    }


    public static int uniquePathsIII(int[][] grid) {
        int m= grid.length;
        int n = grid[0].length;

        int starti=0;
        int startj=0;

        int step= 0;
        for(int i=0; i<m; i++){
            for(int j=0; j<n; j++){
                if(grid[i][j] == 0 || grid[i][j] == 1 || grid[i][j] == 2){
                    step++;
                }
                if(grid[i][j] == 1){
                    starti = i;
                    startj = j;
                }
            }
        }

        Map<String, String> map = new LinkedHashMap<>();
        map.put("(" + starti + ", " + startj + ")", "0");

        int ret = 0;
        if(starti + 1 < grid.length){
            ret +=  access(starti+1, startj, grid, step, map);
        }
        if(starti - 1 >= 0){
            ret +=  access(starti - 1, startj, grid, step, map);
        }
        if(startj + 1 < grid[0].length){
            ret +=  access(starti, startj+1, grid, step, map);
        }
        if(startj - 1 >= 0){
            ret +=  access(starti, startj-1, grid, step, map);
        }

        return ret;
    }

    public static int access(int x, int y, int[][] grid, int step, Map<String, String> map) {
        // 标记当前方格走过
        map.put("(" + x + ", " + y + ")", "0");

        // 如果到达终点,并且走遍所有无障碍方格,则为一条成功路径
        if (grid[x][y] == 2 && step == map.size()) {
            // 打印当前成功路径
            System.out.println("成功路径;");
            for(Map.Entry<String, String> mapEntry : map.entrySet()){
                System.out.print(mapEntry.getKey() + " - ");
            }
            System.out.println();

            // 因为map相当于全局变量,一定移除走过的方格
            map.remove("(" + x + ", " + y + ")");
            return 1;
        }

        if (grid[x][y] == -1) {
            // 因为map相当于全局变量,一定移除走过的方格
            map.remove("(" + x + ", " + y + ")");
            return 0;
        }

        int ret = 0;

        // 上下左右四个方向,并且是没走过的方格,都走一遍
        if (x + 1 < grid.length && (map.get("(" + (x + 1) + ", " + y + ")") == null)) {
            ret += access(x + 1, y, grid, step, map);
        }

        if (y + 1 < grid[0].length && (map.get("(" + x + ", " + (y + 1) + ")") == null)) {
            ret += access(x, y + 1, grid, step, map);
        }

        if (x - 1 >= 0 && (map.get("(" + (x - 1) + ", " + y + ")") == null)) {
            ret += access(x - 1, y, grid, step, map);
        }

        if (y - 1 >= 0 && (map.get("(" + x + ", " + (y - 1) + ")") == null)) {
            ret += access(x, y - 1, grid, step, map);
        }

        // 因为map相当于全局变量,一定移除走过的方格
        map.remove("(" + x + ", " + y + ")");

        return ret;
    }
}

我打印了成功路径,看看结果

上面代码从写法上,比较容易理解,再上一版,从代码写法上简洁点的,逻辑相同。

上代码:

package dynamicProgram;

public class UniquePathsIIIConcise {

    int ans;
    int[][] grid;
    int tr, tc;
    // 表示四个方向,不用四个if语句判断了
    int[] dr = new int[]{0, -1, 0, 1};
    int[] dc = new int[]{1, 0, -1, 0};
    int R, C;

    public static void main(String[] args){

        int[][] obstacleGrid = new int[][]{{1,0,0,0},{0,0,0,0},{0,0,2,-1}};
        UniquePathsIIIConcise uniquePathsIIIConcise = new UniquePathsIIIConcise();
        System.out.println("成功路径总数:" + uniquePathsIIIConcise.uniquePathsIII(obstacleGrid));
    }

    public int uniquePathsIII(int[][] grid) {
        this.grid = grid;
        R = grid.length;
        C = grid[0].length;

        int todo = 0;
        int sr = 0, sc = 0;
        for (int r = 0; r < R; ++r)
            for (int c = 0; c < C; ++c) {
                if (grid[r][c] != -1) {
                    todo++;
                }

                if (grid[r][c] == 1) {
                    sr = r;
                    sc = c;
                } else if (grid[r][c] == 2) {
                    tr = r;
                    tc = c;
                }
            }

        ans = 0;
        dfs(sr, sc, todo);
        return ans;
    }

    public void dfs(int r, int c, int todo) {
        todo--;
        if (todo < 0) return;
        // 到达终点,并且经过所有无障碍方格
        if (r == tr && c == tc) {
            if (todo == 0) ans++;
            return;
        }

        grid[r][c] = 3;
        for (int k = 0; k < 4; ++k) {
            int nr = r + dr[k];
            int nc = c + dc[k];
            if (0 <= nr && nr < R && 0 <= nc && nc < C) {
                // 没经过,并且不是障碍方格-1
                if (grid[nr][nc] % 2 == 0)
                    dfs(nr, nc, todo);
            }
        }
        grid[r][c] = 0;
    }
}

 

<think>好的,用户想查找蓝桥杯竞赛中关于“行按键”问题的代码实现或解决方案。首先,我需要明确“行按键”具体指的是哪类题目。蓝桥杯的题目通常涉及算法和编程,可能涉及状态机、动态规划或模拟类问题。 根据经验,“行按键”可能类似于手机T9键盘的输入问题,或者是某种需要处理多行按键输入的算法题。例如,用户可能需要根据按键序列生成所有可能的字母组合,这在LeetCode上有类似题目,如第17题的电话号码字母组合。这类问题通常使用回溯算法解决。 接下来,我需要验证是否存在蓝桥杯真题中的类似题目。例如,蓝桥杯往届题目中可能有涉及按键输入的模拟题,比如处理多次按键后的状态变化,或者计算特定按键序列的可能结果数。假设题目要求每行按键只能按一次,或者有特定的规则,可能需要使用深度优先搜索(DFS)或动态规划来记录状态。 然后,考虑用户可能需要具体的代码实现。例如,一个典型的解决方案是使用递归或回溯来生成所有可能的组合。假设按键对应的字母如下:第一行按键对应字母A-C,第二行D-F,第行G-I,依此类推。代码需要将数字映射到字母,然后遍历所有可能的组合。 另外,需要注意输入可能包含重复数字或需要处理边界情况,如空输入。代码的结构应包括映射关系的建立、回溯函数的编写以及结果的收集。同时,需要处理数字到字母的转换,比如数字2对应"abc",数字3对应"def"等。 最后,确保代码符合蓝桥杯的输入输出格式,通常是从标准输入读取,处理后再输出结果。可能还需要考虑时间复杂度和空间复杂度,以应对大规模输入的情况,避免超时。例如,使用回溯算法的时间复杂度为O(3^N),对于较小的N是可行的。 总结可能的解决方案步骤:建立数字到字母的映射,使用回溯递归生成所有组合,处理输入字符串,并返回结果列表。验证示例输入输出,如输入"23"应输出["ad","ae","af","bd","be","bf","cd","ce","cf"]。</think>关于蓝桥杯竞赛中“行按键”问题的代码实现,根据常见题型分析,这类问题可能涉及**按键状态转换**或**字母组合生成**。例如,类似LeetCode 17题“电话号码的字母组合”[^1],但需适配行按键的特殊规则。以下是典型解决方案: ### 问题假设 假设行按键对应字母如下(示例): - 第1行按键:`1 -> "ABC"` - 第2行按键:`2 -> "DEF"` - 第3行按键:`3 -> "GHI"` 输入为按键序列(如`"123"`),需生成所有可能的字母组合。 ### 代码实现 ```python def letter_combinations(digits): if not digits: return [] phone_map = { &#39;1&#39;: &#39;ABC&#39;, &#39;2&#39;: &#39;DEF&#39;, &#39;3&#39;: &#39;GHI&#39; } result = [] backtrack("", digits, 0, phone_map, result) return result def backtrack(combination, digits, index, phone_map, result): if index == len(digits): result.append(combination) return current_digit = digits[index] letters = phone_map[current_digit] for letter in letters: backtrack(combination + letter, digits, index + 1, phone_map, result) # 示例输入输出 print(letter_combinations("123")) # 输出[&#39;ADG&#39;,&#39;ADH&#39;,&#39;ADI&#39;,&#39;AEG&#39;,..., &#39;CFI&#39;]共27种组合 ``` ### 关键点解析 1. **映射关系**:需明确按键数字与字母的对应规则 2. **回溯算法**:通过递归遍历所有可能的组合路径 3. **边界处理**:空输入直接返回空列表
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值