动态规划初阶-N-0+leetcode解题思路

本文深入探讨了动态规划在路径问题中的应用,从不同路径到不同路径II,再到最小路径和,逐步揭示了动态规划的状态定义、状态转移方程和时间复杂度分析。此外,还讲解了如何处理障碍物、路径成本以及进阶的路径查找和空间复杂度优化。最后,介绍了如何将动态规划应用于最佳路径和方案数的综合问题,如最大得分的路径数目。通过这些例子,读者可以掌握动态规划在解决路径问题中的核心技巧和方法。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

本篇拜读于三叶大佬并结合自身总结兼有此得
动态规划 问题首先从【路径问题】开始

  1. 路径问题较可视化并得出DP转移路径
  2. 路径题目丰富,层次明显,容易上手。

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. 如何确定可以使用动态规划来求解问题
  2. 如何确定本题的状态定义
  3. 如何确定状态转移方程
  4. 对状态转移的要求是什么
  5. 如何分析动态规划的时间复杂度

小结-1:

  1. (1)通常我们要从【有无后效性】进行入手分析。
    如果对某个状态,我们只关注状态的值,而不需要关注状态是如何转移过来的话,那么就可以称为无后效性问题,此考虑DP解法。
    (2)通过【数据范围】来估测。由于DP是一个递推的过程,数据范围于10^5 ~10 ^6可以使用一维DP解决;数据范围于10 ^2 ~ 10 ^3的话,考虑使用二维DP来求解。

  2. 状态定义大多靠经验理论

  3. 状态转移方程就是对【最后一步的分情况讨论】。对的状态定义,离状态方程就不远了。

  4. 状态转移的要求:
    (1)如果是求最值问题,我们要做到【不漏】,因为重复并不影响问题结果。
    (2)如果是求方案数问题,我们要确保【不重不漏

  5. 动态规划问题的复杂度/计算量分析=>有多少个状态,复杂度/计算量就是多少。因此通常一维的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]
由于题目我们只能选择向下或者向右移动状态转移:

  1. 当前位置只能向下:dp[i][j] =dp[i-1][j]+grid[i][j];
  2. 当前位置只能向右:dp[i][j] =dp[i][j-1]+grind[i][j];
  3. 当前位置既能向下也能向右: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。没问题。
进阶版:

  1. DP状态转移部分共有n*n个状态需要转移
  2. 每次转移过程,枚举上一行的所有列

因为我们需要确保所有的方案都能枚举到,所以转态DP不能少。所以只能从枚举上一行的所有列这部分优化。:
我们不难发现,在计算某行的状态值时只能用到上一行的最小值次小值。因此,在处理第i行时可以得到:

  1. 处理第i行中列下标为i1的状态值时,由于不能选择正上方的数字,用到的是次小值的数值状态转移为f[i][j] = f[i-1][i2]+arr[i][j];
  2. 处理第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};
    }
}

最后再次感谢三叶大佬给予的技术支持

资源下载链接为: https://pan.quark.cn/s/f1ead55c4354 以下标题“H5页面模板源码,很不错的例子”暗示了我们讨论的主题是关于HTML5页面模板的源代码。HTML5是现代网页开发的核心技术,它提供了丰富的功能和元素,让开发者能够构建出更具交互性、动态性和响应式的网页。“很不错的例子”表明这些源码不仅具有实用性,还具备一定的教学意义,既可以作为项目开发的直接素材,也能供学习参考。 在描述“H5页面模板源码,非常酷炫的HTML5模板,可以直接使用,也可以参考学习”中,“非常酷炫”意味着这些模板可能融合了诸多高级特性,例如动画效果、媒体元素的运用以及响应式设计等,这些都是HTML5技术的优势所在。可以直接使用表明用户无需从零开始编写代码,能迅速搭建出吸引人的网页。同时,这些模板也适合学习,用户通过查看源代码可以了解特定设计和功能的实现方式,从而提升自身的HTML5开发能力。 标签“H5 手机网页 H5源代码 手机html”进一步明确了主题。“H5”是HTML5的简称,“手机网页”和“手机html”则强调这些模板是针对移动设备优化的。在如今移动优先的时代,适应各种屏幕尺寸和触摸操作的网页设计极为重要。这表明这些源码很可能是响应式的,能够根据设备自动调整布局,以适配手机、平板电脑等多种设备。 从“压缩包文件的文件名称列表”来看,虽然无法直接从文件名得知具体源码内容,但可以推测这些文件可能包含多种HTML5模板示例。“不错的样子.txt”可能是一个介绍或说明文件,对模板进行简要描述或提供使用指南。而“1-30”这样的命名方式可能意味着有30个不同的模板实例,每个模板对应一个独立文件,涵盖多种设计风格和功能,为学习和实践提供了全面的平台。 总的来说,这个资源集合为HTML5开发者或初学者提供了一套实用且酷炫的移动网页模板源代码。这些模板既可以直接应用于项目
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值