2 - 网格图DP

对于一些二维 DP(例如背包、最长公共子序列),如果把 DP 矩阵画出来,其实状态转移可以视作在网格图上的移动。所以在学习相对更抽象的二维 DP 之前,做一些形象的网格图 DP 会让后续的学习更轻松(比如 0-1 背包的空间优化写法为什么要倒序遍历)。

例题

滑雪

901. 滑雪 - AcWing题库

状态表示

f[i, j],所有从(i,j)开始滑的路径的最大长度。

状态计算

可以从f[i, j]滑任意四个方向(前提是(i, j)比该方向的点高):

  • 向上滑:f[i, j] = f[i + 1, j] + 1
  • 向右滑:f[i, j] = f[i, j + 1] + 1
  • 向下滑:f[i, j] = f[i - 1, j] + 1
  • 向左滑:f[i, j] = f[i, j + 1] + 1

那最终f[i, j] = max(四个情况) + 1。此外,这道题不会出现环状路径,因此可以用dp,这里用记忆化搜索写:

#include <cstring>
#include <iostream>
#include <algorithm>

using namespace std;

const int N = 310;

int n, m;
int g[N][N];
int f[N][N];

int dx[4] = {-1, 0, 1, 0}, dy[4] = {0, 1, 0, -1};

int dp(int x, int y)
{
    int &v = f[x][y];
    if (v != -1) return v;

    v = 1;
    for (int i = 0; i < 4; i ++ )
    {
        int a = x + dx[i], b = y + dy[i];
        if (a >= 1 && a <= n && b >= 1 && b <= m && g[x][y] > g[a][b])
            v = max(v, dp(a, b) + 1);
    }

    return v;
}

int main()
{
    scanf("%d%d", &n, &m);
    for (int i = 1; i <= n; i ++ )
        for (int j = 1; j <= m; j ++ )
            scanf("%d", &g[i][j]);

    memset(f, -1, sizeof f);

    int res = 0;
    for (int i = 1; i <= n; i ++ )
        for (int j = 1; j <= m; j ++ )
            res = max(res, dp(i, j));

    printf("%d\n", res);

    return 0;
}

练习题

基础

LCR 166. 珠宝的最高价值 - 力扣(LeetCode)

状态表示

f[i + 1, j + 1]表示在frame[i + 1][j + 1]时拿到的最大价值,零起点最好写成这样。

状态计算

  • f[i, j + 1]往下走得到f[i + 1, j + 1],有f[i + 1, j + 1] = f[i, j + 1] + frame[i][j]
  • f[i + 1, j]往右走得到f[i + 1, j + 1],有f[i + 1, j + 1] = f[i + 1, j] + frame[i][j]

综上f[i + 1, j + 1]要取它们的最大值。

参考代码如下:

int jewelleryValue(vector<vector<int>>& frame) {
    int n = frame.size(), m = frame[0].size();
    vector<vector<int>> dp(n + 5, vector<int>(m + 5, 0));
    for (int i = 0; i < n; ++i)
        for (int j = 0; j < m; ++j)
            dp[i + 1][j + 1] = max(dp[i + 1][j], dp[i][j + 1]) + frame[i][j];
    return dp[n][m];
}

62. 不同路径 - 力扣(LeetCode)

状态表示

f[i, j]表示走到[i, j]的总方案数量。

状态计算

  • f[i - 1, j]往下走得到f[i, j]
  • f[i, j - 1]往右走得到f[i, j]

那么f[i, j]就是这俩方案数的总和。

参考代码如下:

int uniquePaths(int m, int n) {
    vector<vector<int>> f(m + 5, vector<int>(n + 5, 0));
    f[1][1] = 1;
    for (int i = 1; i <= m; ++i)
        for (int j = 1; j <= n; ++j)
            f[i][j] += f[i - 1][j] + f[i][j - 1];
    return f[m][n];
}

63. 不同路径 II - 力扣(LeetCode)

在上一题的基础上增加了障碍物,因此要进行判断,如果[i, j]是障碍物,

参考代码如下:

int uniquePathsWithObstacles(vector<vector<int>>& obstacleGrid) {
    int m = obstacleGrid.size(), n = obstacleGrid[0].size();
    vector<vector<int>> f(m + 1, vector<int>(n + 1, 0));
    f[1][1] = obstacleGrid[0][0] != 1;
    for (int i = 1; i <= m; ++i)
        for (int j = 1; j <= n; ++j)
            if (obstacleGrid[i - 1][j - 1] == 0)
                f[i][j] += f[i - 1][j] + f[i][j - 1];
    return f[m][n];
}
64. 最小路径和
120. 三角形最小路径和
931. 下降路径最小和 1573
2684. 矩阵中移动的最大次数 1626
2304. 网格中的最小路径代价 1658
1289. 下降路径最小和 II 1697

进阶

1594. 矩阵的最大非负积 - 力扣(LeetCode)

状态表示

maxVal[i, j]表示从[0, 0][i, j]的所有路径的乘积结果,我们要最大值。

minVal[i, j]表示从[0, 0][i, j]的所有路径的乘积结果,我们要最小值。

状态计算

以计算maxVal[i, j]为例,maxVal[i, j]可由如下项一步得到:

  • maxVal[i, j] = max(maxVal[i - 1, j] * grid[i][j], minVal[i - 1, j] * grid[i][j])
  • maxVal[i, j] = max(maxVal[i, j - 1] * grid[i][j], minVal[i, j - 1] * grid[i][j])

我们要取它俩的最大值。

为了简化代码,我们将最大值和最小值存成pair,参考代码如下:

const int MOD = 1e9 + 7;
using PLL = pair<long long, long long>;
int maxProductPath(vector<vector<int>>& grid) {
    int m = grid.size(), n = grid[0].size();
    vector<vector<PLL>> dp(m, vector<PLL>(n));
    dp[0][0] = {grid[0][0], grid[0][0]};
    for (int i = 0; i < m; ++i)
        for (int j = 0; j < n; ++j) {
            if (!i && j)
                dp[i][j] = minmax({dp[i][j - 1].first * grid[i][j],
                                   dp[i][j - 1].second * grid[i][j]});
            else if (!j && i)
                dp[i][j] = minmax({dp[i - 1][j].first * grid[i][j],
                                   dp[i - 1][j].second * grid[i][j]});
            else if (i && j)
                dp[i][j] = minmax({dp[i - 1][j].first * grid[i][j],
                                   dp[i - 1][j].second * grid[i][j],
                                   dp[i][j - 1].first * grid[i][j],
                                   dp[i][j - 1].second * grid[i][j]});
        };

    long long ans = dp[m - 1][n - 1].second;
    return ans >= 0 ? ans % MOD : -1;
}

非常坏0起点,使我讨论一堆情况。

1301. 最大得分的路径数目 1853
2435. 矩阵中和能被 K 整除的路径 1952
174. 地下城游戏
329. 矩阵中的最长递增路径
2328. 网格图中递增路径的数目 2001
2267. 检查是否有合法括号字符串路径 2085
1937. 扣分后的最大得分 2106
1463. 摘樱桃 II
741. 摘樱桃
2510. 检查是否有路径经过相同数量的 0 和 1(会员题)