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

576. 出界的路径数

2021.8.15 每日一题

题目描述

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

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

示例 1:
LeetCode 576. 出界的路径数(动规) / 526. 优美的排列(全排列,状压dp)_第1张图片
输入:m = 2, n = 2, maxMove = 2, startRow = 0, startColumn = 0
输出:6

示例 2:
LeetCode 576. 出界的路径数(动规) / 526. 优美的排列(全排列,状压dp)_第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))

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

你可能感兴趣的:(LeetCode,java,leetcode)