☰
动态规划解决网格最大金币收集问题详解
2026/10/26 3:19:48 网站建设 项目流程

1. 题目背景与核心需求解析

这道题目源自信息学竞赛领域的经典动态规划问题,属于典型的"路径最优化"类型。题目场景设定在一个N×M的网格中,每个格子存放着不同数量的金币。玩家从左上角出发,每次只能向右或向下移动,最终到达右下角,目标是收集尽可能多的金币。

1.1 题目形式化描述

给定一个N行M列的二维矩阵grid,其中grid[i][j]表示第i行第j列格子中的金币数量。我们需要设计算法计算从(1,1)到(N,M)的所有可能路径中,能够获得的最大金币总和。这是动态规划入门必须掌握的"数字三角形"问题的二维扩展版本。

1.2 问题特征分析

这个问题具有两个关键特征:

  1. 最优子结构:到达某个格子的最大金币数,只取决于其上方和左方格子的最大金币数
  2. 无后效性:后续路径的选择不会影响之前已经做出的决策

这两个特征正是动态规划适用的典型场景。与DFS暴力搜索相比,动态规划能将时间复杂度从指数级O(2^(n+m))降低到多项式级O(nm)。

2. 动态规划解法详解

2.1 状态定义与转移方程

我们定义dp[i][j]表示从起点(1,1)到达(i,j)时能获得的最大金币数。状态转移方程如下:

dp[i][j] = max(dp[i-1][j], dp[i][j-1]) + grid[i][j]

其中:

  • dp[i-1][j] 表示从上方格子下来的路径
  • dp[i][j-1] 表示从左方格子过来的路径
  • 取两者中的较大值加上当前格子的金币数

2.2 边界条件处理

对于第一行和第一列的特殊情况需要单独处理:

  • 第一行(i=1):只能从左方过来,dp[1][j] = dp[1][j-1] + grid[1][j]
  • 第一列(j=1):只能从上方下来,dp[i][1] = dp[i-1][1] + grid[i][1]
  • 起点(1,1):dp[1][1] = grid[1][1]

2.3 空间优化技巧

原始解法需要O(nm)的空间存储整个dp表。观察发现,计算当前行时只需要上一行的数据,因此可以优化到O(m)空间:

vector<int> dp(m+1, 0); for(int i=1; i<=n; ++i){ for(int j=1; j<=m; ++j){ dp[j] = max(dp[j], dp[j-1]) + grid[i][j]; } }

注意:这种优化在竞赛中非常实用,特别是当网格较大时(比如1000×1000),可以显著减少内存使用。

3. 完整代码实现与解析

3.1 C++标准解法

#include <iostream> #include <vector> #include <algorithm> using namespace std; int main() { int n, m; cin >> n >> m; vector<vector<int>> grid(n+1, vector<int>(m+1)); vector<vector<int>> dp(n+1, vector<int>(m+1, 0)); for(int i=1; i<=n; ++i) for(int j=1; j<=m; ++j) cin >> grid[i][j]; dp[1][1] = grid[1][1]; for(int i=2; i<=n; ++i) dp[i][1] = dp[i-1][1] + grid[i][1]; for(int j=2; j<=m; ++j) dp[1][j] = dp[1][j-1] + grid[1][j]; for(int i=2; i<=n; ++i) for(int j=2; j<=m; ++j) dp[i][j] = max(dp[i-1][j], dp[i][j-1]) + grid[i][j]; cout << dp[n][m] << endl; return 0; }

3.2 代码关键点解析

  1. 输入处理:题目通常给出n,m和网格数据,注意从(1,1)开始存储,方便边界处理
  2. 初始化顺序:先处理第一行和第一列,再处理内部格子,确保依赖的子问题已求解
  3. 空间优化版:适用于内存限制严格的情况,但会丢失具体路径信息

3.3 路径回溯实现

如果需要输出具体路径,可以额外维护一个path数组记录决策:

vector<vector<char>> path(n+1, vector<char>(m+1)); // 在状态转移时记录选择 if(dp[i-1][j] > dp[i][j-1]){ dp[i][j] = dp[i-1][j] + grid[i][j]; path[i][j] = 'D'; // 来自上方(Down) }else{ dp[i][j] = dp[i][j-1] + grid[i][j]; path[i][j] = 'R'; // 来自左方(Right) } // 回溯路径 int i=n, j=m; stack<char> st; while(i>1 || j>1){ st.push(path[i][j]); if(path[i][j] == 'D') i--; else j--; } while(!st.empty()){ cout << st.top() << " "; st.pop(); }

4. 常见问题与调试技巧

4.1 典型错误分析

  1. 数组越界:忘记处理边界条件,直接访问dp[0][j]或dp[i][0]

    • 解决方法:显式初始化第一行和第一列
  2. 初始化错误:dp[1][1]没有初始化为grid[1][1]

    • 解决方法:仔细检查起点初始化
  3. 输入读取错误:使用0-based还是1-based索引不一致

    • 解决方法:统一采用1-based,方便与题目描述对应

4.2 测试用例设计

设计测试用例时应考虑以下情况:

  • 最小网格:1×1,只有起点
  • 单行或单列网格:只能沿一个方向移动
  • 负金币值:虽然题目通常规定金币为正,但考虑这种情况可以检验鲁棒性
  • 大网格:测试时间复杂度和空间复杂度

示例测试用例:

// 输入 3 3 1 3 1 1 5 1 4 2 1 // 输出 12 (路径:右→下→右→下)

4.3 性能优化建议

  1. 输入输出加速:对于大规模数据,使用ios::sync_with_stdio(false)加快cin/cout速度
  2. 循环展开:在内部循环中适当展开可以提高缓存命中率
  3. 位运算优化:当金币值较小时,可以考虑用位运算替代比较操作

5. 算法扩展与变种

5.1 允许对角线移动

如果题目允许从左上、上、左三个方向移动,只需修改状态转移方程:

dp[i][j] = max({dp[i-1][j], dp[i][j-1], dp[i-1][j-1]}) + grid[i][j];

5.2 收集特定数量金币的路径计数

增加一个状态维度,dp[i][j][k]表示到达(i,j)时收集k个金币的路径数。这需要处理更复杂的状态转移。

5.3 带障碍物的金币收集

某些格子不可通过时,需要在状态转移中跳过这些格子:

if(grid[i][j] == -1) continue; // -1表示障碍 dp[i][j] = max(dp[i-1][j], dp[i][j-1]) + grid[i][j];

5.4 三维金币收集问题

扩展到三维空间,每个移动有更多选择,状态转移方程会更复杂,但核心思想不变。

6. 竞赛中的应用技巧

  1. 快速识别动态规划问题:看到"最大/最小路径和"、"不重复/不遗漏计数"等关键词时,优先考虑DP
  2. 状态设计原则:状态应包含足够信息确定后续决策,但不宜过多以免复杂度爆炸
  3. 调试打印技巧:在竞赛中可打印整个dp表辅助调试
  4. 滚动数组技巧:当n很大时,使用两行交替存储代替整个dp表

重要提示:在比赛中遇到类似题目时,务必先在小样例上手动模拟dp过程,确保完全理解状态转移逻辑后再开始编码。我曾多次因为急于编码而忽略边界条件,导致浪费大量调试时间。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询