leetcode刷题(129)——576. 出界的路径数

给你一个大小为 m x n 的网格和一个球。球的起始坐标为 [startRow, startColumn] 。你可以将球移到在四个方向上相邻的单元格内(可以穿过网格边界到达网格之外)。你 最多 可以移动 maxMove 次球。

给你五个整数 m、n、maxMove、startRow 以及 startColumn ,找出并返回可以将球移出边界的路径数量。因为答案可能非常大,返回对 109 + 7 取余 后的结果。

示例 1:
在这里插入图片描述

输入:m = 2, n = 2, maxMove = 2, startRow = 0, startColumn = 0
输出:6

示例 2:
在这里插入图片描述

输入:m = 1, n = 3, maxMove = 3, startRow = 0, startColumn = 1
输出:12

提示:

1 <= m, n <= 50
0 <= maxMove <= 50
0 <= startRow < m
0 <= startColumn < n

方法一、DFS

刚看到这题,感觉还挺简单的,这不就是一个简单的 DFS 嘛,从给定的起点,一直往下深搜,直到 i 和 j 超出边界了就说明找到了一条路径,如果在给定的移动次数范围内还没有越界,那这条路径就不符合要求。

OK,代码比较简单,请看注释:

class Solution {

    // 四个方向
    int[][] dirs = new int[][] {{0, 1}, {0, -1}, {1, 0}, {-1, 0}};
    // 取余
    int MOD = 1000000007;

    public int findPaths(int m, int n, int maxMove, int startRow, int startColumn) {
        return dfs(m, n, maxMove, startRow, startColumn);
    }

    private int dfs(int m, int n, int moveCount, int i, int j) {
        // 越界了就找到了一条路径
        if (i < 0 || j < 0 || i >= m || j >= n) {
            return 1;
        }

        // 没有移动次数了,返回0
        if (moveCount == 0) {
            return 0;
        }

        // 从这个点出发的符合条件的路径数量
        int sum = 0;
        for (int[] dir : dirs) {
            // 记得取余
            sum = (sum + dfs(m, n, moveCount - 1, i + dir[0], j + dir[1])) % MOD;
        }
        
        return sum;
    }
}

超时:
在这里插入图片描述

方法二、DFS + 剪枝

既然普通的DFS无法满足条件,肯定是需要加上一些剪枝的技巧的,那我们来看看哪些地方可以剪枝呢?

试想,给定如下网络,小球在中间的位置,给定的移动次数为2,可以看到这时候小球不管怎么移动,都不会超出网格。
在这里插入图片描述
所以,剪枝技巧就是每次DFS的时候判断如果小球不管怎么移动都无法超出网格,那从这个点开始往后的枝就都可以剪掉了,简单修改下代码即可:

class Solution {

    // 四个方向
    int[][] dirs = new int[][] {{0, 1}, {0, -1}, {1, 0}, {-1, 0}};
    // 取余
    int MOD = 1000000007;

    public int findPaths(int m, int n, int maxMove, int startRow, int startColumn) {
        return dfs(m, n, maxMove, startRow, startColumn);
    }

    private int dfs(int m, int n, int moveCount, int i, int j) {
        // 越界了就找到了一条路径
        if (i < 0 || j < 0 || i >= m || j >= n) {
            return 1;
        }

        // 没有移动次数了,返回0
        if (moveCount == 0) {
            return 0;
        }

        // 剪枝:如果小球不管怎么移动都无法越出网格,那就剪掉这个枝
        if (i - moveCount >= 0 && j - moveCount >= 0 && i + moveCount < m && j + moveCount < n) {
            return 0;
        }

        // 从这个点出发的符合条件的路径数量
        int sum = 0;
        for (int[] dir : dirs) {
            // 记得取余
            sum = (sum + dfs(m, n, moveCount - 1, i + dir[0], j + dir[1])) % MOD;
        }

        return sum;
    }
}

依然超时:
在这里插入图片描述

方法三、记忆化搜索

剪枝也不行,我们再深入思考一下。

请看下图,假设我们的起始位置是在 A 位置,最多可以移动 6 步,我们可以很容易地发现,会有很多次经过B位置的情况,而从 B 位置出去我们只需要计算一次就可以了,比如,图中列举了三种到达 B 位置的情况。

在这里插入图片描述
所以,第三种方法,我们需要增加一个缓存,记录下来从每个位置在给定移动次数的范围内可以越界的次数,这就是记忆化搜索。

请看代码,我们增加了一个三维数组作为缓存,前两维表示位置,第三维表示移动次数:

class Solution {

    // 四个方向
    int[][] dirs = new int[][] {{0, 1}, {0, -1}, {1, 0}, {-1, 0}};
    // 取余
    int MOD = 1000000007;

    public int findPaths(int m, int n, int maxMove, int startRow, int startColumn) {
        // 缓存
        int[][][] memo = new int[m][n][maxMove + 1];
        for (int i = 0; i < m; i++) {
            for (int j = 0; j < n; j++) {
                for (int k = 0; k <= maxMove; k++) {
                    memo[i][j][k] = -1;
                }
            }
        }
        return dfs(m, n, maxMove, startRow, startColumn, memo);
    }

    private int dfs(int m, int n, int moveCount, int i, int j, int[][][] memo) {
        // 越界了就找到了一条路径
        if (i < 0 || j < 0 || i >= m || j >= n) {
            return 1;
        }

        // 没有移动次数了,返回0
        if (moveCount == 0) {
            return 0;
        }

        // 缓存中存在
        if (memo[i][j][moveCount] != -1) {
            return memo[i][j][moveCount];
        }

        // 剪枝:如果小球不管怎么移动都无法越出网格,那就剪掉这个枝
        if (i - moveCount >= 0 && j - moveCount >= 0 && i + moveCount < m && j + moveCount < n) {
            return 0;
        }

        // 从这个点出发的符合条件的路径数量
        int sum = 0;
        for (int[] dir : dirs) {
            // 记得取余
            sum = (sum + dfs(m, n, moveCount - 1, i + dir[0], j + dir[1], memo)) % MOD;
        }

        // 记录缓存
        memo[i][j][moveCount] = sum;

        return sum;
    }
}

时间复杂度:O(m * n * maxMove),构建memo及每个位置向下搜索的过程中都是O(m * n * maxMove)的时间复杂度。
空间复杂度:O(m * n * maxMove),memo数组需要额外占用O(m * n * maxMove)的空间。

运行结果:通过

方法四、动态规划

一般来说,能使用记忆化搜索的题目都可以使用动态规划来解。
在这里插入图片描述
从图中可以看出,A位置的结果是来自于它的上下左右四个方向的结果,所以,我们这样来定义动态规划:

dp[i][j][k]表示从 [i,j] 位置最多移动 k 次能够把小球移出去的最大路径数量;
dp[i][j][k] = dp[i-1][j][k-1] + dp[i+1][j][k-1] + dp[i][j-1][k-1] + dp[i][j+1][k-1];
注意边界条件,如果是正方形的四个顶点,有两种方法越界,其他边上的位置只有一种方法越界。
在这里插入图片描述
另外,要注意移动次数2的都是从移动次数为1的扩展来的,同理,移动次数3的都是从移动次数为2的扩展来的,所以要注意循环的顺序。

代码如下:

class Solution {

    // 四个方向
    int[][] dirs = new int[][] {{0, 1}, {0, -1}, {1, 0}, {-1, 0}};
    // 取余
    int MOD = 1000000007;

    public int findPaths(int m, int n, int maxMove, int startRow, int startColumn) {
        int[][][] dp = new int[m][n][maxMove + 1];
        // 移动步数2的都是从移动步数1的转移来的
        // 移动步数3的都是从移动步数2的转移来的
        // 所以,要从移动步数从1开始递增
        for (int k = 1; k <= maxMove; k++) {
            for (int i = 0; i < m; i++) {
                for (int j = 0; j < n; j++) {
                    // 处理四条边
                    if (i == 0) dp[i][j][k]++;
                    if (j == 0) dp[i][j][k]++;
                    if (i == m - 1) dp[i][j][k]++;
                    if (j == n - 1) dp[i][j][k]++;

                    // 中间的位置,向四个方向延伸
                    for (int[] dir : dirs) {
                        int nextI = i + dir[0];
                        int nextJ = j + dir[1];
                        if (nextI >= 0 && nextI < m && nextJ >= 0 && nextJ < n) {
                            dp[i][j][k] = (dp[i][j][k] + dp[nextI][nextJ][k - 1]) % MOD;
                        }
                    }
                }
            }
        }

        return dp[startRow][startColumn][maxMove];
    }
}

时间复杂度:O(m * n * maxMove),三层循环,dirs的循环固定4次,算常量。
空间复杂度:O(m * n * maxMove),dp数组占用O(m * n * maxMove)的空间。

运行结果:通过

方法五:动态规划 + 终极优化

通过方法四,我们可以看到,计算移动次数为 k 的时候只与 k-1 有关,所以,我们完全没有必要记录 k-1 之前的数据,这里我们可以把移动次数这个维度去掉。

另外,二维数组可以通过一定的方法转换成一维数组。

好了,直接看代码:

class Solution {

    // 四个方向
    int[][] dirs = new int[][] {{0, 1}, {0, -1}, {1, 0}, {-1, 0}};
    // 取余
    int MOD = 1000000007;

    public int findPaths(int m, int n, int maxMove, int startRow, int startColumn) {
        int[] dp = new int[m * n];
        // 移动步数2的都是从移动步数1的转移来的
        // 移动步数3的都是从移动步数2的转移来的
        // 所以,要从移动步数从1开始递增
        for (int k = 1; k <= maxMove; k++) {
            // 需要声明一个临时数组
            // 比如计算[1,2]的时候会用到[2,2],同时计算[2,2]的时候也会用到[1,2]
            // 这样计算[1,2]的时候就不能直接把值覆盖了,必须一轮计算完了才能覆盖
            int[] tmp = new int[m * n];
            for (int i = 0; i < m; i++) {
                for (int j = 0; j < n; j++) {
                    int index = index(i, j, n);
                    // 处理四条边
                    if (i == 0) tmp[index]++;
                    if (j == 0) tmp[index]++;
                    if (i == m - 1) tmp[index]++;
                    if (j == n - 1) tmp[index]++;

                    // 中间的位置,向四个方向延伸
                    for (int[] dir : dirs) {
                        int nextI = i + dir[0];
                        int nextJ = j + dir[1];
                        int nextIndex = index(nextI, nextJ, n);
                        if (nextI >= 0 && nextI < m && nextJ >= 0 && nextJ < n) {
                            tmp[index] = (tmp[index] + dp[nextIndex]) % MOD;
                        }
                    }
                }
            }
            dp = tmp;
        }

        return dp[index(startRow, startColumn, n)];
    }

    private int index(int i, int j, int n) {
        return i * n + j;
    }
}

时间复杂度:O(m * n * maxMove),三层循环,dirs的循环固定4次,算常量。
空间复杂度:O(m * n),dp数组虽然降成了一维,但是仍然要占用O(m * n)的空间

运行结果:通过

可以看到,时间基本没变,空间确实降下来了。

但是,仍然没办法跟记忆化搜索相比,因为记忆化搜索我们可以通过剪枝等手段减少循环(递归)的次数,但是动态规划的方法每一轮都要把(m * n)个格子重新计算一遍。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

进击的代码家

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值