【动态规划篇】专题(二):路径问题——在网格图中的决策艺术

文章目录

    • [从一维到二维:网格图 DP 模型](#从一维到二维:网格图 DP 模型)
    • [一、 前言:什么是网格图模型?](#一、 前言:什么是网格图模型?)
      • [1.1 升维打击](#1.1 升维打击)
      • [1.2 核心套路](#1.2 核心套路)
    • [二、 基础篇:路径计数](#二、 基础篇:路径计数)
      • [2.1 不同路径 (Medium)](#2.1 不同路径 (Medium))
        • [1. 题目描述](#1. 题目描述)
        • [2. 解题思路](#2. 解题思路)
        • [3. 代码实现 (C++)](#3. 代码实现 (C++))
      • [2.2 不同路径 II (Medium)](#2.2 不同路径 II (Medium))
        • [1. 题目描述](#1. 题目描述)
        • [2. 解题思路](#2. 解题思路)
        • [3. 代码实现 (C++)](#3. 代码实现 (C++))
    • [三、 进阶篇:最值路径](#三、 进阶篇:最值路径)
      • [3.1 礼物的最大价值 (Medium)](#3.1 礼物的最大价值 (Medium))
        • [1. 题目描述](#1. 题目描述)
        • [2. 解题思路](#2. 解题思路)
        • [3. 代码实现 (C++)](#3. 代码实现 (C++))
      • [3.2 最小路径和 (Medium)](#3.2 最小路径和 (Medium))
        • [1. 题目描述](#1. 题目描述)
        • [2. 解题思路](#2. 解题思路)
        • [3. 代码实现 (C++)](#3. 代码实现 (C++))
      • [3.3 下降路径最小和 (Medium)](#3.3 下降路径最小和 (Medium))
        • [1. 题目描述](#1. 题目描述)
        • [2. 解题思路](#2. 解题思路)
        • [3. 代码实现 (C++)](#3. 代码实现 (C++))
    • [四、 终极挑战:反向 DP](#四、 终极挑战:反向 DP)
      • [4.1 地下城游戏 (Hard)](#4.1 地下城游戏 (Hard))
        • [1. 题目描述](#1. 题目描述)
        • [2. 为什么正向 DP 会失败?](#2. 为什么正向 DP 会失败?)
        • [3. 正确思路:反向 DP](#3. 正确思路:反向 DP)
        • [4. 代码实现 (C++)](#4. 代码实现 (C++))
    • [五、 总结:网格 DP 的心法](#五、 总结:网格 DP 的心法)

从一维到二维:网格图 DP 模型

一、 前言:什么是网格图模型?

1.1 升维打击

💬 开篇:上一篇我们搞定了斐波那契数列,那是一维的跳台阶。今天我们要"升维"了!

🚀 循序渐进 :想象一下,你不再是在一条线上跳,而是站在一个棋盘(矩阵)上。你可以向下走,也可以向右走。这种在 m * n 的网格中移动,求路径数最小路径和最大价值 的问题,统称为网格图模型

👍 点赞、收藏与分享:本篇内容涵盖了 6 道高频面试题,特别是最后一道"地下城游戏",是很多人的噩梦。学会这一篇,网格 DP 不再怕!

1.2 核心套路

对于网格类 DP,我们的五步法依然适用,但有两个固定套路:

  1. 状态定义 :通常定义 dp[i][j] 为"从起点走到坐标 (i, j) 时的 xxx"。
  2. 空间技巧 :为了处理第 0 行和第 0 列的边界问题,我们通常多开一行一列 (辅助节点),这样代码会极其清爽,不需要写一堆 if(i==0)

二、 基础篇:路径计数

2.1 不同路径 (Medium)

1. 题目描述

题目链接62. 不同路径

描述

一个机器人位于一个 m x n 网格的左上角。机器人每次只能向下 或者向右 移动一步。机器人试图达到网格的右下角。

问总共有多少条不同的路径?

示例

输入:m = 3, n = 7

输出:28

2. 解题思路

这是网格 DP 的 Hello World

  1. 状态表示
    dp[i][j] 表示:走到 [i, j] 这个位置,一共有多少种走法。

  2. 状态转移方程

    既然只能"向下"或"向右"走,那么想要到达 [i, j],只有两条路:

    • 从上面下来:即从 [i-1, j] 走过来。
    • 从左边过来:即从 [i, j-1] 走过来。
      所以:dp[i][j] = dp[i-1][j] + dp[i][j-1]
  3. 初始化(核心技巧:虚拟边框)

    如果我们直接从 dp[0][0] 开始算,处理第一行时 dp[i-1] 会越界,处理第一列时 dp[j-1] 会越界。
    技巧

    • 申请 dp[m+1][n+1] 的空间。

    • 原来的 (0, 0) 对应现在的 (1, 1)

    • 关键点dp[0][1] = 1。这是一个"引子"。

      • 当计算起点 (1, 1) 时,dp[1][1] = dp[0][1] + dp[1][0] = 1 + 0 = 1。这样起点的方法数就正确初始化为 1 了。
  4. 填表顺序

    从上往下,从左往右。

3. 代码实现 (C++)
cpp 复制代码
class Solution {
public:
    int uniquePaths(int m, int n) {
        // 1. 创建 dp 表,多开一行一列
        // 初始值设为 0
        vector<vector<int>> dp(m + 1, vector<int>(n + 1, 0));

        // 2. 初始化辅助位置
        // 这是为了保证 dp[1][1] 能算出来是 1
        dp[0][1] = 1; 

        // 3. 填表
        // 注意:实际网格从 row=1, col=1 开始对应题目中的 0,0
        for (int i = 1; i <= m; i++) {
            for (int j = 1; j <= n; j++) {
                dp[i][j] = dp[i - 1][j] + dp[i][j - 1];
            }
        }

        // 4. 返回结果
        return dp[m][n];
    }
};

2.2 不同路径 II (Medium)

1. 题目描述

题目链接63. 不同路径 II

描述

和上一题一样,但是网格中出现了障碍物 (用 1 表示)。

遇到障碍物不能走,问现在有多少条路径?

示例

输入:obstacleGrid = [[0,0,0],[0,1,0],[0,0,0]]

输出:2

2. 解题思路

这题是上一题的"补丁版"。

  1. 状态表示

    同上,dp[i][j] 表示走到该位置的方法数。

  2. 状态转移方程(分类讨论)

    在填表通过 (i, j) 时,先看一眼原图 obstacleGrid

    • 如果有障碍物dp[i][j] = 0。(此路不通,方法数为 0)。
    • 如果没有障碍物 :照常计算 dp[i][j] = dp[i-1][j] + dp[i][j-1]
  3. 初始化

    同样利用 dp[0][1] = 1 或者 dp[1][0] = 1 来启动。

    注意:原数组 obstacleGrid 下标是 0 ~ m-1,对应的 DP 数组下标是 1 ~ m。所以判断障碍物时要用 obstacleGrid[i-1][j-1]

3. 代码实现 (C++)
cpp 复制代码
class Solution {
public:
    int uniquePathsWithObstacles(vector<vector<int>>& obstacleGrid) {
        int m = obstacleGrid.size();
        int n = obstacleGrid[0].size();
        
        // 1. 建立 dp 表
        vector<vector<int>> dp(m + 1, vector<int>(n + 1));
        
        // 2. 初始化启动资金
        // 这样计算 dp[1][1] 时,如果是空地,就会得到 1
        dp[0][1] = 1; 

        // 3. 填表
        for(int i = 1; i <= m; i++) {
            for(int j = 1; j <= n; j++) {
                // 注意下标映射:dp的(i,j) 对应 原图的(i-1, j-1)
                // 如果当前位置是障碍物,保持默认值 0,直接跳过
                if(obstacleGrid[i - 1][j - 1] == 1) {
                    continue; 
                }
                
                // 否则,正常转移
                dp[i][j] = dp[i - 1][j] + dp[i][j - 1];
            }
        }
        
        return dp[m][n];
    }
};

三、 进阶篇:最值路径

3.1 礼物的最大价值 (Medium)

1. 题目描述

题目链接剑指 Offer 47. 礼物的最大价值

描述

棋盘的每一格都有礼物(价值 > 0)。从左上角出发,每次向右或向下移动。

请计算你最多能拿到多少价值的礼物?

2. 解题思路

从"方案数"变成了"价值累加",核心逻辑不变,只是计算符号变了。

  1. 状态表示
    dp[i][j] 表示:走到 [i, j] 位置时,身上累计能拿到的最大价值。

  2. 状态转移方程

    为了让当前手里的价值最大,我肯定要贪心:是上一步(上方)给我的多,还是上一步(左方)给我的多?
    dp[i][j] = max(dp[i-1][j], dp[i][j-1]) + grid[i][j]
    (选一条肥路走,然后加上当前格子的钱)

  3. 初始化

    多开一行一列。因为礼物价值都是正数,辅助边框里的值初始化为 0 即可(表示边界外没有礼物)。

3. 代码实现 (C++)
cpp 复制代码
class Solution {
public:
    int maxValue(vector<vector<int>>& grid) {
        int m = grid.size();
        int n = grid[0].size();
        // 1. 多开一行一列,默认初始化为 0
        vector<vector<int>> dp(m + 1, vector<int>(n + 1));

        // 2. 填表
        for(int i = 1; i <= m; i++) {
            for(int j = 1; j <= n; j++) {
                // 状态转移:选上面和左边较大的那个 + 当前礼物价值
                int fromUp = dp[i - 1][j];
                int fromLeft = dp[i][j - 1];
                int currentGift = grid[i - 1][j - 1]; // 注意下标映射
                
                dp[i][j] = max(fromUp, fromLeft) + currentGift;
            }
        }
        
        return dp[m][n];
    }
};

3.2 最小路径和 (Medium)

1. 题目描述

题目链接64. 最小路径和

描述

给定一个 m x n 网格,网格里是非负整数。找出一条从左上角到右下角的路径,使得路径上的数字总和最小

只能向下或向右。

2. 解题思路

这题和上一题完全是镜像关系:求最大值变成了求最小值。

  1. 状态表示
    dp[i][j] 表示:走到 [i, j] 时,最小的路径和。

  2. 状态转移方程
    dp[i][j] = min(dp[i-1][j], dp[i][j-1]) + grid[i][j]

  3. 初始化(这里有坑!)

    因为我们要求的是 min,如果我们把辅助行(第0行、第0列)都初始化为 0,那完蛋了。
    min(0, 500) 会选 0,结果你的路径就会"穿墙"去选那个虚拟的 0。
    正确做法

    • 把辅助行、辅助列都初始化为 无穷大 (INT_MAX)
    • 唯独 起点的入口 需要特殊处理。让 dp[0][1] = 0dp[1][0] = 0。这样算起点 dp[1][1] 时,min(INF, 0) + grid[0][0] 就能正确得到起点的数值。
3. 代码实现 (C++)
cpp 复制代码
class Solution {
public:
    int minPathSum(vector<vector<int>>& grid) {
        int m = grid.size();
        int n = grid[0].size();
        
        // 1. 初始化为最大整数,防止干扰 min 计算
        vector<vector<int>> dp(m + 1, vector<int>(n + 1, INT_MAX));
        
        // 2. 开个口子,让起点能正确计算
        // 这里的 0 表示到达起点之前没有花费
        dp[0][1] = 0; 
        
        // 3. 填表
        for(int i = 1; i <= m; i++) {
            for(int j = 1; j <= n; j++) {
                // 取上面和左边的较小值
                int val = grid[i - 1][j - 1];
                dp[i][j] = min(dp[i - 1][j], dp[i][j - 1]) + val;
            }
        }
        
        return dp[m][n];
    }
};

3.3 下降路径最小和 (Medium)

1. 题目描述

题目链接931. 下降路径最小和

描述

给你一个 n x n 的方阵。从第一行 任意一个元素开始,每次往下走。

下一步可以是:正下方左下方右下方

求到达最后一行的最小路径和。

2. 解题思路

这题的特点是:方向变多了(3个方向),且起点不固定(第一行哪里开始都行),终点也不固定(最后一行哪里结束都行)。

  1. 状态表示
    dp[i][j] 表示:到达 [i, j] 位置时的最小下降和。

  2. 状态转移方程

    到达 [i, j] 可以从上一行的三个位置过来:

    • 正上方:dp[i-1][j]
    • 左上方:dp[i-1][j-1]
    • 右上方:dp[i-1][j+1]
      dp[i][j] = min(正上, 左上, 右上) + matrix[i][j]
  3. 初始化(核心技巧:左右扩列)

    这题如果你只扩充上边和左边是不够的,因为还有"右上方"。最右边的一列会去访问 j+1,导致越界。
    方案

    • 左右各多加一列,初始化为 无穷大(墙壁)。
    • 上面多加一行,初始化为 0(因为题目说可以从第一行任意位置开始,意味着第一行作为起点的代价就是它本身,上一行的虚拟代价为0)。
3. 代码实现 (C++)
cpp 复制代码
class Solution {
public:
    int minFallingPathSum(vector<vector<int>>& matrix) {
        int n = matrix.size();
        
        // 1. 申请 dp 表
        // 行:n + 1 (多一行顶部的虚拟行)
        // 列:n + 2 (左右各多一列虚拟墙)
        vector<vector<int>> dp(n + 1, vector<int>(n + 2, INT_MAX));
        
        // 2. 初始化第一行(虚拟行)为 0
        // 这样计算实际第一行时,就是 0 + matrix[0][j]
        for(int j = 0; j < n + 2; j++) dp[0][j] = 0;
        
        // 3. 填表
        for(int i = 1; i <= n; i++) {
            for(int j = 1; j <= n; j++) {
                // 三个方向找最小
                // 注意:dp 表的列 j 对应 matrix 的列 j-1
                int val = matrix[i - 1][j - 1];
                
                int min_prev = min(dp[i - 1][j],       // 正上
                               min(dp[i - 1][j - 1],   // 左上
                                   dp[i - 1][j + 1])); // 右上
                
                dp[i][j] = min_prev + val;
            }
        }
        
        // 4. 返回值
        // 答案是最后一行 dp[n][1...n] 中的最小值
        int result = INT_MAX;
        for(int j = 1; j <= n; j++) {
            result = min(result, dp[n][j]);
        }
        
        return result;
    }
};

四、 终极挑战:反向 DP

4.1 地下城游戏 (Hard)

1. 题目描述

题目链接174. 地下城游戏

描述

骑士在左上角,公主要在右下角。网格里有怪兽(负数,扣血)和血瓶(正数,回血)。

骑士从左上走到右下,要求路途上任何时刻血量都 > 0

问骑士出发时至少需要多少初始血量?

2. 为什么正向 DP 会失败?

很多同学上来就定义:dp[i][j] 表示走到 (i, j) 剩余的最大血量。

这是错的!
原因

假设在这个格子里,你剩余 100 滴血,但接下来面对的是个 -999 的大BOSS;

另一条路你剩余 5 滴血,但接下来全是血瓶。

正向推导时,你不知道未来的路有多难走,所以无法判断当前哪个状态是"最优"的。这道题具有"后效性"。

3. 正确思路:反向 DP

我们从终点往回推

假设我们已经到了公主面前,然后一步步倒退回起点,看沿途至少需要多少血。

  1. 状态表示
    dp[i][j] 表示:从 [i, j] 出发,到达终点,至少需要的初始血量。

  2. 状态转移方程

    [i, j] 出发,你可以往 走,也可以往 走。

    为了活命,我们肯定选一条门槛更低(需要血量更少)的路。

    • 如果往右走:需要 dp[i][j+1] 的血。
    • 如果往下走:需要 dp[i+1][j] 的血。
    • min_need = min(往右, 往下)

    那么在当前 [i, j] 位置,我们需要多少血呢?
    dp[i][j] + dungeon[i][j] >= min_need

    移项得:dp[i][j] = min_need - dungeon[i][j]

    关键修正

    如果算出来 dp[i][j] 是负数或者 0(说明这里全是血瓶,甚至不需要血就能过),但这不符合题目规则(血量必须 > 0)。所以最低也要有 1 滴血。
    dp[i][j] = max(1, min(dp[i+1][j], dp[i][j+1]) - dungeon[i][j])

  3. 初始化与填表

    • 因为是反向,所以我们要多开最后一行、最后一列。
    • 初始化为 无穷大(边界墙)。
    • 终点出口dp[m][n-1]dp[m-1][n] 初始化为 1。这表示到达公主之后,至少还要剩 1 滴血不死。
4. 代码实现 (C++)
cpp 复制代码
class Solution {
public:
    int calculateMinimumHP(vector<vector<int>>& dungeon) {
        int m = dungeon.size();
        int n = dungeon[0].size();
        
        // 1. dp 表,多开一行一列
        // 初始化为 INT_MAX,表示墙壁(走不通,需要的血量无穷大)
        vector<vector<int>> dp(m + 1, vector<int>(n + 1, INT_MAX));
        
        // 2. 初始化终点的出口
        // 也就是到了公主那里,至少要留 1 滴血
        dp[m][n - 1] = 1;
        dp[m - 1][n] = 1;
        
        // 3. 填表:从右下角开始,往左上角推
        for(int i = m - 1; i >= 0; i--) {
            for(int j = n - 1; j >= 0; j--) {
                // 1. 找后路中要求最低的那条
                int min_future_need = min(dp[i + 1][j], dp[i][j + 1]);
                
                // 2. 倒推当前位置需要的血
                int current_need = min_future_need - dungeon[i][j];
                
                // 3. 修正:如果算出来是负数或0,说明这里血瓶管够,
                // 但活着至少需要 1 滴血
                dp[i][j] = max(1, current_need);
            }
        }
        
        // 4. 返回起点需要的血量
        return dp[0][0];
    }
};

五、 总结:网格 DP 的心法

💬 总结:恭喜你!这 6 道题刷完,你已经攻克了二维 DP 的半壁江山。

📚 核心知识点回顾:

题目 核心逻辑 边界初始化技巧
不同路径 上 + 左 dp[0][1]=1, 其余 0
礼物最大值 max(上, 左) + val 全 0
最小路径和 min(上, 左) + val 全 INF, dp[0][1]=0
下降路径 min(上, 左上, 右上) + val 左右列 INF, 顶行 0
地下城 反向 min(右, 下) - val 边框 INF, 终点出口 1

🧠 记忆口诀

网格 DP 并不难,

状态定义是关键。

边界多开一行列,

初始化里藏玄机。

正常路径左上推,

生命游戏右下逆。

相关推荐
货拉拉技术2 小时前
文本大模型评测实践
人工智能·深度学习·算法
CoovallyAIHub2 小时前
模糊、噪声、压缩……让检测器学会主动评估画质
深度学习·算法·计算机视觉
跃龙客2 小时前
atomic笔记
笔记·算法
智驱力人工智能3 小时前
地铁隧道轨道障碍物实时检测方案 守护城市地下动脉的工程实践 轨道障碍物检测 高铁站区轨道障碍物AI预警 铁路轨道异物识别系统价格
人工智能·算法·yolo·目标检测·计算机视觉·边缘计算
陈天伟教授3 小时前
人工智能应用- 预测化学反应:05. AI 预测化学反应类型
人工智能·深度学习·学习·算法·机器学习
LYS_06183 小时前
C++学习(7)(输入输出)
c++·学习·算法
仰泳的熊猫3 小时前
蓝桥杯算法提高VIP-种树
数据结构·c++·算法·蓝桥杯·深度优先·图论
Remember_9933 小时前
SpringCloud:Nacos注册中心
java·开发语言·后端·算法·spring·spring cloud·list
圣保罗的大教堂3 小时前
leetcode 761. 特殊的二进制字符串 困难
leetcode