本篇拜读于三叶大佬并结合自身总结兼有此得
动态规划 问题首先从【路径问题】开始
- 路径问题较可视化并得出DP转移路径
- 路径题目丰富,层次明显,容易上手。
leetcode - 62.不同路径
class Solution {
public:
int uniquePaths(int m, int n) {
vector<vector<int>> dp(m, vector<int>(n,0));
dp[0][0] = 1;
for(int i=0;i<m;i++){
for(int j=0;j<n;j++){
if(i>=1&&j>=1)dp[i][j] = dp[i][j-1]+dp[i-1][j];
else if(i>0) dp[i][j]= dp[i-1][j];//左边
else if(j>0)dp[i][j] = dp[i][j-1];//上边
}
}
return dp[m-1][n-1];
}
};
从这道题我们能够接触到第一种通用的DP解法:{经验解法}
并掌握五个核心问题:
- 如何确定可以使用动态规划来求解问题
- 如何确定本题的状态定义
- 如何确定状态转移方程
- 对状态转移的要求是什么
- 如何分析动态规划的时间复杂度
小结-1:
-
(1)通常我们要从【有无后效性】进行入手分析。
如果对某个状态,我们只关注状态的值,而不需要关注状态是如何转移过来的话,那么就可以称为无后效性问题,此考虑DP解法。
(2)通过【数据范围】来估测。由于DP是一个递推的过程,数据范围于10^5 ~10 ^6可以使用一维DP解决;数据范围于10 ^2 ~ 10 ^3的话,考虑使用二维DP来求解。 -
状态定义大多靠经验理论
-
状态转移方程就是对【最后一步的分情况讨论】。对的状态定义,离状态方程就不远了。
-
状态转移的要求:
(1)如果是求最值问题,我们要做到【不漏】,因为重复并不影响问题结果。
(2)如果是求方案数问题,我们要确保【不重不漏】 -
动态规划问题的复杂度/计算量分析=>有多少个状态,复杂度/计算量就是多少。因此通常一维的DP复杂度通常是线性的O(n),而二维的DP复杂度通常是平方O(n^2)。
leetcode- 63 . 不同路径 II
class Solution {
public:
int uniquePathsWithObstacles(vector<vector<int>>& obstacleGrid) {//g
//法1:
//vector<vector<int>>dp(obstacleGrid.size(),vector<int>(obstacleGrid[0].size(),0));
// dp[0][0] = 1;
// for(int i= 0;i<dp.size();i++){
// for(int j=0;j<dp[0].size();j++){
// if(i>=1 && j>=1) dp[i][j] = dp[i-1][j]+dp[i][j-1];
// else if(i>0)dp[i][j] = dp[i-1][j];
// else if(j>0)dp[i][j] = dp[i][j-1];
// if(obstacleGrid[i][j] == 1) dp[i][j] = 0;
// }
// }
// return dp[obstacleGrid.size()-1][obstacleGrid[0].size()-1];
//法2:
int f[120][120];
int m = obstacleGrid.size(),n=obstacleGrid[0].size();
memset(f,0,sizeof(f));
//设定f的初始状态
f[1][1] = obstacleGrid[0][0]==1?0:1;
for(int i=1;i<=m;i++){
for(int j=1;j<=n;j++){
if(i == 1&& j==1) continue;
if(obstacleGrid[i-1][j-1] == 1)f[i][j] = 0;
else f[i][j] = f[i-1][j]+f[i][j-1];
}
}
return f[m][n];
}
本题与leetoce-62相比多了障碍物(不可达点)限制,
结果仍是f[n-1][m-1]。 f[0][0] = 1,为起始条件。对于grid[i][j] == 1的格子, 有f[i][j] = 0;
状态转移:题目规定只能向下和向右移动则有:
1. 当前位置只能向下移动:f[i][j] = f[i][j-1]
2. 当前位置只能向右移动:f[i][j] = f[i-1][j]
3. 当前位置既能向下又能向右移动: f[i][j] = f[i-1][j]+f[i][j-1]
时间复杂度O(n * m)
空间复杂度O(n * m)
leetcode-64. 最小路径和
class Solution {
public:
int minPathSum(vector<vector<int>>& grid) {
vector<vector<int>> dp(grid.size(),vector<int>(grid[0].size(),0));
int m = grid.size(),n = grid[0].size();
dp[0][0] = grid[0][0];
for(int i=0;i<m;i++){
for(int j=0;j<n;j++){
if(i>=1&&j>=1)dp[i][j] = min(dp[i-1][j]+grid[i][j],dp[i][j-1]+grid[i][j]);
else if(i>0) dp[i][j] = dp[i-1][j]+grid[i][j];
else if(j>0) dp[i][j] = dp[i][j-1]+grid[i][j];
}
}
return dp[m-1][n-1];
}
};
这道题在leetcode-62的基础上,增加了路径成本的概念。
此时需要调整我们的状态定义:dp[i][j]为从(0,0)位置开始到达(i,j)位置的最小总和。
所以dp[m-1][n-1]即为我们需要的答案,起始状态为dp[0][0] = grid[0][0]
由于题目我们只能选择向下或者向右移动状态转移:
- 当前位置只能向下:dp[i][j] =dp[i-1][j]+grid[i][j];
- 当前位置只能向右:dp[i][j] =dp[i][j-1]+grind[i][j];
- 当前位置既能向下也能向右:dp[i][j] = min(dp[i][j-1],dp[i-1][j])+grid[i][j];
进阶版:输出总和最低的路径:
由原问题我们可以得到从(0,0)到(m-1,n-1)。那么我们可以使用额外的数据结构来记录,我们是如何一步步转移得到dp[m-1][n-1]的,当整个DP 过程结束之后m,我们在用辅助记录的数据结构来推回我们的路径。
同时,我们原有的DP部分已经创建了一个二维数组来存储转态值,这次用于记录上一步的g数组使用一维数组来记录。
class Solution {
int m, n;
public int minPathSum(int[][] grid) {
m = grid.length;
n = grid[0].length;
int[][] f = new int[m][n];
int[] g = new int[m * n];
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
if (i == 0 && j == 0) {
f[i][j] = grid[i][j];
} else {
int top = i - 1 >= 0 ? f[i - 1][j] + grid[i][j] : Integer.MAX_VALUE;
int left = j - 1 >= 0 ? f[i][j - 1] + grid[i][j] : Integer.MAX_VALUE;
f[i][j] = Math.min(top, left);
g[getIdx(i, j)] = top < left ? getIdx(i - 1, j) : getIdx(i, j - 1);
}
}
}
// 从「结尾」开始,在 g[] 数组中找「上一步」
int idx = getIdx(m - 1, n - 1);
// 逆序将路径点添加到 path 数组中
int[][] path = new int[m + n][2];
path[m + n - 1] = new int[]{m - 1, n - 1};
for (int i = 1; i < m + n; i++) {
path[m + n - 1 - i] = parseIdx(g[idx]);
idx = g[idx];
}
// 顺序输出位置
for (int i = 1; i < m + n; i++) {
int x = path[i][0], y = path[i][1];
System.out.print("(" + x + "," + y + ") ");
}
System.out.println(" ");
return f[m - 1][n - 1];
}
int[] parseIdx(int idx) {
return new int[]{idx / n, idx % n};
}
int getIdx(int x, int y) {
return x * n + y;
}
}
这里对找路径的过程进行简化,需要对原问题进行等价代换:
将【(0,0)到(m-1,n-1)的最短路径】转换为【从(m-1,n-1)到(0,0)的最短路径】,
同时移动方向从【向下&向右】转换为【向上&向左】。
如此实现【找路径】的顺序和【输出】顺序同向。调整定义f[i][j]为从(m-1,n-1)开始到达位置(i,j)的最小总和。
class Solution {
int m, n;
public int minPathSum(int[][] grid) {
m = grid.length;
n = grid[0].length;
int[][] f = new int[m][n];
int[] g = new int[m * n];
for (int i = m - 1; i >= 0; i--) {
for (int j = n - 1; j >= 0; j--) {
if (i == m - 1 && j == n - 1) {
f[i][j] = grid[i][j];
} else {
int bottom = i + 1 < m ? f[i + 1][j] + grid[i][j] : Integer.MAX_VALUE;
int right = j + 1 < n ? f[i][j + 1] + grid[i][j] : Integer.MAX_VALUE;
f[i][j] = Math.min(bottom, right);
g[getIdx(i, j)] = bottom < right ? getIdx(i + 1, j) : getIdx(i, j + 1);
}
}
}
int idx = getIdx(0,0);
for (int i = 1; i <= m + n; i++) {
if (i == m + n) continue;
int x = parseIdx(idx)[0], y = parseIdx(idx)[1];
System.out.print("(" + x + "," + y + ") ");
idx = g[idx];
}
System.out.println(" ");
return f[0][0];
}
int[] parseIdx(int idx) {
return new int[]{idx / n, idx % n};
}
int getIdx(int x, int y) {
return x * n + y;
}
}
leetcode-120. 三角形最小路径和
class Solution {
public int minimumTotal(List<List<Integer>> tri) {
int n = tri.size();
int ans = Integer.MAX_VALUE;
int [][] f=new int[n][n];
f[0][0] = tri.get(0).get(0);
for(int i=1;i<n;i++){
for(int j=0;j<=i;j++){
int val = tri.get(i).get(j);
f[i][j] = Integer.MAX_VALUE;
if(j!=0)f[i][j] = Math.min(f[i][j],f[i-1][j-1]+val);
if(i!=j)f[i][j] = Math.min(f[i][j],f[i-1][j]+val);
}
}
for(int i=0;i<n;i++){
ans = Math.min(ans,f[n-1][i]);
}
return ans;
}
}
这道题首先是从上到下的路径,那么最后一个点必然落在最后一行。对于最后一行的某个位置的值,只能从上一行的某一位置或者某两个位置之一转移而来的。同时,我们只关注了前一位的累加值是多少,而不关注累加值的结果是什么路径而来额这就满足了【无后效性的定义】:转移某个状态需要用到某个值,但是并不关心该值是如何来的(当某个状态确定后,之后的状态转移与之前的决策无关)。
这里状态定义我们通常会选择结果(状态的最后一步)、答案来确定DP的状态定义。所以本题的DP状态:f[i][j]代表到达某点的最小路径和。min(f[n-1][i])(最后一行的每列的路径和的最小值)就是答案。
状态转移通过i,j的性质可得到:
1. 除第一列(j!=0)位置上的数 ,都能通过左上方转移获得
2. 除对角线上的数(每行的最后一列)都能通过上方转移得到。
该解法的空间复杂度为O(n^2)
进阶版:
将空间复杂度缩减到O(n)
在递推的过程中,第i行的状态我们需要依赖i-1行的状态,但我们不需要存储所有行的状态值,所以可以对空间进行优化。优化的方式一般分为两种:
1. 滚动数组(推荐)
2. 根据状态依赖调整迭代/循环的方向
class Solution {
public int minimumTotal(List<List<Integer>> tri) {
int n = tri.size();
int ans = Integer.MAX_VALUE;
int[][] f = new int[2][n];
f[0][0] = tri.get(0).get(0);
for (int i = 1; i < n; i++) {
for (int j = 0; j < i + 1; j++) {
int val = tri.get(i).get(j);
f[i & 1][j] = Integer.MAX_VALUE;
if (j != 0) f[i & 1][j] = Math.min(f[i & 1][j], f[(i - 1) & 1][j - 1] + val);
if (j != i) f[i & 1][j] = Math.min(f[i & 1][j], f[(i - 1) & 1][j] + val);
}
}
for (int i = 0; i < n; i++) ans = Math.min(ans, f[(n - 1) & 1][i]);
return ans;
}
}
leetocde-931. 下降路径最小和
class Solution {
public:
int minFallingPathSum(vector<vector<int>>& matrix) {
int f[110][110];
int n = matrix.size();
memset(f,0x3f,sizeof f);
//将f数组的第二行填matrix的第一行
for(int i = 1;i<=n;i++){
f[1][i] = matrix[0][i-1];
}
//求f的第三行以及后面的累加和
for(int i=2;i<=n;i++){
for(int j=1;j<=n;j++){
f[i][j] = min(f[i-1][j],min(f[i-1][j-1],f[i-1][j+1]))+matrix[i-1][j-1];
}
}
int ans = 0x3f3f3f3f;
for(int i=1;i<=n;i++){
ans = min(ans,f[n][i]);
}
return ans ;
}
};
本题是对三角形最小路径的变形题,在上一题中我们从一个确定起点出发,按照状态转移不断下降。本题能从任意首行位置开始转移。
leetocde1289. 下降路径最小和 II
class Solution {
int MAX = Integer.MAX_VALUE;
public int minFallingPathSum(int[][] arr) {
int n = arr.length;
int [][] f= new int[n][n];
int i1 =-1,i2 = -1;
for(int i=0;i<n;i++){
int val = arr[0][i];
f[0][i] = val;
//寻找最小值和次小值的下标
if(val<(i1==-1?MAX:f[0][i1])){//先判断最小值是否小于当前值的下标i
i2=i1;
i1=i;
}else if(val<(i2==-1?MAX:f[0][i2])){//在判断次小值是否小于当前值的下标i
i2 = i;
}
}
//剩余行
for(int i=1;i<n;i++){
//在每一行的开始重新更新最小值和次小值的下标ti1,ti2
int ti1=-1,ti2=-1;
for(int j=0;j<n;j++){
int val = arr[i][j];
f[i][j] =MAX;
if(j!=i1){
f[i][j] = f[i-1][i1]+val;
}else{
f[i][j] = f[i-1][i2]+val;
}
//更新ti1和ti2
if(f[i][j]<(ti1==-1?MAX:f[i][ti1])){
ti2 = ti1;
ti1 = j;
}else if(f[i][j]<(ti2==-1?MAX:f[i][ti2])){
ti2 = j;
}
}
i1 = ti1;i2 = ti2;
}
int ans = Integer.MAX_VALUE;
for(int i=0;i<n;i++){
ans = Math.min(ans,f[n-1][i]);
}
return ans;
}
}
本题f[i][j]为到达位置(i,j)的最小路径和。所以答案是f[n-1][j]中的最小值,i的取值范围是[0,n)。
因为题目要求每一行的取数不能于上一行的数列下标相同。那么不能取正上方的值。在状态转移时需要枚举上一行的所有列下标。使得时间复杂度为O(n^ 3),题目范围是210^ 2所以计算量为810 ^6。没问题。
进阶版:
- DP状态转移部分共有n*n个状态需要转移
- 每次转移过程,枚举上一行的所有列
因为我们需要确保所有的方案都能枚举到,所以转态DP不能少。所以只能从枚举上一行的所有列这部分优化。:
我们不难发现,在计算某行的状态值时只能用到上一行的最小值和次小值。因此,在处理第i行时可以得到:
- 处理第i行中列下标为i1的状态值时,由于不能选择正上方的数字,用到的是次小值的数值状态转移为f[i][j] = f[i-1][i2]+arr[i][j];
- 处理第i行其他列的下标的状态值时,用最小值的状态转移方程为:f[i][j] = f[i-1][i1]+arr[i][j];
(我们用i1保存上一行的最小值下标,i2保存次小值对应的下标)
对应的代码表达:
if(val<(i1==-1)?MAX:f[0][i1]){
i2 =i1;
i1=i;
}else if(val<(i2==-1)?MAX:f[0][i2]){
i2 = i1;
}
leetcode-1575. 统计所有可行路径
这道题是一种新的动态规划类型题,
前面题给定了具体的起点或者自己枚举的起点出发,再结合题目给定的转移规则。
其模型可归为:特定起点,明确且有限的移动方向(转移状态),求解所有状态中的最优值。
本题只是给了移动规则,没有规定移动。相当于变种的路径问题。本题的数据范围10^2,使用记忆化搜索可解决。
(DFS)记忆化搜索:
1. 设计好递归函数的【入参】和【出参】
2. 设置好递归函数的出口(Base Case)(难点)
3. 编写【最小单元】处理逻辑
base case 指的是在确定:什么情况下,算是0条路径;什么样的情况下算是1条路径。最后再在DFS的过程中,不断累加有效情况(路径数量为1的条件)并返回。
这是DFS的本质,也是找BaseCase的过程。对于本题,只要抵达finish的位置就是一条有效路径。相反,对于无效路径的确定:油量耗完但是位置不在finish算是一个无效路径;油量没耗完,但是无法再移动到其他任何认为也算无效路径。
我们使用二维数组cache[][]作为缓冲器。用cache[i][fuel]表示从位置i出发,当前剩余的油量为fuel的前提下,到达目标位置的路径数。前面的题目我们只采用缓冲中间结果做法是因为【在i和fuel确定的情况下,其到达目的地的路径数量是唯一确定的。】
class Solution {
int mod = 1000000007;
// 缓存器:用于记录「特定状态」下的结果
// cache[i][fuel] 代表从位置 i 出发,当前剩余的油量为 fuel 的前提下,到达目标位置的「路径数量」
int[][] cache;
public int countRoutes(int[] ls, int start, int end, int fuel) {
int n = ls.length;
// 初始化缓存器
// 之所以要初始化为 -1
// 是为了区分「某个状态下路径数量为 0」和「某个状态尚未没计算过」两种情况
cache = new int[n][fuel + 1];
for (int i = 0; i < n; i++) {
Arrays.fill(cache[i], -1);
}
return dfs(ls, start, end, fuel);
}
/**
* 计算「路径数量」
* @param ls 入参 locations
* @param u 当前所在位置(ls 的下标)
* @param end 目标位置(ls 的下标)
* @param fuel 剩余油量
* @return 在位置 u 出发,油量为 fuel 的前提下,到达 end 的「路径数量」
*/
int dfs(int[] ls, int u, int end, int fuel) {
// 如果缓存器中已经有答案,直接返回
if (cache[u][fuel] != -1) {
return cache[u][fuel];
}
int n = ls.length;
// base case 1:如果油量为 0,且不在目标位置
// 将结果 0 写入缓存器并返回
if (fuel == 0 && u != end) {
cache[u][fuel] = 0;
return 0;
}
// base case 2:油量不为 0,且无法到达任何位置
// 将结果 0 写入缓存器并返回
boolean hasNext = false;
for (int i = 0; i < n; i++) {
if (i != u) {
int need = Math.abs(ls[u] - ls[i]);
if (fuel >= need) {
hasNext = true;
break;
}
}
}
if (fuel != 0 && !hasNext) {
cache[u][fuel] = u == end ? 1 : 0;
return cache[u][fuel];
}
// 计算油量为 fuel,从位置 u 到 end 的路径数量
// 由于每个点都可以经过多次,如果 u = end,那么本身就算一条路径
int sum = u == end ? 1 : 0;
for (int i = 0; i < n; i++) {
if (i != u) {
int need = Math.abs(ls[i] - ls[u]);
if (fuel >= need) {
sum += dfs(ls, i, end, fuel - need);
sum %= mod;
}
}
}
cache[u][fuel] = sum;
return sum;
}
}
【简化BaseCase】
对于本题的无效情况的BaseCase可以进一步进行简化。
我们不难发现:如果我们从某个位置start出发,不能一步到达目的地finfish,将永远无法到达目的地。
class Solution {
int mod = 1000000007;
// 缓存器:用于记录「特定状态」下的结果
// cache[i][fuel] 代表从位置 i 出发,当前剩余的油量为 fuel 的前提下,到达目标位置的「路径数量」
int[][] cache;
public int countRoutes(int[] ls, int start, int end, int fuel) {
int n = ls.length;
// 初始化缓存器
// 之所以要初始化为 -1
// 是为了区分「某个状态下路径数量为 0」和「某个状态尚未没计算过」两种情况
cache = new int[n][fuel + 1];
for (int i = 0; i < n; i++) {
Arrays.fill(cache[i], -1);
}
return dfs(ls, start, end, fuel);
}
/**
* 计算「路径数量」
* @param ls 入参 locations
* @param u 当前所在位置(ls 的下标)
* @param end 目标哦位置(ls 的下标)
* @param fuel 剩余油量
* @return 在位置 u 出发,油量为 fuel 的前提下,到达 end 的「路径数量」
*/
int dfs(int[] ls, int u, int end, int fuel) {
// 如果缓存中已经有答案,直接返回
if (cache[u][fuel] != -1) {
return cache[u][fuel];
}
// 如果一步到达不了,说明从位置 u 不能到达 end 位置
// 将结果 0 写入缓存器并返回
int need = Math.abs(ls[u] - ls[end]);
if (need > fuel) {
cache[u][fuel] = 0;
return 0;
}
int n = ls.length;
// 计算油量为 fuel,从位置 u 到 end 的路径数量
// 由于每个点都可以经过多次,如果 u = end,那么本身就算一条路径
int sum = u == end ? 1 : 0;
for (int i = 0; i < n; i++) {
if (i != u) {
need = Math.abs(ls[i] - ls[u]);
if (fuel >= need) {
sum += dfs(ls, i, end, fuel - need);
sum %= mod;
}
}
}
cache[u][fuel] = sum;
return sum;
}
}
进阶版:
将记忆化搜索改成动态规划:通过这种技巧,你讲不要猜状态定义和根据状态定义推导状态转移方程。
f[i][j]代表从i位置出发,当剩余的油量为j的前提下,到达目的地的路径数量。
所以状态转移为:f[i][fuel] = f[i][fuel]+f[x][fuel-need];(x在这里代表计算位置i油量fuel的状态时枚举的下一个位置,need代表从I到x所需要的油量),在计算f[i][fuel]的时候依赖f[x][fuel-need]。其中i和x没有严格的大小关系,而fuel和fuel-need之前需要 fuel>=fuel-need。所以我们需要从小到大枚举油量。
class Solution {
int mod = 1000000007;
public int countRoutes(int[] ls, int start, int end, int fuel) {
int n = ls.length;
// f[i][j] 代表从位置 i 出发,当前油量为 j 时,到达目的地的路径数
int[][] f = new int[n][fuel + 1];
// 对于本身位置就在目的地的状态,路径数为 1
for (int i = 0; i <= fuel; i++) f[end][i] = 1;
// 从状态转移方程可以发现 f[i][fuel]=f[i][fuel]+f[k][fuel-need]
// 在计算 f[i][fuel] 的时候依赖于 f[k][fuel-need]
// 其中 i 和 k 并无严格的大小关系
// 而 fuel 和 fuel-need 具有严格大小关系:fuel >= fuel-need
// 因此需要先从小到大枚举油量
for (int cur = 0; cur <= fuel; cur++) {
for (int i = 0; i < n; i++) {
for (int k = 0; k < n; k++) {
if (i != k) {
int need = Math.abs(ls[i] - ls[k]);
if (cur >= need) {
f[i][cur] += f[k][cur-need];
f[i][cur] %= mod;
}
}
}
}
}
return f[start][fuel];
}
}
时间复杂度为O(n^2*fuel)
小结:
1.从DFS出发。分析哪些参数可变,并将其作为DP数组的维度;将返回值作为DP数组的存储值。
2.从DFS的逻辑中抽象出单个状态的计算方法。
leetcode-576. 出界的路径数
首先设置一个DFS函数:
int dfs(int m,int n,int x,int y,int k){}
(m,n是对应题目的源输入。用来表示矩阵的行列的。x,y表示当前所在位置,k代表最多移动的次数,返回值为路径的数量)里面的重点应该放在签名的【可变参数】与【返回值】中,因为只是状态转移防尘的强调的部分。
为了方便,我们可以将当前坐标(x,y)映射为一个独立的编号index:
index = x*n+y;
(x,y) = (index/n,index%n);
设计二维数组f[][]作为DP数组:
1.第一维代表DFS可变参数中(x,y)对应的index。范围为[0,m*n)
2.第二维代表DFS可变参数中的k。取值范围为[0,N]
DP数组中存储的就是我们DFS的返回值:路径数量。
根据DO数组中的维度设计和存储目标值,我们可以得到[状态定义]:
f[i][j]代表【从位置i出发,使用步数不超过j时的路径数量】。
状态转移:
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]
第一维存储的是(x,y)对应的idx。
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};
}
}
最后一道:最佳方案和最佳方案数的综合题型:
leetcode-1301. 最大得分的路径数目
我们像上题一样将(x,y)用idx进行替代
从下向左上移动因此f(0)就是最终答案。
对于某个位置可以由正下,正右,右下转换得来,得出状态转移为:
f[(x,y)] = max(f[(x+1,y)],f[(x,y+1)],f[(x+1,y+1)]+board[(x,y)](合法格子内)
使用一个一维度数组来存储我们方案数
class Solution {
int INF = Integer.MIN_VALUE;
int mod = (int)1e9+7;
int n;
public int[] pathsWithMaxScore(List<String> board) {
n = board.size();
// 将 board 转存成二维数组
char[][] cs = new char[n][n];
for (int i = 0; i < n; i++) {
cs[i] = board.get(i).toCharArray();
}
// f(i) 代表从右下角到位置 i 的最大得分
int[] f = new int[n * n];
// f(i) 代表从右下角到位置 i 并取到最大得分的方案数量
int[] g = new int[n * n];
for (int i = n - 1; i >= 0; i--) {
for (int j = n - 1; j >= 0; j--) {
int idx = getIdx(i, j);
// 一个初始化的状态,如果是在最后一格(起点):
// g[idx] = 1 : 代表到达起点的路径只有一条,这样我们就有了一个「有效值」可以滚动下去
// f[idx] = 0 : 代表在起点得分为 0
if (i == n - 1 && j == n - 1) {
g[idx] = 1;
continue;
}
// 如果该位置是「障碍点」,那么对应状态为:
// g[idx] = 0 : 「障碍点」不可访问,路径为 0
// f[idx] = INF : 「障碍点」不可访问,得分为无效值
if (cs[i][j] == 'X') {
f[idx] = INF;
continue;
}
// 如果是第一个格子(终点),这时候位置得分为 0
int val = (i == 0 && j == 0) ? 0 : cs[i][j] - '0';
// u 代表当前位置的「最大得分」;t 代表取得最大得分的「方案数」
int u = INF, t = 0;
// 如果「下方格子」合法,尝试从「下方格子」进行转移
if (i + 1 < n) {
int cur = f[getIdx(i + 1, j)] + val;
int cnt = g[getIdx(i + 1, j)];
int[] res = update(cur, cnt, u, t);
u = res[0]; t = res[1];
}
// 如果「右边格子」合法,尝试从「右边格子」进行转移
if (j + 1 < n) {
int cur = f[getIdx(i, j + 1)] + val;
int cnt = g[getIdx(i, j + 1)];
int[] res = update(cur, cnt, u, t);
u = res[0]; t = res[1];
}
// 如果「右下角格子」合法,尝试从「右下角格子」进行转移
if (i + 1 < n && j + 1 < n) {
int cur = f[getIdx(i + 1, j + 1)] + val;
int cnt = g[getIdx(i + 1, j + 1)];
int[] res = update(cur, cnt, u, t);
u = res[0]; t = res[1];
}
// 更新 dp 值
f[idx] = u < 0 ? INF : u;
g[idx] = t;
}
}
// 构造答案
int[] ans = new int[2];
// 如果终点不可达(动规值为 INF)时,写入 0
ans[0] = f[getIdx(0, 0)] == INF ? 0 : f[getIdx(0, 0)];
// 如果终点不可达(动规值为 INF)时,写入 0
ans[1] = f[getIdx(0, 0)] == INF ? 0 : g[getIdx(0, 0)];
return ans;
}
// 更新 dp 值
int[] update(int cur, int cnt, int u, int t) {
// 起始答案为 [u, t] : u 为「最大得分」,t 为最大得分的「方案数」
int[] ans = new int[]{u, t};
// 如果当前值大于 u,更新「最大得分」和「方案数」
if (cur > u) {
ans[0] = cur;
ans[1] = cnt;
// 如果当前值等于 u,增加「方案数」
} else if (cur == u && cur != INF) {
ans[1] += cnt;
}
ans[1] %= mod;
return ans;
}
// 二维坐标 (x,y) 与 idx 的相互转换
int getIdx(int x, int y) {
return x * n + y;
}
int[] parseIdx(int idx) {
return new int[]{idx / n, idx % n};
}
}
最后再次感谢三叶大佬给予的技术支持