LeetCode 576. 出界的路径数(动规) / 526. 优美的排列(全排列,状压dp)

本文详细解析了LeetCode中两道题目——出界的路径数和优美的排列——的解决方案,涉及深度优先搜索(DFS)、记忆化搜索和动态规划。对于出界路径数问题,从最初的DFS尝试,到记忆化搜索的改进,再到动态规划的两种实现,逐步揭示了解题思路。而对于优美的排列问题,给出了全排列和动态规划的解法。通过这两个问题,深入理解了如何运用不同的搜索策略来解决复杂问题。
摘要由CSDN通过智能技术生成

576. 出界的路径数

2021.8.15 每日一题

题目描述

给你一个大小为 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

来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/out-of-boundary-paths
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。

思路

第一时间dfs,明知超时,但是还是倔强了一下

class Solution {
    public static final int MOD = (int)1.0e9 + 7;
    int m;
    int n;
    int res = 0;
    int[][] dir = {{1,0},{-1,0},{0,1},{0,-1}};
    public int findPaths(int m, int n, int maxMove, int startRow, int startColumn) {
        //第一时间想到的肯定还是dfs,但是答案很大吓到我了,估计会超范围
        //不过先写一下试试吧
        this.m = m;
        this.n = n;
        //因为可以折返,所以不应该有标记走到过的数组
        for(int i = 1; i <= maxMove; i++){
            dfs(startRow, startColumn, i);
        }
        return res;
    }

    public boolean inArea(int x, int y){
        return x >= 0 && x < m && y >= 0 && y < n;
    }

    public void dfs(int x, int y, int k){
        if(k == 0 && !inArea(x, y)){
            res = (res + 1) % MOD;
            return;
        }
        if(k <= 0)
            return;
        if(!inArea(x, y))
            return;
        for(int i = 0; i < 4; i++){
            int nx = x + dir[i][0];
            int ny = y + dir[i][1];
            dfs(nx, ny, k - 1);
        }

    }
}

动态规划想一下:
想不出来,看了一下题解,先根据状态定义写个记忆化搜索吧,可以在dfs基础上改,好久没写过了
开始想直接加一个记忆化数组memo,然后发现没有返回值好像不太行,因为k这里是从大到小变化的,转移会出问题。然后就加了返回值,可以了
memo[i][j][k]表示还剩下k步,从(i,j)这个点出发走出边界的路径数
记忆化搜索:

class Solution {
    public static final int MOD = (int)1.0e9 + 7;
    int m;
    int n;
    int[][] dir = {{1,0},{-1,0},{0,1},{0,-1}};
    int[][][] memo;
    public int findPaths(int m, int n, int maxMove, int startRow, int startColumn) {
        //第一时间想到的肯定还是dfs,但是答案很大吓到我了,估计会超范围
        //超时以后回过头来想哪些状态重复了
        //想一个很简单的例子,走两步可以回到初始状态,所以可以把之前多少步,走到哪里的结果记录下来
        //即memo[i][j][k]表示还剩下k步,从(i,j)这个点出发走出边界的路径数
        this.m = m;
        this.n = n;
        //记忆化数组
        memo = new int[m][n][maxMove + 1];
        //因为可以折返,所以不应该有标记走到过的数组
        //初始化
        for(int i = 0; i < m; i++){
            for(int j = 0; j < n; j++){
                Arrays.fill(memo[i][j], -1);
            }
        }
        return dfs(startRow, startColumn, maxMove);
        
    }

    public boolean inArea(int x, int y){
        return x >= 0 && x < m && y >= 0 && y < n;
    }

    public int dfs(int x, int y, int k){
        //如果步数小于0,return
        if(k < 0)
            return 0;
        //如果步数够,且出界了,就返回1
        if(k >= 0 && !inArea(x, y)){
            return 1;
        }
        //如果不在范围内,返回
        if(!inArea(x, y))
            return 0;
        //如果到了这个点,剩余同样的步数,那么结果是已经存在过的,就直接加这个结果
        if(memo[x][y][k] != -1){
            return memo[x][y][k];
        }
        int ans = 0;
        for(int i = 0; i < 4; i++){
            int nx = x + dir[i][0];
            int ny = y + dir[i][1];
            ans = (ans + dfs(nx, ny, k - 1)) % MOD;

        }
        memo[x][y][k] = ans;
        return ans;

    }
}

官解的动态规划:
根据当前状态推后面的状态
这个动态规划的状态定义发生了改变:dp[i][j][k] 表示k步到达(i,j)这个点的路径数目,所以能这样进行转移

class Solution {
    public static final int MOD = (int)1.0e9 + 7;
    int[][] dir = {{1,0},{-1,0},{0,1},{0,-1}};
    public int findPaths(int m, int n, int maxMove, int startRow, int startColumn) {
        //动规写一版,与一般的动规不同的是,这个是从前面的状态推后面的状态
        //这种也要熟练的写出来
        //dp[i][j][k] 表示k步到达(i,j)这个点的路径数目
        int[][][] dp = new int[m][n][maxMove + 1];
        //初始化,开始点0步是1,其他都是0
        dp[startRow][startColumn][0] = 1;
        int res = 0;
        for(int k = 0; k < maxMove; k++){
            for(int i = 0; i < m; i++){
                for(int j = 0; j < n; j++){
                    int count = dp[i][j][k];
                    //如果能到达这里
                    if(count > 0){
                        for(int[] d : dir){
                            int ni = i + d[0];
                            int nj = j + d[1];
                            //如果在范围内
                            if(ni >= 0 && ni < m && nj >= 0 && nj < n){
                                dp[ni][nj][k + 1] = (dp[ni][nj][k + 1] + count) % MOD;
                            //如果不在范围内,就在结果中加上这个路径数目
                            }else{
                                res = (res + count) % MOD;
                            }
                        }
                    }
                }
            }
        }
        return res;
    }
}

上面是从一个状态往后面的状态推导,即dp[i + x] = dp[i]…
而如果常用的那种思路,dp[i] = dp[i - 1],沿用上面的状态定义:
但是这种思路会导致个什么问题呢,就是我们定义的范围是在这个矩形内,所以想要出边界是无法转移的
所以到了边界上,需要对当前步数为1的情况进行处理,也就是最后处理一下边界情况
或者我认为也可以扩展一层边界,然后到达扩展层就是结果

看了三叶姐的动规,发现状态定义是和记忆化搜索的记忆化数组一样的
dp[i][j][k]表示从(i,j)点出发,步数在k次以内,能走出边界的路径数
记忆化递归是自顶向下,动规是自底向上
因此过程反过来了,是从边界返回到初始点
先处理边界,例如角就有三种可能走出去,边就有一种(一行或者一列)或者两种(多行多列)
然后倒着去找到到达初始点的路径数,这个思路怎么说呢,理解了就好了,感觉一般不会这样想

class Solution {
    int MOD = (int)1e9+7;
    int m, n, max;
    int[][] dirs = new int[][]{{1,0},{-1,0},{0,1},{0,-1}};
    public int findPaths(int _m, int _n, int _max, int r, int c) {
        m = _m; n = _n; max = _max;
        int[][] f = new int[m * n][max + 1];
        // 初始化边缘格子的路径数量
        for (int i = 0; i < m; i++) {
            for (int j = 0; j < n; j++) {
                if (i == 0) add(i, j, f);
                if (j == 0) add(i, j, f);
                if (i == m - 1) add(i, j, f);
                if (j == n - 1) add(i, j, f);
            }
        }
        // 从小到大枚举「可移动步数」
        for (int k = 1; k <= max; k++) {
            // 枚举所有的「位置」
            for (int idx = 0; idx < m * n; idx++) {
                int[] info = parseIdx(idx);
                int x = info[0], y = info[1];
                for (int[] d : dirs) {
                    int nx = x + d[0], ny = y + d[1];
                    if (nx < 0 || nx >= m || ny < 0 || ny >= n) continue;
                    int nidx = getIdx(nx, ny);
                    f[idx][k] += f[nidx][k - 1];
                    f[idx][k] %= MOD;
                }
            }
        }
        return f[getIdx(r, c)][max];       
    }
    void add(int x, int y, int[][] f) {
        for (int k = 1; k <= max; k++) {
            f[getIdx(x, y)][k]++;
        }
    }
    int getIdx(int x, int y) {
        return x * n + y;
    }
    int[] parseIdx(int idx) {
        return new int[]{idx / n, idx % n};
    }
}

作者:AC_OIer
链接:https://leetcode-cn.com/problems/out-of-boundary-paths/solution/gong-shui-san-xie-yi-ti-shuang-jie-ji-yi-asrz/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

526. 优美的排列

2021.8.16 每日一题

题目描述

假设有从 1 到 N 的 N 个整数,如果从这 N 个数字中成功构造出一个数组,使得数组的第 i 位 (1 <= i <= N) 满足如下两个条件中的一个,我们就称这个数组为一个优美的排列。条件:

第 i 位的数字能被 i 整除
i 能被第 i 位上的数字整除
现在给定一个整数 N,请问可以构造多少个优美的排列?

示例1:

输入: 2
输出: 2
解释:
第 1 个优美的排列是 [1, 2]:
第 1 个位置(i=1)上的数字是1,1能被 i(i=1)整除
第 2 个位置(i=2)上的数字是2,2能被 i(i=2)整除
第 2 个优美的排列是 [2, 1]:
第 1 个位置(i=1)上的数字是2,2能被 i(i=1)整除
第 2 个位置(i=2)上的数字是1,i(i=2)能被 1 整除

说明:

N 是一个正整数,并且不会超过15。

来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/beautiful-arrangement
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。

思路

简单的全排列问题

class Solution {
    int res = 0;
    int n;
    boolean[] used;
    public int countArrangement(int n) {
        //看到这个范围,感觉没什么好办法,就是个全排列
        this.n = n;
        used = new boolean[n + 1];
        dfs(1);
        return res;
    }

    //k表示当期第几位
    public void dfs(int i){
        if(i == n + 1){
            res++;
            return;
        }
        for(int k = 1; k <= n; k++){
            //如果这个数字使用过了
            if(used[k])
                continue;
            //否则,选一个满足条件的数字递归
            if(k / i * i == k || i / k * k == i){
                used[k] = true;
                dfs(i + 1);
            }
            //回溯
            used[k] = false;
        }
    }
}

状态压缩+动态规划:
看到最大范围15,想到用状压
mask和往常一样,表示1到n是否被使用过,如果使用过,mask对应位置就是1
如何转移呢?
首先确定mask中有多少个1,即新增的数字要放在哪个位置num
然后遍历mask中为1的位置,也就是当前优美队列中可以包含的数字
然后将这些数字分别放在num这个位置,如果能放进去,那么就可以通过之前的状态来转移
如果放不进去,跳过
最终结果就是f[(1 << n) - 1]

class Solution {
    public int countArrangement(int n) {
        //再练个状压
        int[] f = new int[1 << n];
        f[0] = 1;
        //mask表示1到n位n个数字,哪个被使用过
        //确定这些数字组成的优美排列的情况
        for(int mask = 1; mask < (1 << n); mask++){
            //mask中1的个数,表示当前数字要被放在第num位
            int num = Integer.bitCount(mask);
            //num个1,然后确定这些1放在什么位置
            for(int i = 0; i < n; i++){
                //如果mask的第i位为1,那么假定当前新增的数字就是i + 1
                if(((mask >> i) & 1) == 1){
                    //如果把当前数字i + 1放在第num位可以的话
                    if(num % (i + 1) == 0 || (i + 1) % num == 0){
                        f[mask] += f[mask ^ (1 << i)];
                    }
                }
            }
        }
        return f[(1 << n) - 1];
    }
}

这个状压的转移我还是没想出来,想想为什么
因为没明确mask的含义,其次没有想到mask中1的数量就是优美排列中数字的个数
其实状压dp这种套路很固定的

首先mask代表每一个数是否被选取过,选取过就是1
其次统计mask中1的个数,就是当前选了多少个数
然后将这几个数分别放在最后一位,然后由前面的状态转移(mask ^ (1 << i))

下次一定要能够自己写出来

### 回答1: 好的,我来用中文回复这个链接:https://leetcode-cn.com/tag/dynamic-programming/ 这个链接是 LeetCode 上关于动态规划的题目集合。动态规划是一种常用的算法思想,可以用来解决很多实际问题,比如最长公共子序列、背包问题、最短路径等等。在 LeetCode 上,动态规划也是一个非常重要的题型,很多题目都需要用到动态规划的思想来解决。 这个链接里包含了很多关于动态规划的题目,按照难度从简单到困难排列。每个题目都有详细的题目描述、输入输出样例、题目解析和代码实现等内容,非常适合想要学习动态规划算法的人来练习和提高自己的能力。 总之,这个链接是一个非常好的学习动态规划算法的资源,建议大家多多利用。 ### 回答2: 动态规划是一种算法思想,通常用于优化具有重叠子问题和最优子结构性质的问题。由于其成熟的学理论和强大的实用效果,动态规划在计算机科学、学、经济学、管理学等领域均有重要应用。 在计算机科学领域,动态规划常用于解决最优化问题,如背包问题、图像处理、语音识别、自然语言处理等。同时,在计算机网络和分布式系统中,动态规划也广泛应用于各种优化算法中,如链路优化、路由算法、网络流量控制等。 对于算法领域的程序员而言,动态规划是一种必要的技能和知识点。在LeetCode这样的程序员平台上,题目分类和标签设置十分细致和方便,方便程序员查找并深入学习不同类型的算法。 LeetCode的动态规划标签下的题目涵盖了各种难度级别和场景的问题。从简单的斐波那契列、迷宫问题到可以用于实际应用的背包问题、最长公共子序列等,难度不断递进且话题丰富,有助于开发人员掌握动态规划的实际应用技能和抽象思维模式。 因此,深入LeetCode动态规划分类下的题目学习和练习,对于程序员的职业发展和技能提升有着重要的意义。 ### 回答3: 动态规划是一种常见的算法思想,它通过将问题拆分成子问题的方式进行求解。在LeetCode中,动态规划标签涵盖了众多经典和优美的算法问题,例如斐波那契列、矩阵链乘法、背包问题等。 动态规划的核心思想是“记忆化搜索”,即将中间状态保存下来,避免重复计算。通常情况下,我们会使用一张二维表来记录状态转移过程中的中间值,例如动态规划求解斐波那契列问题时,就可以定义一个二维组f[i][j],代表第i项斐波那契列中,第j个元素的值。 在LeetCode中,动态规划标签下有众多难度不同的问题。例如,经典的“爬楼梯”问题,要求我们计算到n级楼梯的方案。这个问题的解法非常简单,只需要维护一个长度为n的组,记录到达每一级楼梯的方案即可。类似的问题还有“零钱兑换”、“乘积最大子组”、“通配符匹配”等,它们都采用了类似的动态规划思想,通过拆分问题、保存中间状态来求解问题。 需要注意的是,动态规划算法并不是万能的,它虽然可以处理众多经典问题,但在某些场景下并不适用。例如,某些问题的状态转移过程比较复杂,或者状态转移方程中存在多个参,这些情况下使用动态规划算法可能会变得比较麻烦。此外,动态规划算法也存在一些常见误区,例如错用贪心思想、未考虑边界情况等。 总之,掌握动态规划算法对于LeetCode的学习和解题都非常重要。除了刷题以外,我们还可以通过阅读经典的动态规划书籍,例如《算法竞赛进阶指南》、《算法与据结构基础》等,来深入理解这种算法思想。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值