leetcode 576. 出界的路径数

Posted 大忽悠爱忽悠

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了leetcode 576. 出界的路径数相关的知识,希望对你有一定的参考价值。

在这里插入图片描述
在这里插入图片描述



记忆化搜索

递归三部曲:

  • 递归结束条件(base case):当前位置出界,说明找到一条路径
  • 返回值:以当期位置为起点的路径总数
  • 本级递归:当前位置的路径和等于来自他四个方向路径之和
const int mod = 1e9 + 7;
class Solution {
	map<pair<pair<int,int>, int>, long> ret;
public:
	int findPaths(int m, int n, int maxMove, int startRow, int startColumn) 
	{
		return dfs(m, n, startRow, startColumn,maxMove);
	}
	int dfs(int m, int n, int i, int j, int num)
	{
		//当前位置出界,说明找到一条路径
		if (i< 0 || j< 0 || i>= m || j>= n)
			return 1;
		//判断当前位置的结果是否求出
		if (ret.find({{ i,j }, num}) != ret.end())
			return ret[{ {i, j}, num}];
		//如果当前可移动次数小于等于0,并且还没有出界,那么则不算一条路径
		if (num<=0)
			return 0;
		//当前位置的路径和等于来自他四个方向之和
		int sum = 0;
		sum += dfs(m, n, i + 1, j, num - 1);
		sum %= mod;
		sum += dfs(m, n, i - 1, j, num - 1);
		sum %= mod;
		sum += dfs(m, n, i, j + 1, num - 1);
		sum %= mod;
		sum += dfs(m, n, i, j - 1, num - 1);
		sum %= mod;
		return ret[{ {i, j}, num}]=sum ;
	}
};

在这里插入图片描述


动态规划

我们可以根据上面的记忆化搜索来写出动态规划,方法如下:

  • 首先看记忆化搜索的函数原型:int dfs(int m, int n, int i, int j, int num)
  • 其中 m和 n 是对应了题目的源输入,用来表示矩形是几行几列的,整个 DFS 过程都不会发生变化,因此我们无须理会。
  • (i,j) 代表当前所在的位置,num 代表最多的移动次数,返回值代表路径数量。
  • 重点放在 DFS 函数签名中的「可变参数」与「返回值」。这和我们【动态规划】中的「状态定义」强关联。
  • 我们可以设计一个二维数组 f[][]作为我们的 dp 数组:
  • 第一维代表 DFS 可变参数中的 (x,y)。取值范围为 [0, m*n)
  • 第二维代表 DFS 可变参数中的 num。取值范围为 [0,N]
  • dp 数组中存储的就是我们 DFS 的返回值:路径数量。
  • 根据 dp 数组中的维度设计和存储目标值,我们可以得知「状态定义」为:
  • f[i][j] 代表从位置 i出发,可用步数不超过 j 时的路径数量。
  • 状态定义已经得出,接下来需要考虑「转移方程」。
  • 当有了「状态定义」之后,我们需要从「最后一步」来推导出「转移方程」:
  • 由于题目允许往四个方向进行移动。
  • 因此我们的最后一步也要统计四个相邻的方向。
  • 举个🌰,假设我们当前位置为(x,y),而 (x,y) 四个方向的相邻格子均不超出矩形。即有:
  • (x,y) 出发的路径数量 = 上方 (x-1,y) 的路径数量 + 下方 (x+1,y)的路径数量 + 左方 (x,y-1) 的路径数量 + 右方 (x,y+1) 的路径数量
  • 由此可得我们的状态转移方程:
  • f[(x,y)][step]=f[(x−1,y)][step−1]+f[(x+1,y)][step−1]+f[(x,y−1)][step−1]+f[(x,y+1)][step−1]
  • 从转移方程中我们发现,更新 f[i][j]依赖于 f[i][j−1],因此我们转移过程中需要将最大移动步数进行从小到大枚举。
  • 至此,我们已经完成求解「路径规划」问题的两大步骤:「状态定义」&「转移方程」。
  • 但这还不是所有,我们还需要一些有效值来滚动下去。
  • 其实就是需要一些「有效值」作为初始化状态
  • 观察我们的「转移方程」可以发现,整个转移过程是一个累加过程,如果没有一些有效的状态(非零值)进行初始化的话,整个递推过程并没有意义。
  • 那么哪些值可以作为成为初始化状态呢?
  • 显然,当我们已经位于矩阵边缘的时候,我们可以一步跨出矩阵,这算作一条路径。
  • 同时,由于我们能够往四个方向进行移动,因此不同的边缘格子会有不同数量的路径。
    在这里插入图片描述
  • 换句话说,我们需要先对边缘格子进行初始化操作,预处理每个边缘格子直接走出矩阵的路径数量。目的是为了我们整个 DP 过程可以有效的递推下去。
class Solution {
    int mod = (int)1e9+7;
    int m, n, N;
    public int findPaths(int _m, int _n, int _N, int _i, int _j) {
        m = _m; n = _n; N = _N;
        
        // f[i][j] 代表从 idx 为 i 的位置出发,移动步数不超过 j 的路径数量
        int[][] f = new int[m * n][N + 1];
        
        // 初始化边缘格子的路径数量
        for (int i = 0; i < m; i++) {
            for (int j = 0; j < n; j++) {
                if (i == 0) add(i, j, f);
                if (i == m - 1) add(i, j, f);
                if (j == 0) add(i, j, f);
                if (j == n - 1) add(i, j, f);
            }
        }
        
        // 定义可移动的四个方向
        int[][] dirs = new int[][]{{1,0},{-1,0},{0,1},{0,-1}};
        
        // 从小到大枚举「可移动步数」
        for (int step = 1; step <= N; step++) {
            // 枚举所有的「位置」
            for (int k = 0; k < m * n; k++) {
                int x = parseIdx(k)[0], y = parseIdx(k)[1];
                for (int[] d : dirs) {
                    int nx = x + d[0], ny = y + d[1];
                    // 如果位置有「相邻格子」,则「相邻格子」参与状态转移
                    if (nx >= 0 && nx < m && ny >= 0 && ny < n) {
                        f[k][step] += f[getIndex(nx, ny)][step - 1];
                        f[k][step] %= mod;
                    }
                }
            }
        }
        
        // 最终结果为从起始点触发,最大移动步数不超 N 的路径数量
        return f[getIndex(_i, _j)][N];
    }
    
    // 为每个「边缘」格子,添加一条路径
    void add(int x, int y, int[][] f) {
        int idx = getIndex(x, y);
        for (int step = 1; step <= N; step++) {
            f[idx][step]++;
        }
    }
    
    // 将 (x, y) 转换为 index
    int getIndex(int x, int y) {
        return x * n + y;
    }
    
    // 将 index 解析回 (x, y)
    int[] parseIdx(int idx) {
        return new int[]{idx / n, idx % n};
    }
} 

在这里插入图片描述


额外补充–动态规划套壳法

  • 但是上诉求解出边缘格子进行初始化的操作优点繁琐,可否有避免初始化边缘格子的方法呢?
  • YES!
  • 套壳法,看图:
    在这里插入图片描述
  • 外层新增的一圈值都初始化为1,表示一旦出界那么就会得到一条路径

状态定义

  • dp[i][j][num]:表示从(i,j)出发第num步出界的路径总数,等价于从外界出发第k步走到(i,j)的路径总数

状态转移

  • 显然我们可以直接获得如下状态转移方程

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]

初始化:我们需要注意外界的坐标的初始状态对应的值为1,即

在这里插入图片描述
如何求解

  • 有了每一个点的每一步对应的值,我们可以说是什么都不怕了
  • 题目求的是最多移动N次,出界的路径数,因此我们只需要讲每一步对应的值都加起来即可
  • 人话:对每个位置来说,把它从一步出界的路径数量,二步出界的路径数量…num步出界的路径数量累加起来,得到当前位置出界的路径数量总和
    在这里插入图片描述
    在这里插入图片描述

代码展示:

const int mod = 1e9 + 7;
class Solution {
public:
	int findPaths(int m, int n, int maxMove, int startRow, int startColumn) 
	{
		//f[i][j][num]从(i,j)出发第num步出界的路径总数,等价于从外界出发第num步走到(i,j)的路径总数
		vector<vector<vector<int>>> dp(m + 2, vector<vector<int>>(n + 2,vector<int>(maxMove+1,0)));
		//外部一圈初始化
		for (int i = 0; i < n + 2; i++)//第一行和最后一行同时进行初始化
		{
			dp[0][i][0] = 1;
			dp[m + 1][i][0] = 1;
		}
		for (int j = 1; j < m+2; j++)//左右两列初始化
		{
			dp[j][0][0] = 1;
			dp[j][n + 1][0] = 1;
		}
		for (int k = 1; k <= maxMove; k++)
		{
			for (int i = 1; i < m + 1; i++)
			{
				for (int j = 1; j < n + 1; j++)
				{
					dp[i][j][k] += dp[i - 1][j][k - 1];
					dp[i][j][k] %= mod;
					dp[i][j][k] += dp[i + 1][j][k - 1];
					dp[i][j][k] %= mod;
					dp[i][j][k] += dp[i][j - 1][k - 1];
					dp[i][j][k] %= mod;
					dp[i][j][k] += dp[i][j + 1][k - 1];
					dp[i][j][k] %= mod;
				}
			}
		}
		//计算起点出界路径总和
		int sum = 0;
		for (int k = 1; k <=maxMove; k++)
		{
			sum=(sum+dp[startRow+1][startColumn+1][k])%mod;
		}
		return sum;
	}
};

在这里插入图片描述

以上是关于leetcode 576. 出界的路径数的主要内容,如果未能解决你的问题,请参考以下文章

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

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

Leetcode 576.出界的路劲数

第十一周 Leetcode 576. Out of Boundary Paths (HARD) 计数dp

出界的路径数--dfs记忆化+剪枝

出界的路径数(记忆化bfs)