在一个 n×m 的网格中,每个格子是 0、1 或 ?。从左上角 (1,1) 出发,每次只能向右或向下移动,到达右下角 (n,m)。经过字符 1 时得 1 分,经过其它字符不得分。我们可以在开始移动前,将不超过 x 个 ? 修改为 1(修改后的格子视为 1),然后以最优路径移动。目标是最大化最终得分。
设某条路径上有 a 个原本的 1,b 个 ?。若我们将其中 k 个 ? 改为 1(0≤k≤min(b,x)),则得分为 a+k。显然在有剩余修改次数时,应尽可能修改路径上的 ?,因此路径的最终得分为:
因此问题等价于:寻找一条从 (1,1) 到 (n,m) 的路径,最大化该路径上原有 1 的数量与 min(路径 ? 数,x) 之和。
我们可以采用动态规划,直接计算在消耗 k 次修改的前提下能够获得的最大得分。这样不仅能解决 “不超过 x 次” 的问题,还能自然地取所有 k≤x 的最大值作为答案。
设 dp[i][j][k] 表示从 (1,1) 走到 (i,j),恰好消耗了 k 次修改(将 k 个 ? 变为 1)时,能获得的最大分数。若某状态不可达,则值为 −∞。
根据格子 (i,j) 的字符类型,从上方 (i−1,j) 和左方 (i,j−1) 转移过来:
当前格子为 1
本身就可以得分,不消耗修改次数:
当前格子为 0
不得分,不消耗修改:
当前格子为 ?
有两种选择:
?,不得分:1,消耗 1 次修改,得 1 分(前提 k≥1):初始化:只有起点 (1,1) 可达,根据其字符设定 dp[1][1][⋅] 的值,其余为 −∞。
n,m≤500,若直接开三维数组会达到 500×500×301≈7.5×107,内存过大。观察转移方程,(i,j) 只依赖于第 i−1 行和第 i 行的前一列,因此可以将第一维压缩,只保留当前行的信息:
? 的修改转移 k→k−1,为保证同一格子处的修改次数不被重复利用,转移时 k 应从 x 往 0 逆向循环(类似 01 背包)。这样空间降为 O(m×x),在题目限制下完全可行。
对每组测试数据:
a[1][1] == '1':dp[1][0]=1;若 k>0,也可消耗修改,但没必要(此处消耗无意义)。a[1][1] == '0':dp[1][0]=0。a[1][1] == '?':dp[1][0]=0;dp[1][1]=1(若 x≥1)。best = max(dp[j][k], dp[j-1][k]),再按字符增加得分或考虑修改。以下是修正和优化后的 C++ 实现。该代码基于滚动数组的 DP,并对起点单独初始化,其余格子逐行更新。
cpp1#include <bits/stdc++.h> 2using namespace std; 3 4const int N = 505; // 最大列数 5const int K = 305; // 最大修改次数 x 6const int INF = -1e9; 7 8int dp[N][K]; // dp[j][k]:到达当前行第 j 列,消耗 k 次修改的最大得分 9char a[N][N]; 10int n, m, x; 11 12int main() { 13 int t; 14 scanf("%d", &t); 15 while (t--) { 16 scanf("%d%d%d", &n, &m, &x); 17 // 跳过 x 过大时的无用维度(若 x > 总格子数也可缩小,但此处直接保留 x) 18 for (int i = 1; i <= n; i++) { 19 scanf("%s", a[i] + 1); 20 } 21 22 // 初始化 dp 为负无穷 23 for (int j = 1; j <= m; j++) 24 for (int k = 0; k <= x; k++) 25 dp[j][k] = INF; 26 27 // 单独初始化起点 (1,1) 28 char c = a[1][1]; 29 if (c == '0') { 30 dp[1][0] = 0; 31 } else if (c == '1') { 32 dp[1][0] = 1; 33 } else { // '?' 34 dp[1][0] = 0; 35 if (x >= 1) dp[1][1] = 1; 36 } 37 38 // 处理第一行的其余列 (i=1) 39 for (int j = 2; j <= m; j++) { 40 c = a[1][j]; 41 for (int k = x; k >= 0; k--) { 42 // 只能从左边转移 43 int best = dp[j-1][k]; 44 if (best == INF) { dp[j][k] = INF; continue; } 45 if (c == '1') { 46 best += 1; 47 } else if (c == '?') { 48 // 修改的情况 49 if (k > 0 && dp[j-1][k-1] != INF) { 50 best = max(best, dp[j-1][k-1] + 1); 51 } 52 } 53 dp[j][k] = best; 54 } 55 } 56 57 // 处理后续行 58 for (int i = 2; i <= n; i++) { 59 // 第一列只能从上方来 60 c = a[i][1]; 61 for (int k = x; k >= 0; k--) { 62 int best = dp[1][k]; // dp[1][k] 此时还是上一行的值 63 if (best == INF) { dp[1][k] = INF; continue; } 64 if (c == '1') { 65 best += 1; 66 } else if (c == '?') { 67 if (k > 0 && dp[1][k-1] != INF) { 68 best = max(best, dp[1][k-1] + 1); 69 } 70 } 71 dp[1][k] = best; 72 } 73 // 其余列 74 for (int j = 2; j <= m; j++) { 75 c = a[i][j]; 76 for (int k = x; k >= 0; k--) { 77 int best = max(dp[j][k], dp[j-1][k]); // dp[j][k] 是上方,dp[j-1][k] 是左方 78 if (best == INF) { dp[j][k] = INF; continue; } 79 if (c == '1') { 80 best += 1; 81 } else if (c == '?') { 82 if (k > 0) { 83 int fromUp = dp[j][k-1]; // 上侧消耗 1 修改 84 int fromLeft = dp[j-1][k-1]; // 左侧消耗 1 修改 85 if (fromUp != INF) best = max(best, fromUp + 1); 86 if (fromLeft != INF) best = max(best, fromLeft + 1); 87 } 88 } 89 dp[j][k] = best; 90 } 91 } 92 } 93 94 int ans = 0; 95 for (int k = 0; k <= x; k++) { 96 ans = max(ans, dp[m][k]); 97 } 98 printf("%d\n", ans); 99 } 100 return 0; 101}
dp = INF,保证不可达状态不会参与最优值更新。? 时,k 从 x 递减,确保修改次数不会在同一格子内被重复计算(类似 01 背包的思想)。以题目样例为例(未在题面完全给出,但可自行测试),该代码能够正确输出修改后路径的得分最大值。
本题本质是带修改次数限制的网格路径最优化问题。通过二维滚动背包动态规划,我们可以在 O(n⋅m⋅x) 的时间内高效求解。核心在于正确设计消耗修改的状态转移,以及利用逆向循环避免重复使用修改次数。