1 条题解

  • 0
    @ 2025-12-10 20:46:03

    这是一个关于 [JSOI2018] 机器人 的详细题解。

    题目背景与分析

    题目核心: 在一个 N×MN \times M 的网格上(构成环面,即左右相通、上下相通),机器人从 (1,1)(1,1) 出发,只能向右或向下移动,要求经过每个格子恰好一次并回到起点。这实际上是在求网格图上的哈密顿回路。 题目要求对于所有可能的合法路径方案,计算在加入障碍物后,机器人能走的步数总和。

    数学性质: 这是一个经典的网格哈密顿回路构造问题。在 N×MN \times M 的环面网格上,只能向右或向下走,若要不重不漏地遍历所有点,该路径必然具有极强的规律性。 结论:所有合法的路径都可以由一个“基本步长向量” (s,t)(s, t) 描述,满足:

    1. s+t=gcd(N,M)s + t = \gcd(N, M)
    2. 路径可以看作是由若干个大小为 (s+1)×(t+1)(s+1) \times (t+1) 的“微元矩形”拼接而成的。机器人每次在一个微元内从左上角 (1,1)(1,1) 走到右下角 (s+1,t+1)(s+1, t+1),然后“穿越”到下一个微元的左上角。
    3. 总共有 N×Mgcd(N,M)\frac{N \times M}{\gcd(N, M)} 个这样的微元。

    代码逻辑解析

    这份代码利用了上述数学性质,将复杂的全图路径计数问题转化为了在一个较小的“微元”上的动态规划问题。

    1. 枚举路径形态 (Outer Loop)

    代码首先计算 d=gcd(N,M)d = \gcd(N, M)。 然后枚举 ss00dd,计算 t=dst = d - s

    int dd = __gcd(n, m);
    for (int s = 0, t; s <= dd; s++) {
        t = dd - s;
        // ...
    }
    

    这里的 (s,t)(s, t) 决定了路径的“形状”。如果 sns \ge ntmt \ge m,则该形态非法。

    2. 生成微元序列 (Block Generation)

    对于固定的 (s,t)(s, t),机器人会按照某种顺序遍历网格中的不同区域。代码通过模拟计算出了这些区域(微元)的左上角坐标序列 odr

    while (xx || yy)
        top++, odr[top][0] = xx, odr[top][1] = yy, xx = (xx + s) % n, yy = (yy + t) % m;
    

    top 即为总微元数,如果数量不对(没有遍历完所有格子),则该 (s,t)(s, t) 无效。

    3. 动态规划 (Core DP)

    这是解题的关键。我们需要计算 f(S)\sum f(S),根据期望/计数的线性性质,这等价于:对于路径上的每一个点,有多少种方案使得机器人能“存活”到这一步

    我们将所有微元按访问顺序编号为 1,2,,K1, 2, \dots, K。 对于第 kk 个微元中的某个格子 (i,j)(i, j),机器人能走到这里的条件是:

    1. 全局存活:在第 11k1k-1 个微元中,机器人没有撞到任何障碍。
    2. 局部存活:在第 kk 个微元中,机器人从入口走到 (i,j)(i, j) 的过程中没有撞到障碍。

    为了高效计算,代码使用了两个 DP 数组和一个状态压缩(实际上是障碍物的叠加):

    • pre[i][j]:表示前 kk 个微元中,对应位置 (i,j)(i, j) 是否曾经出现过障碍。如果出现了,意味着在这个位置,对于当前的 kk 来说是不可通行的(要么之前就撞了,要么现在撞了)。
    • suf[i][j]:表示前 k1k-1 个微元中,对应位置是否出现过障碍。

    DP 状态定义

    • f[i][j]f[i][j]:在当前微元内,考虑 pre(即 1k1 \dots k 的障碍并集),从 (1,1)(1,1) 走到 (i,j)(i,j) 的路径方案数。
    • g[i][j]g[i][j]:在当前微元内,考虑 suf(即 1k11 \dots k-1 的障碍并集),从 (i,j)(i,j) 走到终点 (s+1,t+1)(s+1, t+1) 的路径方案数。

    贡献计算ans = (ans + f[i][j] * g[i][j]) % mod; 这一项的含义是:路径形态在微元内经过 (i,j)(i,j),且该路径在 1k11 \dots k-1 微元完全合法(由 gg 保证后缀合法性,即前面的微元没死),并且在第 kk 个微元能顺利走到 (i,j)(i,j)(由 ff 保证前缀合法性,且避开了 1k1 \dots k 的所有障碍)。

    4. 滚动叠加障碍

    // 计算完第 k 个微元后,将第 k 个微元的实际障碍加入 suf,为第 k+1 轮做准备
    suf[i][j] |= (mp[(xx + i + n) % n][(yy + j + m) % m] - '0');
    

    在每一轮循环中,pre 是临时生成的(包含当前块障碍),而 suf 是随着循环滚动更新的(只包含之前的障碍)。

    题解总结

    1. 转化:利用 gcd(N,M)\gcd(N, M)N×MN \times M 的大网格路径问题分解为 gcd(N,M)\gcd(N, M) 种基本形态。每种形态由 NMgcd(N,M)\frac{NM}{\gcd(N,M)} 个大小为 (s+1)×(t+1)(s+1) \times (t+1) 的微元块串联而成。
    2. 计数:问题转化为对每个微元块内的每个点,计算“机器人能活着走到该点”的方案数。
    3. DP
      • 利用 pre 数组记录“截止到当前块,位置 (i,j)(i,j) 是否有障碍”。
      • 利用 suf 数组记录“截止到上一块,位置 (i,j)(i,j) 是否有障碍”。
      • f[i][j]f[i][j] 计算在 1k1 \dots k 块的约束下到达 (i,j)(i,j) 的方案。
      • g[i][j]g[i][j] 计算在 1k11 \dots k-1 块的约束下,路径通过 (i,j)(i,j) 并能走完该块几何形状的方案(保证了之前的块都不会撞车)。
      • 乘积 f×gf \times g 即为经过该点且合法的方案数,累加到答案中。
    4. 复杂度
      • 外层枚举 (s,t)(s,t)O(gcd(N,M))O(\gcd(N, M))
      • 内层遍历所有微元:总面积为 N×MN \times M
      • DP 复杂度与微元面积成正比。
      • 总时间复杂度:O(TNMgcd(N,M))O(T \cdot N \cdot M \cdot \gcd(N, M))。鉴于 N,MN, M 较小(通常 50\le 50),该复杂度完全可以通过。

    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
    上传者