P7668 JOI 2018 Final 团子制作 / Dango Maker 题解
复盘
赛时写的二维DP,转移非常复杂,而且正确性得不到保证
赛后发现题解非常短小,对于这个贪心的性质分析感到十分巧秒,特此记录
题意简述
在 \(N \times M\) 的字符网格上,每个格子放着一个颜色为 R、G 或 W 的团子。每次选三个连续格子(横向或竖向),颜色必须恰好为 R G W,且每个格子只能被使用一次。求最多能串多少串团子。
核心分析
关键观察:冲突构型
一根横向串和一根竖向串如果要共用某个格子,几何上只有三种重叠方式:
构型 A(共享 G 格) 构型 B(共享 R 格) 构型 C(共享 W 格)
R G W R R
G R G W G
W W R G W
逐一检验这三种构型能否实际发生:
| 构型 | 共享格在横向串中的角色 | 共享格在竖向串中的角色 | 是否可能 |
|---|---|---|---|
| A | G(中心) | R(顶端) | 不可能,一个格子不可能既是 G 又是 R |
| B | R(左端) | G(中心) | 不可能,一个格子不可能既是 R 又是 G |
| C | W(右端) | W(底端) | 可能,两边都要求 W |
结论:横向串与竖向串的冲突只可能在构型 C 下发生。
冲突的位置关系
以每根串的 G 格作为标志点(一根串由 G 的位置唯一确定),分析构型 C 中两个 G 点的位置关系:
j j+1
i-1 G' ← 竖向串(向下:R→G'→W)
i G ←───── 横向串(向右:R→G→W)
-
竖向串标志点 \(G'\) 在 \((i-1,; j+1)\),使用格子 \((i,; j+1)\) 作为 W。
-
横向串标志点 \(G\) 在 \((i,; j)\),也使用格子 \((i,; j+1)\) 作为 W。
两个 G 点的相对位置恰好是「右上方」关系 :\(G' = (i-1,; j+1)\),\(G = (i,; j)\)。
这意味着:处理 \(G(i,j)\) 时,只需检查它的右上方邻居 \(G(i-1,; j+1)\) 是否与之冲突,其余位置不会产生冲突。
算法设计
DP 状态
\f\\_{i,j,0/1} = \\text{以 } (i,j) \\text{ 为中心的 G 能否形成横向(0)/竖向(1) 串} \\
其中 \(f\{i,j,0} = 1\) 当且仅当 \(s\{i,j-1} = \text{R}\) 且 \(s\_{i,j+1} = \text{W}\),竖向同理。
贪心转移
按行优先序扫描。处理 \(G(i,j)\) 时,右上方 \(G(i-1,; j+1)\) 已处理完毕。
冲突规则(由构型 C 导出):
| 右上方 G' 的选择 | 占用的冲突格 | 当前 G 被禁止的方向 |
|---|---|---|
| 横向 | \((i-1, j)\) 作为 R | 竖向(竖向也需要 \((i-1, j)\) 作为 R) |
| 竖向 | \((i, j+1)\) 作为 W | 横向(横向也需要 \((i, j+1)\) 作为 W) |
核心逻辑:如果 G' 只能走某个方向,就被迫占用冲突格,从而禁止 G 的对应方向。如果 G' 两个方向都可行,它有弹性去适应 G 的需求,因此不限制 G。
代码实现为:
cpp
if (f[i-1][j+1][0] || f[i-1][j+1][1]) { // G' 存在且至少有一个方向可行
f[i][j][0] &= f[i-1][j+1][0]; // G' 不能横向 → G' 被迫竖向 → 禁止当前横向
f[i][j][1] &= f[i-1][j+1][1]; // G' 不能竖向 → G' 被迫横向 → 禁止当前竖向
}
ans += (f[i][j][0] || f[i][j][1]); // 只要还有一个可行方向就算一根
贪心正确性
冲突关系只在反斜对角线 (\(i+j = \text{const}\))上相邻的 G 点之间传递,冲突图是若干条链(而非一般图)。
在链上做贪心是正确的:
-
\(G'\) 能两个方向都走 \(\Rightarrow\) 它可以迁就 \(G\) 的任何选择,不损失任何一根。
-
\(G'\) 只能走一个方向 \(\Rightarrow\) 被迫占用冲突格,\(G\) 对应方向必然失效,无可选择。
-
\(G'\) 两个方向都不行 \(\Rightarrow\) 无冲突,\(G\) 不受影响。
每一步的局部最优就是全局最优,因为每条链上的决策互不影响。
AC code
cpp
#include <bits/stdc++.h>
using namespace std;
const int N = 3e3 + 5;
int n, m;
char s[N][N];
int f[N][N][2];
int main() {
freopen("dango.in", "r", stdin);
freopen("dango.out", "w", stdout);
ios::sync_with_stdio(false);
cin.tie(0);
cin >> n >> m;
for (int i = 1; i <= n; i++)
for (int j = 1; j <= m; j++)
cin >> s[i][j];
int ans = 0;
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= m; j++) {
if (s[i][j] == 'G') {
// 判断横向/竖向是否基本可行
f[i][j][0] = (s[i][j-1] == 'R' && s[i][j+1] == 'W');
f[i][j][1] = (s[i-1][j] == 'R' && s[i+1][j] == 'W');
// 与右上方 G' 的冲突处理
if (f[i-1][j+1][0] || f[i-1][j+1][1]) {
f[i][j][0] &= f[i-1][j+1][0];
f[i][j][1] &= f[i-1][j+1][1];
}
ans += (f[i][j][0] || f[i][j][1]);
}
}
}
cout << ans << '\n';
return 0;
}
复杂度
-
时间:\(O(NM)\),每个格子常数次操作。
-
空间:\(O(NM)\),f 数组。
总结
| 步骤 | 要点 |
|---|---|
| 1. 分析冲突构型 | 横竖串重叠只有 3 种几何构型,其中仅「共享 W 格」一种可行 |
| 2. 确定冲突位置关系 | 冲突只发生在 \(G(i,j)\) 与右上方 \(G(i-1,; j+1)\) 之间 |
| 3. 贪心/DP 转移 | 行优先扫描;G' 有弹性则不限制,G' 被迫单方向则禁止对应方向 |
| 4. 正确性 | 冲突图是反斜对角线上的链,链上贪心即全局最优 |