1 条题解
-
0
这是一个关于 [JSOI2018] 机器人 的详细题解。
题目背景与分析
题目核心: 在一个 的网格上(构成环面,即左右相通、上下相通),机器人从 出发,只能向右或向下移动,要求经过每个格子恰好一次并回到起点。这实际上是在求网格图上的哈密顿回路。 题目要求对于所有可能的合法路径方案,计算在加入障碍物后,机器人能走的步数总和。
数学性质: 这是一个经典的网格哈密顿回路构造问题。在 的环面网格上,只能向右或向下走,若要不重不漏地遍历所有点,该路径必然具有极强的规律性。 结论:所有合法的路径都可以由一个“基本步长向量” 描述,满足:
- 路径可以看作是由若干个大小为 的“微元矩形”拼接而成的。机器人每次在一个微元内从左上角 走到右下角 ,然后“穿越”到下一个微元的左上角。
- 总共有 个这样的微元。
代码逻辑解析
这份代码利用了上述数学性质,将复杂的全图路径计数问题转化为了在一个较小的“微元”上的动态规划问题。
1. 枚举路径形态 (Outer Loop)
代码首先计算 。 然后枚举 从 到 ,计算 。
int dd = __gcd(n, m); for (int s = 0, t; s <= dd; s++) { t = dd - s; // ... }这里的 决定了路径的“形状”。如果 或 ,则该形态非法。
2. 生成微元序列 (Block Generation)
对于固定的 ,机器人会按照某种顺序遍历网格中的不同区域。代码通过模拟计算出了这些区域(微元)的左上角坐标序列
odr。while (xx || yy) top++, odr[top][0] = xx, odr[top][1] = yy, xx = (xx + s) % n, yy = (yy + t) % m;top即为总微元数,如果数量不对(没有遍历完所有格子),则该 无效。3. 动态规划 (Core DP)
这是解题的关键。我们需要计算 ,根据期望/计数的线性性质,这等价于:对于路径上的每一个点,有多少种方案使得机器人能“存活”到这一步。
我们将所有微元按访问顺序编号为 。 对于第 个微元中的某个格子 ,机器人能走到这里的条件是:
- 全局存活:在第 到 个微元中,机器人没有撞到任何障碍。
- 局部存活:在第 个微元中,机器人从入口走到 的过程中没有撞到障碍。
为了高效计算,代码使用了两个 DP 数组和一个状态压缩(实际上是障碍物的叠加):
pre[i][j]:表示前 个微元中,对应位置 是否曾经出现过障碍。如果出现了,意味着在这个位置,对于当前的 来说是不可通行的(要么之前就撞了,要么现在撞了)。suf[i][j]:表示前 个微元中,对应位置是否出现过障碍。
DP 状态定义:
- :在当前微元内,考虑
pre(即 的障碍并集),从 走到 的路径方案数。 - :在当前微元内,考虑
suf(即 的障碍并集),从 走到终点 的路径方案数。
贡献计算:
ans = (ans + f[i][j] * g[i][j]) % mod;这一项的含义是:路径形态在微元内经过 ,且该路径在 微元完全合法(由 保证后缀合法性,即前面的微元没死),并且在第 个微元能顺利走到 (由 保证前缀合法性,且避开了 的所有障碍)。4. 滚动叠加障碍
// 计算完第 k 个微元后,将第 k 个微元的实际障碍加入 suf,为第 k+1 轮做准备 suf[i][j] |= (mp[(xx + i + n) % n][(yy + j + m) % m] - '0');在每一轮循环中,
pre是临时生成的(包含当前块障碍),而suf是随着循环滚动更新的(只包含之前的障碍)。题解总结
- 转化:利用 将 的大网格路径问题分解为 种基本形态。每种形态由 个大小为 的微元块串联而成。
- 计数:问题转化为对每个微元块内的每个点,计算“机器人能活着走到该点”的方案数。
- DP:
- 利用
pre数组记录“截止到当前块,位置 是否有障碍”。 - 利用
suf数组记录“截止到上一块,位置 是否有障碍”。 - 计算在 块的约束下到达 的方案。
- 计算在 块的约束下,路径通过 并能走完该块几何形状的方案(保证了之前的块都不会撞车)。
- 乘积 即为经过该点且合法的方案数,累加到答案中。
- 利用
- 复杂度:
- 外层枚举 :。
- 内层遍历所有微元:总面积为 。
- DP 复杂度与微元面积成正比。
- 总时间复杂度:。鉴于 较小(通常 ),该复杂度完全可以通过。
C++ 代码注释版
#include <iostream> #include <algorithm> #include <cstring> typedef long long ll; using namespace std; // IO 优化部分省略... // 假设已有 Read 和 Write 函数 const int MAXN = 60, MAXM = 2510, mod = 998244353; int n, m; char mp[MAXN][MAXN]; // 地图 int odr[MAXM][2], top; // 记录微元块的左上角坐标序列 int pre[MAXN][MAXN], suf[MAXN][MAXN]; // 障碍物并集掩码 ll f[MAXN][MAXN], g[MAXN][MAXN], ans; // DP数组和答案 inline void Solve() { Read(n), Read(m); for (int i = 0; i < n; i++) Read(mp[i]); int dd = __gcd(n, m); ans = 0; // 枚举步长向量 (s, t),满足 s + t = gcd(n, m) for (int s = 0, t; s <= dd; s++) { t = dd - s; if (s >= n || t >= m) continue; // 生成微元块的遍历顺序 top = 0; int xx = s, yy = t; // 模拟路径直到回到原点(或者循环) while (xx || yy) top++, odr[top][0] = xx, odr[top][1] = yy, xx = (xx + s) % n, yy = (yy + t) % m; // 补上最后一个回到 (1,1) 的块(代码中逻辑实际上是把 (1,1) 放在序列最后处理或作为哨兵) top++, odr[top][0] = odr[top][1] = 1; // 校验生成的块数是否覆盖全图 if (top != n * m / dd) continue; // 微元块的实际大小是 (s+1) * (t+1) s++, t++; // 初始化障碍掩码 for (int i = 1; i <= s; i++) for (int j = 1; j <= t; j++) pre[i][j] = suf[i][j] = 0; // 遍历每一个微元块 for (int cs = 1; cs <= top; cs++) { // 计算当前块在原图中的左上角偏移量 xx = odr[cs][0] - s, yy = odr[cs][1] - t; // pre = 当前块障碍 | 之前所有块的障碍 for (int i = 1; i <= s; i++) for (int j = 1; j <= t; j++) pre[i][j] |= (mp[(xx + i + n) % n][(yy + j + m) % m] - '0'); // 清空 DP 数组 memset(f, 0, sizeof(f)), memset(g, 0, sizeof(g)); // DP 计算 f: 从左上角走到 (i, j) 且不碰到 pre 中障碍的方案数 for (int i = 1; i <= s; i++) for (int j = 1; j <= t; j++) if (i == 1 && j == 1) f[i][j] = !pre[i][j]; else if (!pre[i][j]) f[i][j] = (f[i - 1][j] + f[i][j - 1]) % mod; // DP 计算 g: 从 (i, j) 走到右下角 且不碰到 suf 中障碍的方案数 // 注意 g 使用的是 suf (即 1...k-1 块的障碍),这意味着只要之前没死,且这一步本身也没死(由f保证) for (int i = s; i >= 1; i--) for (int j = t; j >= 1; j--) if (i == s && j == t) g[i][j] = !suf[i][j]; else if (!suf[i][j]) g[i][j] = (g[i + 1][j] + g[i][j + 1]) % mod; // 统计答案 for (int i = 1; i <= s; i++) for (int j = 1; j <= t; j++) // (i!=s || j!=t) 是因为最后一个点往往是连接点,避免重复计算或边界处理 if (i != s || j != t) ans = (ans + f[i][j] * g[i][j]) % mod; // 更新 suf,将当前块的障碍加入,供下一个块使用 for (int i = 1; i <= s; i++) for (int j = 1; j <= t; j++) suf[i][j] |= (mp[(xx + i + n) % n][(yy + j + m) % m] - '0'); } // 还原 s, t 供循环使用 s--, t--; } cout << ans << '\n'; } int main() { int T; Read(T); while (T--) Solve(); return 0; }
- 1
信息
- ID
- 6051
- 时间
- 1000ms
- 内存
- 256MiB
- 难度
- 10
- 标签
- 递交数
- 1
- 已通过
- 1
- 上传者