杂题
2025-07-12
暂无标签

我生待明日,万事成蹉跎。


town

http://222.180.160.110:61235/contest/6407/problem/1

给定一棵大小为 \(n\) 的树,点有点权,欲将树划分为若干个连通块,使得每个块内的点权异或和都为 \(x\),求方案数,模 \(998244353\)

\(n\le 10^6,0\le x\le 10^9\)

\(u\) 引导的子树异或和为 \(s_u\)。把连通块转化为对断边的讨论。考察一条最末端的边,其下方子树的异或和一定为 \(x\)

简单数归可得断掉的每一条边,其下方子树异或和为 \(x\)\(0\)(那么显然有解的充要条件是 \(s_1=0\lor s_1=x\))。考虑树形 DP,令 \(f_{u,0/1}\) 表示当前 DP 到 \(u\) 子树头上那条边,它连接了一个异或和为 \(s_u\) (或 \(s_u\oplus x\))的连通块的合法方案数,分别代表已经『断』掉了偶数 / 奇数个合法连通块。

首先自然可以一条边都不断,\(f_{u,0}=1\)。那么更新就应为:

\[ f'_{u,0}=f_{u,0}\cdot f_{v,0}+f_{u,1}\cdot f_{v,1}\\ f'_{u,1}=f_{u,0}\cdot f_{v,1}+f_{u,1}\cdot f_{v,0} \]

接着,如果 \(s_u\) 恰好为 \(0\)\(x\),这条边连接的连通块的异或和就可以是 \(x\),那么就可以在这条边处断开,提供一个空连通块(加入异或和为 \(0\) 的方案)。

最后注意 \(x\) 可能为 \(0\),需要特殊处理——每个 \(s_u=0\) 处都可以断开。

这个 DP 状态实在非常新奇;同时 DP 和删边又是分开的,显得非常割裂。看 sol、听 grisses 讲解的时候都完全一头雾水。quack 说这是因为我没做过连通块 DP /ll

#include <bits/stdc++.h>
const int mod = 998244353;
int main() {
#ifdef ONLINE_JUDGE
    std::ios::sync_with_stdio(false);
    std::cin.tie(nullptr), std::cout.tie(nullptr);
#else
    std::freopen("ex_town1.in", "r", stdin);
    std::freopen(".out", "w", stdout);
#endif
    int n, m;
    std::cin >> n >> m;
    std::vector<int> a(n + 1), s(n + 1);
    for (int i = 1; i <= n; ++i)
        std::cin >> a[i];
    std::vector<std::vector<int> > g(n + 1);
    for (int i = 1, x, y; i < n; ++i) {
        std::cin >> x >> y;
        g[x].push_back(y), g[y].push_back(x);
    }
    std::function<void(int, int)> DFS = [&](int x, int fa) {
        s[x] = a[x];
        for (auto i : g[x])
            if (i != fa) {
                DFS(i, x);
                s[x] ^= s[i];
            }
        return;
    };
    DFS(1, -1);
    if (s[1] != 0 && s[1] != m) {
        std::cout << 0 << '\n';
        return 0;
    }
    if (m == 0) {
        auto res(1ll);
        for (int i = 2; i <= n; ++i)
            if (s[i] == 0)
                (res *= 2) %= mod;
        std::cout << res << '\n';
    }
    else {
        std::vector<std::array<long long, 2> > f(n + 1);
        DFS = [&](int x, int fa) {
            f[x][0] = 1ll;
            for (auto i : g[x])
                if (i != fa) {
                    DFS(i, x);
                    std::tie(f[x][0], f[x][1]) = std::make_tuple((f[x][0] * f[i][0] + f[x][1] * f[i][1]) % mod, (f[x][0] * f[i][1] + f[x][1] * f[i][0]) % mod);
                }
            if (x != 1) {
                if (s[x] == m)
                    (f[x][1] += f[x][0]) %= mod;
                if (s[x] == 0)
                    (f[x][0] += f[x][1]) %= mod;
            }
            // printf("f[%d][0] = %lld, f[%d][1] = %lld\n", x, f[x][0], x, f[x][1]);
            return;
        };
        DFS(1, -1);
        std::cout << (s[1] == m ? f[1][0] : f[1][1]) << '\n';
    }
    return 0;
}

perm

http://222.180.160.110:61235/contest/6407/problem/3

给定一个长度为 \(n\)、元素两两不同的序列,你可以交换任意两个元素。试使用最少交换次数使得序列有序,问方案数。

\(n\le 10^6\)

对于一个下标,将它和它上面元素的 rank 连有向边,那么每个下标的出、入度都为 \(1\),则图由若干个简单环组成。

接着,考虑一次任意一次交换 \((x,y)\) 的操作会带来什么:

  • \(x,y\) 不在同一环上:

    该操作使得 \(x,y\) 所在的环合为一个大环。
  • \(x,y\) 在同一环上:

    该操作使得环以 \(x,y\) 为分界线裂成两个小环。

以得到 \(n\) 个自环为目标,数归得到,对于最优方案,当且仅当每次交换的目标都在同一环上。

考虑方案数。把一个大小为 \(k\) 的环,欲将其拆成 \(k\) 个自环,共需 \(k-1\)有序 的拆解。

考虑 DP,令 \(f_k\) 为方案数,那么 \(f_k=\dfrac {k\sum\limits_{j=1}^{k-1} f_j\cdot f_{k-j}\cdot C_{k-2}^j}2\)。尝试打表,(惊讶地)发现 \(f_k=k^{k-2}\)

不同的环之间的操作可以交错,看作多重集排列即可。

#include <bits/stdc++.h>
const int mod = 998244353;
int main() {
    std::ios::sync_with_stdio(false);
    std::cin.tie(nullptr), std::cout.tie(nullptr);
    int n;
    std::cin >> n;
    std::vector<int> a(n + 1), p(n + 1);
    for (int i = 1; i <= n; ++i)
        std::cin >> a[i], p[i] = i;
    std::sort(p.begin() + 1, p.end(), [&](int i, int j) { return a[i] < a[j]; });
    std::vector<int> f(n + 1), siz(n + 1, 1);
    std::iota(f.begin() + 1, f.end(), 1);
    std::function<int(int)> find = [&](int x) {
        return x == f[x] ? x : f[x] = find(f[x]);
    };
    auto merge = [&](int x, int y) {
        x = find(x), y = find(y);
        if (x != y)
            siz[y] += siz[x], f[x] = y;
        return;
    };
    for (int i = 1; i <= n; ++i)
        merge(i, p[i]);
    std::vector<long long> g(n + 1), fac(n + 1), inv(n + 1);
    g[1] = g[2] = 1ll, fac[0] = inv[0] = 1ll;
    for (int i = 1; i <= n; ++i)
        fac[i] = fac[i - 1] * i % mod;
    auto qkp = [&](long long x, int y) {
        auto res(1ll);
        for (; y; (x *= x) %= mod, y >>= 1)
            if (y & 1)
                (res *= x) %= mod;
        return res;
    };
    inv[n] = qkp(fac[n], mod - 2);
    for (int i = n - 1; i; --i)
        inv[i] = inv[i + 1] * (i + 1) % mod;
    for (int i = 3; i <= n; ++i)
        g[i] = qkp(i, i - 2);
    int cnt(0);
    auto res(1ll);
    for (int i = 1; i <= n; ++i)
        if (f[i] == i) {
            cnt += siz[i] - 1;
            (res *= g[siz[i]]) %= mod;
            (res *= inv[siz[i] - 1]) %= mod;
        }
    (res *= fac[cnt]) %= mod;
    std::cout << res << '\n';
    return 0;
}

CF2122D Traffic Lights

https://codeforces.com/contest/2122/problem/D

给你一个由 \(n\) 个顶点和 \(m\) 条边组成的简单无向连通图。在顶点 \(1\) 有一个标记。认为初始时间为 \(t=0\),在 \(t\) 秒后,如果标记位于点 \(u\) ,则必须执行以下操作之一:

  • 等待一秒。
  • 将标记沿 \(u\) 的第 \((t \bmod \mathrm{deg}(u) + 1)\) 条边移动,花费一秒。

计算将标记从顶点 \(1\) 移到顶点 \(n\) 所需的最短时间,以及在使总时间最小化的同时所能达到的最短等待时间。\(m\le 5\times 10^5\)

能够发现这是一个边权和 dis 有关的最短路,本着分层图一类的想法,考察答案的最值。发现一个很松的上界是 \(O(m)\);发现实际情况会比这个乐观得多,这个时候就会考虑用一些暴力做法草过去了。

实际上有一个上界是 \(3n\):取一条最短路,则不存在路径外的点,与路径上至少四个点有连边,否则可以更新最短路。设最短路长 \(k\),让每次等待时间取满,考察答案发现为 \(2k\)(路径上点之间)、\(3(n-k)\)(路径上与路径外),共为 \(\le 3n\)。还有一个上确界是 \(2n-3\),可以欣赏一下 https://codeforces.com/blog/entry/144876?#comment-1295568,我没看懂 😰

总之可以猜到暴力 dij 上 DP 得到…… 啊题解怎么说是 \(O(n^2+m)\) 的。原因是求得答案上界之后就能暴力模拟这个过程了 😅

#include <bits/stdc++.h>
const int inf = 0x3f3f3f3f;
int main() {
#ifdef ONLINE_JUDGE
    std::ios::sync_with_stdio(false);
    std::cin.tie(nullptr), std::cout.tie(nullptr);
#else
    std::freopen(".in", "r", stdin);
    std::freopen(".out", "w", stdout);
#endif
    int T;
    for (std::cin >> T; T--; ) {
        int n, m;
        std::cin >> n >> m;
        std::vector<std::vector<int> > g(n + 1);
        for (int x, y; m--; ) {
            std::cin >> x >> y;
            g[x].push_back(y), g[y].push_back(x);
        }
        std::vector<std::vector<int> > dis(2, std::vector<int> (n + 1, inf));
        dis[1][1] = 0;
        for (int t = 0; ; ++t) {
            auto &d = dis[t & 1], &la = dis[(t & 1) ^ 1];
            std::vector<int> (n + 1, inf).swap(d);
            for (int i = 1; i <= n; ++i) {
                int to = t % ((int)g[i].size());
                d[g[i][to]] = std::min(d[g[i][to]], la[i]);
                d[i] = std::min(d[i], la[i] + 1);
            }
            if (d[n] != inf) {
                std::cout << t + 1 << ' ' << d[n] << '\n';
                break;
            }
        }
    }
    return 0;
}

ARC202A Merge and Increment

https://atcoder.jp/contests/arc202/tasks/arc202_a

假设相邻元素全部不同,至少在当前这一步有一个很显然的策略(记为 策略一)是选全局最小值,把它和它左边、右边更小的一个 merge 起来。

注意到如果当前全局最小值有相邻的偶数个,可以把相邻项合并一次,得到长度减半的区间,一定不劣(记为 策略二);假如当前全局最小值存在相邻的奇数个,很容易注意到有两种处理方式:

  1. 选取偶数个应用策略二,剩余一个应用策略一。
  2. 花一步,补一个全局最小值,转化为策略二。

很容易发现第二种方式一定不劣 😅。模拟即可。

#include <bits/stdc++.h>
int main() {
#ifdef ONLINE_JUDGE
    std::ios::sync_with_stdio(false);
    std::cin.tie(nullptr), std::cout.tie(nullptr);
#else
    std::freopen(".in", "r", stdin);
    std::freopen(".out", "w", stdout);
#endif
    int T;
    for (std::cin >> T; T--; ) {
        int n;
        std::cin >> n;
        std::vector<std::pair<int, int> > a(1);
        {
            for (int i = 1, x; i <= n; ++i) {
                std::cin >> x;
                if (x != a.back().first)
                    a.emplace_back(x, 1);
                else
                    ++a.back().second;
            }
            n = (int)a.size() - 1;
        }
        std::vector<int> pre(n + 2), nex(n + 1), tag(n + 1);
        std::priority_queue<std::pair<int, int>, std::vector<std::pair<int, int> >, std::greater<std::pair<int, int> > > q;
        for (int i = 1; i <= n; ++i) {
            q.emplace(a[i].first, i);
            pre[i] = i - 1, nex[i] = i + 1;
        }
        nex[0] = 1, pre[n + 1] = n;
        auto del = [&](int i) {
            tag[i] = 1;
            pre[nex[i]] = pre[i], nex[pre[i]] = nex[i];
            return;
        };
        auto res(0ll);
        for (; (int)q.size() > 1;) {
            auto [x, id] = q.top();
            q.pop();
            if (tag[id] || a[id].first != x)
                continue;
            if (a[id].second == 1) {
                if (!pre[id] || (nex[id] != n + 1 && a[nex[id]].first <= a[pre[id]].first))
                    res += a[nex[id]].first - x, a[id].first = a[nex[id]].first;
                else
                    res += a[pre[id]].first - x, a[id].first = a[pre[id]].first;
            }
            else {
                if (a[id].second & 1)
                    ++res, ++a[id].second;
                ++a[id].first, a[id].second /= 2;
            }
            if (pre[id] && a[id].first == a[pre[id]].first)
                a[id].second += a[pre[id]].second, del(pre[id]);
            if (nex[id] != n + 1 && a[id].first == a[nex[id]].first)
                a[id].second += a[nex[id]].second, del(nex[id]);
            q.emplace(a[id].first, id);
        }
        {
            auto [x, id] = q.top();
            for ( ; a[id].second != 1; ) {
                if (a[id].second & 1)
                    ++res, ++a[id].second;
                ++a[id].first, a[id].second /= 2;
            }
        }
        std::cout << res << '\n';
    }
    return 0;
}

言论