NOI2025 做题记录

NOI2025 做题记录

\(0 + 100 + 100 + 2 + 0 + 100 + 8 + 10 = 320\)

D1T1 机器人(robot)

首先想到暴力拆点:把一个点拆成 \(k\) 个点,在新图中,点 \((u, i)\) 表示到达 \(u\)\(p = i\) 的状态。于是新图的点数为 \(nk\)。我们考虑两种点之间的转移:一种是令 \(p\) 增加/减少 \(1\),即 \((u, i) \to (u, i + 1)\)\((u, i - 1)\);另一种是从 \(u\) 沿着它的第 \(i\) 条出边走到某个点 \(v\)\(p\) 保持不变。那么边数为 \(O(nk + m)\)。在新图上跑 Dijkstra 即可。

上述做法的点数和边数都太多,考虑优化。注意到一个点不必拆成 \(k\) 个状态,因为如果要从 \(u\) 移动到别的点,\(p\) 不能超过 \(u\) 的出度 \(d_u\)。所以一个点只有 \(d_u\) 个状态是有用的。

那么我们只用拆出 \(\sum_{u} d_u = O(m)\) 个点,点之间的转移同上,即边数为 \(O(m)\)。有一些细节需要处理:如果现在位于 \((u, i)\),沿着 \(u\) 的第 \(i\) 条出边走到 \(v\),而 \(i\) 可能超过了 \(d_v\),相当于图中不存在 \((v, i)\) 这个点。解决方法是简单的:如果要从 \(v\) 继续走,一定要先把 \(p\) 减少至 \(d_v\)。所以可以直接从 \((u, i)\) 转移到 \((v, d_v)\)。不过到达 \(v\) 的最短路可能不需要把 \(p\) 减小到 \(d_v\)(因为不需要从 \(v\) 继续走),所以要先用不调整 \(p\) 的代价更新到达 \(v\) 的最短路。

时间复杂度 \(O(m \log m)\)代码

D2T2 三目运算符(ternary)

解决此类问题的第一步一定是用尽量简洁的方式刻画题目的结构。对本题,我们要找出字符串 \(S\) 在变换 \(f\) 下保持不变的条件。

\(f(S) = T = t_1t_2\dots t_n\)。如果 \(S = T\),则对所有 \(i\),有 \(s_i = t_i\)。如果 \(s_{i - 2} = 0\),那么 \(t_i = s_i\),这个条件已经满足。否则 \(s_{i - 2} = 0\),此时 \(t_{i} = s_{i - 1}\)。我们想让 \(t_{i} = s_{i}\),所以一定有 \(s_{i - 1} = s_{i}\)。也就是说,如果 \(S\) 保持不变,那么 \(S\) 中的所有 \(1\),后面都必须跟着两个相同的字符。等价的说法是:

\(S\) 保持不变的充要条件为:不存在 \(110\)\(101\) 子串。

现在我们找到了 \(S\) 保持不变的充要条件,下一步是研究 \(S\) 在经过多少次 \(f\) 变化后能满足此条件。如果一开始 \(S\) 就没有这两个子串,则答案为 \(0\)。否则我们分类讨论一下:

  1. \(S\) 中有 \(110\) 子串,没有 \(101\) 子串:

    考虑 \(110\) 子串第一次出现的位置,这个子串中的 \(0\) 的下标为 \(p\)。设第一个 \(110\) 之后出现的字符为 \(a\)。令 \(S \gets f(S)\)。由于这是第一个出现的 \(110\),且不存在 \(101\) 子串,所以对于 \(i < p\)\(s_i\) 在变化后都不会改变。我们只考虑子串 \(110a\) 变化后的结果,有:

    \[110a \to b110 \]

    相当于把第一个 \(110\) 的位置向后移了一位,而这次移动并不会导致前面出现 \(110\)\(101\) 子串\(^{\ast}\)。最终 \(110\) 移动到末尾时,再经过一次变换,\(110\) 会变成 \(111\),于是 \(110\) 子串消失了,\(S\) 之后保持不变。因此,在这种情况下,答案为 \(110\) 向后移动的次数,即 \(n - p + 1\)

    \([\ast]\):严谨证明可能需要一些分类讨论,但打表可以直观地观察到这一点。

  2. \(S\) 中有 \(101\) 子串,没有 \(110\) 子串:

    此时 \(S\) 中有若干形如 \(10101\dots01\) 的子串(可以形式化地记为 \(1 + k \cdot01\),其中 \(k \cdot 01\) 表示 \(k\)\(01\) 拼接起来,\(k \ge 0\)),且这些子串都不挨着,其它地方都为 \(0\)(否则就存在 \(110\) 子串)。简单模拟发现,这些子串经过一次变换后都会变成 \(1 + k \cdot 00\),然后不会再变化。所以此时答案为 \(1\)

  3. \(S\) 中同时存在 \(110\)\(101\) 子串:

    注意到 \(110\) 子串向后推的过程实际上不受原位置值的影响,所以 \(101\) 完全不影响 \(110\) 向后推的过程,此时答案和第一种情况相同。

综上所述,我们计算答案的过程为:

  1. 如果存在 \(110\) 子串,设第一个 \(110\) 子串中 \(0\) 的下标为 \(p\),则答案为 \(n - p + 1\)
  2. 否则如果存在 \(101\) 子串,答案为 \(1\)
  3. 否则答案为 \(0\)

(这些结论并不难发现,因为每一步的推导都有迹可循。但我赛时注意力没那么好,写了个暴力打表才观察到这些规律,不过打表也不失为一种寻找结论的好方式。)

据此可以写出单次询问 \(O(n)\) 的做法。考虑优化。

显然可以考虑线段树。根据求答案的过程,容易看出需要维护区间内是否有 \(101\) 子串,是否有 \(110\) 子串,以及第一个 \(110\) 子串出现的位置。由于有区间翻转操作,还需要对应地维护区间内是否有 \(010\) 子串,是否有 \(001\) 子串,以及第一个 \(001\) 子串出现的位置。为了合并区间,对于所有长度为 \(1\)\(2\)\(01\)\(x\),需要维护每个区间是否含有前缀 \(x\) 和后缀 \(x\)。合并时暴力枚举所有情况即可。维护的量比较多,常数略大,且存在一些实现细节。

以下是线段树信息设计的参考代码:

struct Info {
    bool f101, f110, f010, f001;
    int l, r, pos1 = INF, pos2 = INF; // pos1 和 pos2 分别为第一个 110 和第一个 001 的位置
    array<bool, 2> p{}, s{}; // 是否存在某个长度为 1 的前/后缀
    array<array<bool, 2>, 2> p2{}, s2{}; // 是否存在某个长度为 2 的前/后缀

    int len() const {
        return r - l + 1;
    }

    friend Info operator + (const Info &A, const Info &B) {
        Info C;
        C.f101 = A.f101 || B.f101 || (A.s[1] && B.p2[0][1]) || (A.s2[1][0] && B.p[1]);
        C.f110 = A.f110 || B.f110 || (A.s[1] && B.p2[1][0]) || (A.s2[1][1] && B.p[0]);
        C.f010 = A.f010 || B.f010 || (A.s[0] && B.p2[1][0]) || (A.s2[0][1] && B.p[0]);
        C.f001 = A.f001 || B.f001 || (A.s[0] && B.p2[0][1]) || (A.s2[0][0] && B.p[1]);
        C.l = A.l, C.r = B.r;
        C.pos1 = min(A.pos1, B.pos1), C.pos2 = min(A.pos2, B.pos2);
        if(A.s[1] && B.p2[1][0]) {
            chmin(C.pos1, A.r + 2);
        }
        if(A.s2[1][1] && B.p[0]) {
            chmin(C.pos1, A.r + 1);
        }
        if(A.s[0] && B.p2[0][1]) {
            chmin(C.pos2, A.r + 2);
        }
        if(A.s2[0][0] && B.p[1]) {
            chmin(C.pos2, A.r + 1);
        }
        for(int x: {0, 1}) {
            C.p[x] = A.p[x], C.s[x] = B.s[x];
            for(int y: {0, 1}) {
                if(A.len() >= 2) {
                    C.p2[x][y] = A.p2[x][y];
                } else {
                    C.p2[x][y] = A.p[x] && B.p[y];
                }

                if(B.len() >= 2) {
                    C.s2[x][y] = B.s2[x][y];
                } else {
                    C.s2[x][y] = A.s[x] && B.s[y];
                }
            }
        }
        return C;
    }

    void apply() {
        // 区间翻转,暴力交换所有的变量
        swap(f101, f010), swap(f110, f001);
        swap(pos1, pos2);
        for(int x: {0, 1}) {
            int nx = x ^ 1;
            if(x < nx) {
                swap(p[x], p[nx]);
                swap(s[x], s[nx]);
            }
            for(int y: {0, 1}) {
                int ny = y ^ 1;
                int a = (x << 1) | y;
                int b = (nx << 1) | ny;
                // 按字典序比较字符串,以免重复交换
                if(a < b) {
                    swap(p2[x][y], p2[nx][ny]);
                    swap(s2[x][y], s2[nx][ny]);
                }
            }
        }
    }
};

完整代码

总时间复杂度 \(O(n + q \log n)\)

posted @ 2025-07-23 20:45  DengStar  阅读(55)  评论(0)    收藏  举报