1. 问题背景与题目解析
"方格取数"是一道经典的动态规划问题,最初出现在2000年全国青少年信息学奥林匹克竞赛(NOIP)提高组中。题目描述如下:
给定一个N×N的方格图,每个方格中放有一个正整数。现在有一个人从左上角(1,1)出发,每次只能向下或向右移动一步,最终到达右下角(N,N)。在走过的路径中,取走经过的方格中的数(取走后该数字变为0)。我们需要找出两条这样的路径,使得取到的数字总和最大。
这个问题看似简单,但实际上需要考虑两条路径的相互影响,不能简单地分别计算两条最优路径然后相加。因为第一条路径取走数字后,第二条路径经过相同位置时只能得到0。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 动态规划状态设计
解决这个问题的关键在于设计合适的状态表示。传统的单路径问题可以用dp[i][j]表示从(1,1)到(i,j)的最大和,但对于双路径问题,我们需要扩展状态维度。
我们定义dp[i1][j1][i2][j2]表示第一条路径走到(i1,j1),第二条路径走到(i2,j2)时能获得的最大和。但由于两条路径的步数相同(因为每次只能向右或向下移动一步),我们可以优化状态表示:
dp[k][i1][i2]表示两条路径都走了k步,第一条路径在第i1行,第二条路径在第i2行时的最大和。此时第一条路径的位置是(i1, k+1-i1),第二条路径的位置是(i2, k+1-i2)。
这种状态设计将四维状态优化为三维,大大减少了空间复杂度。
3. 状态转移方程推导
基于上述状态设计,我们可以推导状态转移方程。对于每个状态dp[k][i1][i2],它可能来自四种情况:
- 两条路径都从上方移动过来
- 第一条路径从上方,第二条从左方移动过来
- 第一条路径从左方,第二条从上方移动过来
- 两条路径都从左方移动过来
具体状态转移方程为:
dp[k][i1][i2] = max(
dp[k-1][i1-1][i2-1],
dp[k-1][i1-1][i2],
dp[k-1][i1][i2-1],
dp[k-1][i1][i2]
) + value
其中value的计算需要考虑两条路径是否走到同一个格子:
- 如果i1 == i2(即两条路径走到同一行,由于步数相同,列也相同,所以是同一格子),则value = grid[i1][k+1-i1]
- 否则,value = grid[i1][k+1-i1] + grid[i2][k+1-i2]
4. 边界条件与初始化
在实现动态规划时,边界条件的处理至关重要:
- 初始化:dp[0][1][1] = grid[1][1](起点)
- 步数k的范围:1 ≤ k ≤ 2N-2(从起点到终点共2N-2步)
- 行数i1和i2的范围:max(1, k+1-N) ≤ i1, i2 ≤ min(N, k+1)
- 需要检查移动是否合法(不能超出网格边界)
5. 算法实现与优化
基于上述分析,我们可以用三重循环实现这个动态规划算法:
python复制def max_sum(grid):
N = len(grid)
dp = [[[ -1 for _ in range(N+1)] for __ in range(N+1)] for ___ in range(2*N)]
dp[0][1][1] = grid[0][0]
for k in range(1, 2*N-1):
for i1 in range(max(1, k+1-N), min(N, k+1)+1):
for i2 in range(max(1, k+1-N), min(N, k+1)+1):
j1 = k + 1 - i1
j2 = k + 1 - i2
max_val = -1
# 四种可能的转移来源
for di1, di2 in [(0,0), (0,-1), (-1,0), (-1,-1)]:
prev_i1 = i1 + di1
prev_i2 = i2 + di2
if 1 <= prev_i1 <= N and 1 <= prev_i2 <= N and dp[k-1][prev_i1][prev_i2] != -1:
max_val = max(max_val, dp[k-1][prev_i1][prev_i2])
if max_val == -1:
continue
if i1 == i2:
dp[k][i1][i2] = max_val + grid[i1-1][j1-1]
else:
dp[k][i1][i2] = max_val + grid[i1-1][j1-1] + grid[i2-1][j2-1]
return dp[2*N-2][N][N]
在实际实现中,可以进一步优化空间复杂度。注意到dp[k]只依赖于dp[k-1],所以可以用滚动数组将空间复杂度从O(N^3)降到O(N^2)。
6. 算法复杂度分析
时间复杂度:三重循环,O(N^3)
空间复杂度:使用滚动数组优化后为O(N^2)
对于NOIP竞赛中的N≤9的限制,这个复杂度完全足够。对于更大的N(如N=50),这个算法也能在合理时间内完成。
7. 常见错误与调试技巧
在实现这个算法时,容易犯的错误包括:
- 边界条件处理不当:忘记初始化起点或未正确处理网格边界
- 状态转移条件遗漏:漏掉四种可能的转移来源中的某些情况
- 相同格子的处理错误:当两条路径走到同一格子时,只加一次该格子的值
- 数组越界:没有正确计算j1和j2的值导致数组访问越界
调试技巧:
- 打印中间状态,特别是小规模测试用例的完整dp表
- 单独测试边界情况,如N=1, N=2的情况
- 使用assert语句检查数组访问是否越界
8. 扩展与变种
这个问题有几个有趣的变种:
- K条路径取数:扩展到K条路径同时取数,最大化总和
- 带权移动:不同方向的移动有不同的代价
- 障碍物:某些格子不能通过
- 三维方格取数:扩展到三维空间中的路径
对于K条路径的情况,状态维度会增加到2K维,时间和空间复杂度急剧增加,通常需要更巧妙的优化或近似算法。
