第一次在OpenJudge上遇到"最低通行费"这道题时,我盯着屏幕上的"Wrong Answer"提示发了半小时呆。明明按照《信息学奥赛一本通》上的思路写的代码,为什么在另一个平台就不行?这个问题困扰过无数算法初学者。今天,我们就来彻底拆解这道经典动态规划题目,不仅教你写出正确的代码,更要让你理解不同OJ平台间的微妙差异。
"最低通行费"看似简单,实则暗藏玄机。题目描述商人需要从网格左上角移动到右下角,每次只能向右或向下移动,每个格子需要缴纳一定费用,求最小总费用。关键在于理解题目隐含的约束条件:
cpp复制// 经典状态转移方程核心逻辑
if(i == 1 && j == 1)
dp[i][j] = a[i][j];
else if(i == 1)
dp[i][j] = dp[i][j-1] + a[i][j];
else if(j == 1)
dp[i][j] = dp[i-1][j] + a[i][j];
else
dp[i][j] = min(dp[i-1][j], dp[i][j-1]) + a[i][j];
正确的状态定义是动态规划成功的一半。对于坐标型DP,我们需要明确:
注意:许多初学者会混淆状态定义,错误地将dp[i][j]定义为"从起点到当前点的路径数"或"经过当前点的最大收益",这会导致完全错误的递推关系。
边界条件往往是出错的重灾区。对于第一行和第一列:
cpp复制// 更健壮的边界处理写法
memset(dp, 0x3f, sizeof(dp)); // 初始化为极大值
dp[1][1] = a[1][1];
for(int i = 1; i <= n; ++i) {
for(int j = 1; j <= n; ++j) {
if(i == 1 && j == 1) continue;
if(i > 1) dp[i][j] = min(dp[i][j], dp[i-1][j]);
if(j > 1) dp[i][j] = min(dp[i][j], dp[i][j-1]);
dp[i][j] += a[i][j];
}
}
| 对比项 | 《信息学奥赛一本通》 | OpenJudge |
|---|---|---|
| 输入格式 | 通常更宽松 | 可能更严格 |
| 时间限制 | 一般较宽松 | 可能更紧张 |
| 内存限制 | 通常足够 | 需要更注意 |
| 下标约定 | 明确从1开始 | 需要确认 |
cpp复制// 跨平台友好的初始化方式
const int MAXN = 100 + 10;
int dp[MAXN][MAXN];
int a[MAXN][MAXN];
void init() {
memset(dp, 0x3f, sizeof(dp));
// 其他初始化...
}
设计有效的测试用例是快速验证代码的关键:
在关键位置添加调试输出:
cpp复制// 调试输出示例
void debugPrint(int n) {
for(int i = 1; i <= n; ++i) {
for(int j = 1; j <= n; ++j) {
cout << dp[i][j] << " ";
}
cout << endl;
}
}
// 在状态转移后调用
debugPrint(n);
| 错误类型 | 表现 | 修正方法 |
|---|---|---|
| 下标错误 | 数组越界 | 检查循环边界 |
| 初始化不全 | 随机值 | 显式初始化 |
| 状态转移错误 | 结果不符预期 | 重新推导方程 |
| 输入读取错误 | 数据错乱 | 检查读取顺序 |
原始解法空间复杂度为O(N^2),可以优化到O(N):
cpp复制// 滚动数组优化
int dp[2][MAXN];
int now = 0, prev = 1;
dp[now][1] = a[1][1];
for(int i = 1; i <= n; ++i) {
swap(now, prev);
for(int j = 1; j <= n; ++j) {
if(i == 1 && j == 1) continue;
dp[now][j] = min(
i > 1 ? dp[prev][j] : INT_MAX,
j > 1 ? dp[now][j-1] : INT_MAX
) + a[i][j];
}
}
cpp复制// 路径记录示例
pair<int,int> pre[MAXN][MAXN];
// 在状态转移时记录前驱节点
if(dp[i-1][j] < dp[i][j-1]) {
dp[i][j] = dp[i-1][j] + a[i][j];
pre[i][j] = {i-1, j};
} else {
dp[i][j] = dp[i][j-1] + a[i][j];
pre[i][j] = {i, j-1};
}
解决这道题的价值不仅在于AC,更在于培养动态规划的通用思维:
在实际比赛中,我常常先用小黄纸手写状态转移表,确认无误后再开始编码。这种方法虽然原始,但能避免很多思维漏洞。对于"最低通行费"这类题目,当n=3时手动模拟整个DP过程,往往能发现代码中的潜在问题。