杂题
2025-10-19

模拟赛 and so on


CF1923F Shrink-Reverse

https://www.luogu.com.cn/problem/CF1923F

需要想到贪心策略,但正确的策略并没有那么显然。

  • 观察样例可以大胆猜测,SHRINK_REVERSE 只会用最多一次。尝试理解这一点:

    • 只考虑 SHRINK,那么发现只有最后一次 SHRINK 是有效的。
    • 只考虑 REVERSE,那么只需要将两次 REVERSE 中间的 SWAP 进行翻转,并删除这两次 REVERSE,就可以达到相同的作用。

    综上,只需要保留最后一次 SHRINK_REVERSE,剩下的可以通过翻转对应的 SWAP 操作达到相同的效果,节省操作次数。且显然这次 SHRINK_REVERSE 应该放在操作序列的末尾。

  • 因此,只需要讨论不 SHRINK_REVERSE、在最后 SHRINK_REVERSE 两种情况。

    对于不 SHRINK_REVERSE 的情况,直接贪心即可。

  • 对于最后再 SHRINK_REVERSE 的情况,不妨先 REVERSE,考虑找到 SHRINK 后对应的原始字符串上的区间。

    也即考虑找到一个最短的子串,使得能够通过 \(k-1\)SWAP 把子串外的 \(1\) 全部换到子串内。

  • 问题具有一定的单调性,故可以双指针对于每个 \(l\) 找到最小的 \(r\)。接下来仍然可以贪心最小化答案。

    很容易说明区间长最短的才能竞争最优:要么 \(l\) 初始为 \(1\),要么区间长就是 \(1\) 总数;

    很容易说明相同长度的参与竞争的区间,字典序最小的最优。

  • 故接下来需要比较这 \(O(n)\) 个长度相同的答案区间的字典序。SA 即可。

#include <bits/stdc++.h>
const int mod = 1e9 + 7;
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);
    const auto stime = std::chrono::steady_clock::now();
#endif
    int n, k;
    std::string s, res1, res2;
    std::cin >> n >> k >> s, s = '#' + s;
    res1 = s;
    for (int i = 1, j = n, l = 1; l <= k && i < j; ++i, --j, ++l) {
        for (; i < j && res1[i] != '1'; ++i);
        for (; i < j && res1[j] != '0'; --j);
        if (i < j)
            std::swap(res1[i], res1[j]);
    }
    for (int i = 1; i <= n; ++i)
        if (res1[i] == '1') {
            res1 = '#' + res1.substr(i, n - i + 1);
            break;
        }
    std::reverse(s.begin() + 1, s.end());
    std::vector<int> pos;
    const int s1 = std::count(s.begin() + 1, s.end(), '1');
    int len = n;
    for (int l = 1, r = 0, c1 = 0, c0 = 0; l <= n; ++l) {
        for (; r < n && (c0 < s1 - c1 || s1 - c1 > k - 1); )
            if (s[++r] == '1') ++c1;
            else ++c0;
        if (c0 >= s1 - c1 && s1 - c1 <= k - 1) {
            if (r - l + 1 < len)
                len = r - l + 1, pos.clear();
            if (r - l + 1 == len)
                pos.push_back(l);
        }
        if (s[l] == '1') --c1;
        else --c0;
    }
    std::vector<int> sa(n + 1), rk(2 * n + 1);
    {
        int m = 128;
        std::vector<int> c(std::max(n, m) + 1), id, la(2 * n + 1);
        std::copy(s.begin() + 1, s.end(), rk.begin() + 1);
        for (int i = 1; i <= n; ++i)
            ++c[rk[i]];
        std::partial_sum(c.begin() + 1, c.begin() + m + 1, c.begin() + 1);
        for (int i = n; i; --i)
            sa[c[rk[i]]--] = i;
        for (int w = 1, p; ; w <<= 1, m = p) {
            id.clear(), id.push_back(-1);
            for (int i = n - w + 1; i <= n; ++i)
                id.push_back(i);
            for (int i = 1; i <= n; ++i)
                if (sa[i] > w)
                    id.push_back(sa[i] - w);
            std::fill(c.begin() + 1, c.begin() + m + 1, 0);
            for (int i = 1; i <= n; ++i)
                ++c[rk[i]];
            std::partial_sum(c.begin() + 1, c.begin() + m + 1, c.begin() + 1);
            for (int i = n; i; --i)
                sa[c[rk[id[i]]]--] = id[i];
            p = 0;
            std::copy(rk.begin() + 1, rk.end(), la.begin() + 1);
            for (int i = 1; i <= n; ++i)
                if (i != 1 && la[sa[i]] == la[sa[i - 1]] && la[sa[i] + w] == la[sa[i - 1] + w])
                    rk[sa[i]] = p;
                else
                    rk[sa[i]] = ++p;
            if (p == n)
                break;
        }
    }
    int id = 0;
    for (auto i : pos)
        if (id == 0 || rk[i] < rk[id])
            id = i;
    if (id == 0)
        assert(len == n), id = 1;
    res2 = '#' + s.substr(id, len);
    int tot = s1 - std::count(res2.begin() + 1, res2.end(), '1');
    for (int i = len; tot--; --i) {
        for (; res2[i] != '0'; --i);
        res2[i] = '1';
    }
    if (res1.length() == res2.length())
        s = std::min(res1, res2);
    else if (res1.length() < res2.length())
        s = res1;
    else
        s = res2;
    auto res = 0ll;
    for (int i = 1; i < (int)s.size(); ++i)
        res = (res * 2 + s[i] - '0') % mod;
    std::cout << res << '\n';
#ifndef ONLINE_JUDGE
    std::cerr << std::fixed << std::setprecision(6) << std::chrono::duration<double> (std::chrono::steady_clock::now() - stime).count() << "s\n";
#endif
    return 0;
}

CF1859F Teleportation in Byteland

https://www.luogu.com.cn/problem/CF1859F

  • 题设让树自然呈现一个分层图的结构。贪心地,最优的路径一定为 \(a_i\to u\),(在 \(u\) 学习若干次),\(u\to b_i\)

    枚举一个学习次数 \(k\)(显然 \(k\) 不会超过 \(\left\lceil\log_2n\right\rceil\)),并令 \(d_k(u,v)\) 表示在学习 \(k\) 次后的分层图上 \(u\)\(v\) 的距离。最后的答案可以表示为 \(\min\limits_{u,k}\{d_0(a_i,u)+d_k(u,b_i)+k\cdot T\}\)

  • 现在希望能够快速地找到,对于一个固定的 \(k\),使答案最小的 \(u\)。询问与路径相关,所以期望一种能和路径挂钩的答案表示方法(似乎是经典 trick)。容易想到链 \((a_i,b_i)\) 上离 \(u\) 最近的点 \(v\),答案可以改写成:

    \[ \min\limits_{u,v}\{d_0(a_i,v)+d_k(v,b_i)+d_0(v,u)+d_k(u,v)\}+k\cdot T \]

    其中,\(d_0(v,u)+d_k(u,v)\) 与具体的 \(a,b\) 无关,可以用一次多源 Dij 预处理出每个 \(v\) 的答案(记为 \(s_k(v)\))(需要注意到 \(d_0(v,u)\)\(d_k(u,v)\) 可能不同时取最小,可以用 \(w_0+w_k\) 作边权);\(d_0(a_i,v)+d_k(v,b_i)\) 则可以通过讨论 LCA 直接用深度表示,求出最值。

  • \(fa=\text{LCA}(a_i,b_i)\),当 \(v\)\(fa\to a_i\) 上时,答案为:

    \[ (dep_k(v)+s_k(v)-dep_0(v))+(dep_k(b_i)-2dep_k(fa)+dep_0(a_i)+k\cdot T) \]

    \(v\)\(fa\to b_i\) 上时,答案为:

    \[ (dep_0(v)+s_k(v)-dep_k(v))+(dep_0(a_i)-2dep_0(fa)+dep_k(b_i)+k\cdot T) \]

    维护链上 \(dep_k(v)+s_k(v)-dep_0(v)\)\(dep_0(v)+s_k(v)-dep_k(v)\) 的最小值即可。

  • 由于是完全静态的树上路径问题,可以用倍增完成。注意考虑学 \(0\) 次的情况。复杂度 \(O(n\log^2 n)\)
  • 咦你怎么 MLE 了?只需要把所有询问离线下来,每个 \(k\) 就可以共用数组了。

    看到 Limury 的做法是树剖 + st 表,薄纱所有人。Rosmist 树剖 + bit 三只 log 也薄纱倍增做法,我怎么会做这样的梦。

#include <bits/stdc++.h>
const int maxn = 1e5 + 5;
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);
    const auto stime = std::chrono::steady_clock::now();
#endif
    int T;
    for (std::cin >> T; T--; ) {
        int n, t;
        std::cin >> n >> t;
        std::vector<std::vector<std::pair<int, int> > > g(n + 1);
        for (int i = 1, x, y, w; i < n; ++i) {
            std::cin >> x >> y >> w;
            g[x].emplace_back(y, w), g[y].emplace_back(x, w);
        }
        std::vector<int> tag(n + 1);
        for (int i = 1; i <= n; ++i) {
            char t;
            std::cin >> t, tag[i] = t - '0';
        }
        auto fun = [&](int w, int k) {
            return (w + (1 << k) - 1) >> k;
        };
        std::vector<long long> s(n + 1);
        std::vector<int> dep(n + 1), vis(n + 1);
        std::vector<std::array<int, 17> > fa(n + 1);
        std::vector<std::array<long long, 17> > f1(n + 1), f2(n + 1);
        std::vector<std::vector<long long> > dis(2, std::vector<long long> (n + 1));
        std::function<void(int)> DFS = [&](int x) {
            for (auto [i, w] : g[x])
                if (i != fa[x][0]) {
                    dep[i] = dep[x] + 1;
                    dis[0][i] = dis[0][x] + w;
                    fa[i][0] = x;
                    for (int j = 1; j < 17; ++j)
                        fa[i][j] = fa[fa[i][j - 1]][j - 1];
                    DFS(i);
                }
            return;
        };
        dep[1] = 1, DFS(1);
        auto getLCA = [&](int x, int y) {
            if (dep[x] < dep[y])
                std::swap(x, y);
            for (int i = 16; ~i; --i)
                if (dep[fa[x][i]] >= dep[y])
                    x = fa[x][i];
            if (x == y)
                return x;
            for (int i = 16; ~i; --i)
                if (fa[x][i] != fa[y][i])
                    x = fa[x][i], y = fa[y][i];
            return fa[x][0];
        };
        int m;
        std::cin >> m;
        std::vector<long long> res(m + 1);
        std::vector<std::pair<int, int> > q(m + 1);
        for (int i = 1, u, v; i <= m; ++i) {
            std::cin >> u >> v;
            res[i] = dis[0][u] + dis[0][v] - 2 * dis[0][getLCA(u, v)];;
            q[i] = { u, v };
        }
        for (int j = 0; j < 17; ++j)
            f1[0][j] = f2[0][j] = 1e18;
        for (int k = 1; k <= 20; ++k) {
            {
                std::priority_queue<std::pair<long long, int> > q;
                for (int i = 1; i <= n; ++i) {
                    vis[i] = 0;
                    if (tag[i])
                        s[i] = 0ll, q.emplace(0ll, i);
                    else
                        s[i] = 1e18;
                }
                for (; !q.empty(); ) {
                    int x = q.top().second;
                    q.pop();
                    if (vis[x])
                        continue;
                    vis[x] = 1;
                    for (auto [v, w] : g[x])
                        if (s[v] > s[x] + fun(w, k) + w) {
                            s[v] = s[x] + fun(w, k) + w;
                            q.emplace(-s[v], v);
                        }
                }
            }
            DFS = [&](int x) {
                for (auto [i, w] : g[x])
                    if (i != fa[x][0]) {
                        dis[1][i] = dis[1][x] + fun(w, k);
                        f1[i][0] = dis[1][i] + s[i] - dis[0][i];
                        f2[i][0] = dis[0][i] + s[i] - dis[1][i];
                        for (int j = 1; j < 17; ++j) {
                            f1[i][j] = std::min(f1[i][j - 1], f1[fa[i][j - 1]][j - 1]);
                            f2[i][j] = std::min(f2[i][j - 1], f2[fa[i][j - 1]][j - 1]);
                        }
                        DFS(i);
                    }
                return;
            };
            DFS(1);
            for (int i = 1; i <= m; ++i) {
                auto [u, v] = q[i];
                int x = u, y = v;
                long long s1 = 1e18, s2 = 1e18;
                if (dep[x] > dep[y]) {
                    for (int i = 16; ~i; --i)
                        if (dep[fa[x][i]] >= dep[y]) {
                            s1 = std::min(s1, f1[x][i]);
                            x = fa[x][i];
                        }
                }
                else {
                    for (int i = 16; ~i; --i)
                        if (dep[fa[y][i]] >= dep[x]) {
                            s2 = std::min(s2, f2[y][i]);
                            y = fa[y][i];
                        }
                }
                if (x != y) {
                    for (int i = 16; ~i; --i)
                        if (fa[x][i] != fa[y][i]) {
                            s1 = std::min(s1, f1[x][i]);
                            s2 = std::min(s2, f2[y][i]);
                            x = fa[x][i], y = fa[y][i];
                        }
                    s1 = std::min(s1, f1[x][0]);
                    s2 = std::min(s2, f2[y][0]);
                    x = fa[x][0];
                }
                s1 = std::min(s1, dis[1][x] + s[x] - dis[0][x]);
                s2 = std::min(s2, dis[0][x] + s[x] - dis[1][x]);
                res[i] = std::min(res[i], k * t + dis[0][u] + dis[1][v] + std::min(s1 - 2 * dis[1][x], s2 - 2 * dis[0][x]));
            }
        }
        for (int i = 1; i <= m; ++i)
            std::cout << res[i] << '\n';
    }
#ifndef ONLINE_JUDGE
    std::cerr << std::fixed << std::setprecision(6) << std::chrono::duration<double> (std::chrono::steady_clock::now() - stime).count() << "s\n";
#endif
    return 0;
}

CF1556G Gates to Another World

https://www.luogu.com.cn/problem/CF1556G

  • 快速做出来需要一定运气,需要把点顺序排布而不是像题目一样画晶胞(。)手玩一下 \(n=4\) 时的情况:

    看到两个相邻点间连的边就应激想到线段树了,事实上把不同层的边提起来发现就是线段树的结构。

    为什么这个图这么大(没啥用故折起来了)

    tip:实际上那些相邻 / 相反的尖尖是重合的,为了好看一点故错开。

    发现形成一个相邻子树对位连边的结构。这里线段树其实只有底层点是有意义的,其他点只起结构辅助作用。

    实际上,对于两个有连边的点,它们『LCA』的高度是被 flip 的位。

  • 很容易注意到一个线段树子树内的底层点是连通的,这一点也很容易递归地证明。

    考虑动态开点线段树优化建图,对于动态开点出来的叶子(注意不是底层点),容易发现在题目限制下每个叶子内部都没有点被删除,是连通的(那么此时不妨认为叶子就代表了这个连通块)。

    故将某个点进行连边时,并不要求找到对位的那个点(不一定存在),和走到头的叶子连边就可以达到相同的连通效果。这样就把底层点之间的连边转化为了叶子间的连边。

  • 不妨先时光倒流 + 预处理出每个叶子最后(最早)存在的时间戳,考虑暴力连出来这些叶子间的边(对于每个点,将对位的叶子连边)并计算出边最后(最早)存在的时间戳。

    发现暴力对位连边这个操作,是形似线段树合并但比线段树合并更劣的东西。咋分析复杂度。

    边是双向的,考虑更深的叶子的贡献。那么其枚举 lca + flip 对应位(不存在则取路径上的叶子),一共会有 \(O(n)\) 次连边。共有 \(O(nm)\) 个叶子,摊下来暴力连边复杂度为 \(O(n^2m)\)

空间很卡…… 不要把离线的加边数组开成 long long 了。

#include <bits/stdc++.h>
const int maxn = 32e5 + 5;
using ll = long long;
struct { int l, r, d; } t[maxn];
int tot;
#define lt t[p].l
#define rt t[p].r
#define leaf(p) (!t[p].l && !t[p].r)
int newnode(void) {
    ++tot, t[tot].l = t[tot].r = t[tot].d = 0;
    return tot;
}
void pushdown(int p) {
    if (t[p].d) {
        if (!lt)
            lt = newnode();
        if (!rt)
            rt = newnode();
        t[lt].d = t[rt].d = t[p].d;
        t[p].d = 0;
    }
    return;
}
void upd(int &p, ll l, ll r, ll ql, ll qr, int v) {
    if (!p)
        p = newnode();
    if (ql <= l && r <= qr) {
        t[p].d = v;
        return;
    }
    pushdown(p);
    auto mid = (l + r) >> 1;
    if (ql <= mid)
        upd(lt, l, mid, ql, qr, v);
    if (qr > mid)
        upd(rt, mid + 1, r, ql, qr, v);
    return;
}
int ask(int p, ll l, ll r, ll x) {
    if (leaf(p))
        return p;
    ll mid = (l + r) >> 1;
    if (x <= mid)
        return ask(lt, l, mid, x);
    return ask(rt, mid + 1, r, x);
}
#undef lt
#undef rt
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);
    const auto stime = std::chrono::steady_clock::now();
#endif
    int n, m, rt = newnode();
    std::cin >> n >> m;
    t[rt].d = m + 1;
    const auto N = (1ll << n) - 1;
    struct query { int op; ll s, t; };
    std::vector<query> q(m + 1);
    for (int i = 1; i <= m; ++i) {
        std::string op;
        std::cin >> op;
        q[i].op = (op == "ask") + 1;
        std::cin >> q[i].s >> q[i].t;
        if (q[i].op == 1)
            upd(rt, 0ll, N, q[i].s, q[i].t, i);
    }
    std::vector<int> f(tot + 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) {
        f[find(x)] = find(y);
        return;
    };
    std::vector<std::vector<std::pair<int, int> > > u(m + 2);
    std::function<void(int, int)> link = [&](int p, int q) {
        if (leaf(p) && leaf(q)) {
            u[std::min(t[p].d, t[q].d)].emplace_back(p, q);
            return;
        }
        else if (leaf(p))
            link(p, t[q].l), link(p, t[q].r);
        else if (leaf(q))
            link(t[p].l, q), link(t[p].r, q);
        else
            link(t[p].l, t[q].l), link(t[p].r, t[q].r);
        return;
    };
    for (int i = 1; i <= tot; ++i)
        if (t[i].l && t[i].r)
            link(t[i].l, t[i].r);
    for (auto [p, q] : u[m + 1])
        merge(p, q);
    std::vector<int> res;
    for (int i = m; i; --i)
        if (q[i].op == 2)
            res.push_back(find(ask(rt, 0, N, q[i].s)) == find(ask(rt, 0, N, q[i].t)));
        else
            for (auto [p, q] : u[i])
                merge(p, q);
    for (; !res.empty(); res.pop_back())
        std::cout << res.back() << '\n';
#ifndef ONLINE_JUDGE
    std::cerr << std::fixed << std::setprecision(6) << std::chrono::duration<double> (std::chrono::steady_clock::now() - stime).count() << "s\n";
#endif
    return 0;
}

C. 小 Z 爱优化

https://www.becoder.com.cn/contest/6662/problem/3

给定 \(a_{1\sim n}\),将其分为若干连续段,满足每段长度为 \(1\)\(2\),定义一段的权值为元素之和,最小化段权值的极差并输出之。

\(n\le 2\times 10^5,V=10^9\)

  • \(f_{i,j}\) 表示这一段以 \(i\) 结尾,最小值为 \(j\) 时的最大值。把最小值当状态是自然的想法,实际似乎差不多(。)得到(不重要):

    \[ f_{i,a_i}\gets f_{i-1,j},j\ge a_i\\ f_{i,a_i+a_{i-1}}\gets f_{i-2,j},j\ge a_i+a_{i-1}\\ f_{i,j}\gets \max(f_{i-1,j},a_i) ,j\le a_i\\ f_{i,j}\gets \max(f_{i-2,j},a_i+a_{i-1}) ,j\le a_i \]

  • 这个状态很矩阵,可以考虑下标分奇偶性继承,用线段树优化,矩阵作为元素;然而同时涉及两个维度,尤其是第一维的变化,实在是答辩啊!

    By the way,广义矩乘的结合律?

    对于一个 \(\oplus-\otimes\) 矩阵(内层为 \(\otimes\),外层为 \(\oplus\)),其有结合律当且仅当同时满足:

    1. \(\oplus\) 有交换律;
    2. \(\otimes\) 有结合律;
    3. \(\otimes\)\(\oplus\) 有分配律。
    来自 qkhm 关于广义矩阵乘法常见误区的一点纠正
    来自 qkhm 关于广义矩阵乘法常见误区的一点纠正

    发现第二维其实不会怎么变,或者说第二维其实起的是一个限制性的作用,可以考虑把第二维放到外层循环,相当于枚举限制。我们知道上面的做法可以很快地优化,所以拿出去了之后大概也能做。

  • 若限制当前最小值不能小于 \(j\),那么有:

    \[ f_i\gets \max(f_{i-1},a_i),a_j\ge j\\ f_i\gets \max(f_{i-2},a_i+a_{i-1}),a_i+a_{i-1}\ge j \]

    发现这就很显然是一个 DDP 的形式了,每次 \(j\) 改变时,只需要改动对应位置的矩阵(即是否能进行转移),总修改次数是 \(O(n)\) 的。

#include <bits/stdc++.h>
const int maxn = 2e5 + 5;
const int inf = 0x7fffffff;
using mat = std::array<std::array<int, 2>, 2>;
int m[maxn][2][2];
void mul(mat &res, mat &p, mat &q) {
    res[0][0] = std::min(std::max(p[0][0], q[0][0]), std::max(p[0][1], q[1][0]));
    res[0][1] = std::min(std::max(p[0][0], q[0][1]), std::max(p[0][1], q[1][1]));
    res[1][0] = std::min(std::max(p[1][0], q[0][0]), std::max(p[1][1], q[1][0]));
    res[1][1] = std::min(std::max(p[1][0], q[0][1]), std::max(p[1][1], q[1][1]));
    return;
}
struct {
    int l, r;
    mat u;
} t[maxn << 2];
#define lt (p << 1)
#define rt (lt | 1)
void bld(int p, int l, int r) {
    t[p].l = l, t[p].r = r;
    if (l == r) {
        t[p].u[0][0] = m[l][0][0];
        t[p].u[0][1] = m[l][0][1];
        t[p].u[1][0] = m[l][1][0];
        t[p].u[1][1] = m[l][1][1];
        return;
    }
    int mid = (l + r) >> 1;
    bld(lt, l, mid), bld(rt, mid + 1, r);
    mul(t[p].u, t[lt].u, t[rt].u);
    return;
}
void upd(int p, int x) {
    if (t[p].l == t[p].r) {
        t[p].u[0][1] = m[x][0][1];
        t[p].u[1][1] = m[x][1][1];
        return;
    }
    int mid = (t[p].l + t[p].r) >> 1;
    if (x <= mid)
        upd(lt, x);
    else
        upd(rt, x);
    mul(t[p].u, t[lt].u, t[rt].u);
    return;
}
#undef lt
#undef rt
int main() {
#ifdef ONLINE_JUDGE
    std::ios::sync_with_stdio(false);
    std::cin.tie(nullptr), std::cout.tie(nullptr);
    std::freopen("opti.in", "r", stdin);
    std::freopen("opti.out", "w", stdout);
#else
    std::freopen("./test/20251021/opti/1.in", "r", stdin);
    std::freopen(".out", "w", stdout);
    auto stime = std::chrono::steady_clock::now();
#endif
    int T;
    for (std::cin >> T; T--; ) {
        int n;
        std::cin >> n;
        std::vector<int> a(n + 1), b(n + 1), l(1);
        for (int i = 1; i <= n; ++i) {
            std::cin >> a[i];
            l.push_back(a[i]);
            if (i > 1)
                b[i] = a[i] + a[i - 1], l.push_back(b[i]);
        }
        std::sort(l.begin() + 1, l.end());
        l.erase(std::unique(l.begin() + 1, l.end()), l.end());
        int m = (int)l.size() - 1;
        std::vector<std::vector<int> > ida(m + 1), idb(m + 1);
        for (int i = 1; i <= n; ++i) {
            a[i] = std::lower_bound(l.begin() + 1, l.end(), a[i]) - l.begin();
            ::m[i][1][1] = a[i], ida[a[i]].push_back(i);
            if (i > 1) {
                b[i] = std::lower_bound(l.begin() + 1, l.end(), b[i]) - l.begin();
                ::m[i][0][1] = b[i], idb[b[i]].push_back(i);
            }
            else
                ::m[i][0][1] = inf;
            ::m[i][0][0] = inf, ::m[i][1][0] = 0;
        }
        bld(1, 1, n);
        auto res = inf;
        for (int i = 1; i <= m; ++i) {
            if (std::min(t[1].u[0][1], t[1].u[1][1]) != inf)
                res = std::min(res, l[std::min(t[1].u[0][1], t[1].u[1][1])] - l[i]);
            for (auto j : ida[i])
                ::m[j][1][1] = inf, upd(1, j);
            for (auto j : idb[i])
                ::m[j][0][1] = inf, upd(1, j);
        }
        std::cout << res << '\n';
    }
#ifndef ONLINE_JUDGE
    std::cerr << std::fixed << std::setprecision(6) << std::chrono::duration<double>(std::chrono::steady_clock::now() - stime).count() << "s \n";
#endif
    return 0;
}

D. 小 Z 爱考试

https://www.becoder.com.cn/contest/6662/problem/4

给定一个大小为 \(n\) 的内向基环树森林,每个点 \(u\) 上附有点权 \(a_u\)、收益 \(w_u\) 和一个进程,内容为:

  • \(u\) 指向的结点 \(fa\) 满足 \(a_{fa}>a_u\),则令 \(a_u\gets a_u+w_u\)

给定 \(m\) 次操作,形如:

  • 单点修改 \(a_u,w_u\)
  • 给定一个 \(u\),输出:在 \(n!\) 种进程的执行序列中,\(a_u\) 最终值的期望。

\(n,m\le 2\times 10^5\)保证每个点的入度 \(\le 3\)

  • 可以感受到操作的『后效性』是非常弱的,进一步感受到点可以粗暴地分为三种类型:

    1. \(a_{fa}>a_x\) 的;
    2. \(a_{fa}+w_{fa} \le a_x\) 的;
    3. \(a_{fa}\le a_x<a_{fa} + w_{fa}\) 的;

    点的类型是无依赖,可直接求的。显然,若 \(u\) 最终想取到 \(a_u+w_u\),相当于走一条 \(u\) 开头,经过若干 3 类点,在 1 类点结尾的路径(这显然是确定的),假设路径包含 \(len\) 个点,答案为 \(\dfrac {(n-len)!\cdot C_{n}^{len}}{n!}=\dfrac 1{len!}\)

    换句话说,\(u\) 取不到 \(a_u+w_u\),当且仅当路径上在出现 1 类点前出现了 2 类点或 3 类点的环(基环)。

  • 考虑静态的情况,先处理所有基环:若只存在 3 类点,则标记全部非法;否则,任选一个 1 类点或者 2 类点开头,就可以 DP 出环上的 \(len\)

    对于所有树,直接大力 DP 即可。

  • 接着考虑带修的情况,修改相当于改变点及其后继的状态(原来入度限制是拿来做这个的)。

    发现没啥办法很好地做这个类 DDP 的问题。考虑到一场比赛不应该出现两道 DDP,故如果灵感迸发可能会想到干脆放弃预处理的想法,上重链,线段树维护区间 1 类点或 2 类点深度 max,即可做到两只 log。

    如果发现树上没有这样的点,在环上也可以类似地找到第一个 1 类点或 2 类点。

感觉写复杂了,瞻仰了一下别人的代码又感觉流程上没什么区别

喜提最长代码,喜提最优解

#include <bits/stdc++.h>
const int mod = 1e9 + 7;
const int maxn = 6e5 + 5;
struct {
    int l, r, lc, rc, u1, u2;
} t[maxn << 2];
#define lt t[p].lc
#define rt t[p].rc
int tot;
int ty[maxn], dep[maxn];
void bld(int &p, int l, int r) {
    p = ++tot;
    t[p].l = l, t[p].r = r;
    if (l == r) {
        t[p].u1 = t[p].u2 = 0;
        if (ty[l] == 1)
            t[p].u1 = dep[l];
        else if (ty[l] == 2)
            t[p].u2 = dep[l];
        return;
    }
    int mid = (l + r) >> 1;
    bld(lt, l, mid), bld(rt, mid + 1, r);
    t[p].u1 = std::max(t[lt].u1, t[rt].u1);
    t[p].u2 = std::max(t[lt].u2, t[rt].u2);
    return;
}
void upd(int p, int x) {
    if (t[p].l == t[p].r) {
        t[p].u1 = t[p].u2 = 0;
        if (ty[x] == 1)
            t[p].u1 = dep[x];
        else if (ty[x] == 2)
            t[p].u2 = dep[x];
        return;
    }
    int mid = (t[p].l + t[p].r) >> 1;
    if (x <= mid)
        upd(lt, x);
    else
        upd(rt, x);
    t[p].u1 = std::max(t[lt].u1, t[rt].u1);
    t[p].u2 = std::max(t[lt].u2, t[rt].u2);
    return;
}
std::pair<int, int> ask(int p, int l, int r) {
    if (l <= t[p].l && t[p].r <= r)
        return std::make_pair(t[p].u1, t[p].u2);
    int mid = (t[p].l + t[p].r) >> 1;
    std::pair<int, int> res(0, 0);
    if (l <= mid)
        res = ask(lt, l, r);
    if (r > mid) {
        auto s(ask(rt, l, r));
        res.first = std::max(res.first, s.first);
        res.second = std::max(res.second, s.second);
    }
    return res;
}
#undef lt
#undef rt
int main() {
#ifdef ONLINE_JUDGE
    std::ios::sync_with_stdio(false);
    std::cin.tie(nullptr), std::cout.tie(nullptr);
    std::freopen("exam.out", "w", stdout);
    std::freopen("exam.in", "r", stdin);
#else
    std::freopen("./test/20251021/exam/3.in", "r", stdin);
    std::freopen(".out", "w", stdout);
    const auto stime = std::chrono::steady_clock::now();
#endif
    int T;
    for (std::cin >> T; T--; ) {
        int n, m;
        std::cin >> n >> m;
        std::vector<int> f(n + 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) {
            f[find(x)] = find(y);
            return;
        };
        std::vector<long long> fac(n + 1), inv(n + 1);
        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;
        std::vector<std::vector<int> > g(n + 1);
        std::vector<int> a(n + 1), w(n + 1), to(n + 1), col(2 * n + 1);
        for (int i = 1; i <= n; ++i) {
            std::cin >> a[i] >> to[i] >> w[i];
            merge(i, to[i]);
            g[to[i]].push_back(i);
        }
        int now = 0, tot = n;
        std::vector<int> tab(n + 1), tad(n + 1), vis(n + 1), L(2 * n + 1), R(2 * n + 1);
        auto getty = [&](int x) {
            if (a[to[x]] > a[x])
                return 1;
            if (a[to[x]] + w[to[x]] <= a[x])
                return 2;
            return 3;
        };
        for (int i = 1; i <= n; ++i)
            if (f[i] == i) {
                std::vector<int> vis(n + 1), t(1);
                std::stack<int> st;
                for (int x = i; ; ) {
                    st.push(x);
                    if (!vis[to[x]])
                        vis[to[x]] = 1, x = to[x];
                    else {
                        for (;;) {
                            t.push_back(st.top());
                            if (st.top() == to[x])
                                break;
                            st.pop();
                        }
                        break;
                    }
                }
                int k = (int)t.size() - 1;
                std::reverse(t.begin() + 1, t.end());
                t.resize(2 * k + 1);
                L[++tot] = now + 1;
                for (int i = 1; i <= k; ++i) {
                    tab[t[i]] = ++now;
                    col[t[i]] = tot;
                    ::dep[tab[t[i]]] = 2 * k - i + 1;
                    ::ty[tab[t[i]]] = getty(t[i]);
                }
                for (int i = k + 1; i <= 2 * k; ++i) {
                    t[i] = t[i - k];
                    tad[t[i]] = ++now;
                    ::dep[tad[t[i]]] = 2 * k - i + 1;
                    ::ty[tad[t[i]]] = getty(t[i]);
                }
                R[tot] = now;
            }
        std::vector<int> fa(n + 1), siz(n + 1), son(n + 1), dep(n + 1);
        std::function<void(int)> DFS = [&](int x) {
            siz[x] = 1;
            for (auto i : g[x])
                if (!tab[i]) {
                    dep[i] = dep[x] + 1;
                    fa[i] = x, DFS(i);
                    siz[x] += siz[i];
                    if (siz[i] > siz[son[x]])
                        son[x] = i;
                }
            return;
        };
        for (int i = 1; i <= n; ++i)
            if (tab[i])
                dep[i] = 1, DFS(i);
        std::vector<int> rt(2 * n + 1), dfn(n + 1), top(n + 1), bot(n + 1);
        DFS = [&](int x) {
            dfn[x] = ++now;
            ty[dfn[x]] = getty(x);
            ::dep[dfn[x]] = dep[x];
            bot[top[x]] = x;
            if (son[x])
                top[son[x]] = top[x], DFS(son[x]);
            for (auto i : g[x])
                if (i != son[x] && !tab[i])
                    top[i] = i, DFS(i);
            if (x == top[x])
                bld(rt[x], dfn[x], dfn[bot[x]]);
            return;
        };
        for (int i = 1; i <= n; ++i)
            if (tab[i])
                top[i] = i, DFS(i);
        for (int i = n + 1; i <= tot; ++i)
            bld(rt[i], L[i], R[i]);
        auto work = [&](int x) {
            ty[dfn[x]] = getty(x);
            upd(rt[top[x]], dfn[x]);
            if (tab[x]) {
                ty[tab[x]] = ty[tad[x]] = getty(x);
                upd(rt[col[x]], tab[x]), upd(rt[col[x]], tad[x]);
            }
            return;
        };
        for (int op; m--; ) {
            std::cin >> op;
            if (op == 1) {
                int x;
                std::cin >> x, std::cin >> a[x];
                work(x);
                for (auto i : g[x])
                    work(i);
            }
            else if (op == 2) {
                int x;
                std::cin >> x, std::cin >> w[x];
                work(x);
                for (auto i : g[x])
                    work(i);
            }
            else {
                int x;
                std::cin >> x;
                auto calc = [&](int x) {
                    int len = 0, toq = 0;
                    for (; x; x = fa[top[x]]) {
                        auto [u1, u2] = ask(rt[top[x]], dfn[top[x]], dfn[x]);
                        if (u1 > u2)
                            return len + dep[x] - u1 + 1;
                        if (u2 > u1)
                            return -1;
                        len += dep[x] - dep[fa[top[x]]], toq = top[x];
                    }
                    auto [u1, u2] = ask(rt[col[toq]], tab[toq], R[col[toq]]);
                    if (u1 > u2)
                        return len + ::dep[tab[toq]] - u1;
                    return -1;
                };
                auto t(calc(x));
                if (t == -1)
                    std::cout << a[x] << '\n';
                else {
                    // std::cout << "len = " << t << '\n';
                    std::cout << ((a[x] + w[x]) * inv[t] % mod + (1ll + mod - inv[t]) % mod * a[x] % mod) % mod << '\n';
                }
            }
        }
    }
#ifndef ONLINE_JUDGE
    std::cerr << std::fixed << std::setprecision(6) << std::chrono::duration<double> (std::chrono::steady_clock::now() - stime).count() << "s\n";
#endif
    return 0;
}

B. 炒鱿鱼

https://www.becoder.com.cn/contest/6666/problem/2

现在有一个栈。在接下来的 \(n\) 个时刻,每个时刻会发生一次事件,形如:

  • 删除栈顶的 \(a_i\) 个元素,然后向栈顶加入 \(b_i\) 个元素。

现在每个时刻都让一个人去执行事件,需要满足每个元素,加入它的和删除它的不能是同一个人。问最少需要多少个人才能实现这一点。输出方案。有 SPJ。

\(n\le 10^5,V=10^9\)

  • 可以猜到答案只会为 1 2 3,尝试从图染色的角度考虑这个问题(怎么想到这一点?)。
  • 一个元素在哪个时刻被加入 / 删除是固定的,故如果两天有冲突,可以考虑直接连边。考虑模拟,能够线性地求出来所有冲突关系,故边数是 \(O(n)\) 的。
  • 由于这个栈的结构,发现不存在交错的边,或者说不同时存在 \((x_1,y_1)\)\((x_2,y_2)\),使得 \(x_1<x_2<y_1<y_2\)。考虑构造解:

    首先对于 \(1,n\) 任意涂色,忽略 \((1,n)\) 这条边(如果有),显然可以找到一个 \(mid\) 使得没有边跨过之,给其赋一个合法的颜色,接着就可以分治构造。

    感觉这种构造不算太显然,只能算是能想到(实际上并没有)。

    具体分治,由于性质比较好,直接取左端点对应范围内最远的边即可。

  • 需要注意上述构造方案能保证可以构造出 \(k=3\) 的解,但不能保证是最优的(即不能保证 \(k=1\)\(k=2\) 也能构造出对应的解),故需要特判没有边 + 二分图两种情况。

#include <bits/stdc++.h>
int main() {
#ifdef ONLINE_JUDGE
    std::ios::sync_with_stdio(false);
    std::cin.tie(nullptr), std::cout.tie(nullptr);
    std::freopen("fire.in", "r", stdin);
    std::freopen("fire.out", "w", stdout);
#else
    // std::freopen("./test/20251022/fire/fire2.in", "r", stdin);
    std::freopen(".in", "r", stdin);
    std::freopen(".out", "w", stdout);
    const auto stime = std::chrono::steady_clock::now();
#endif
    int n;
    std::cin >> n;
    std::vector<std::pair<int, int> > a(n + 1);
    for (int i = 1; i <= n; ++i)
        std::cin >> a[i].first >> a[i].second;
    bool flag = true;
    std::vector<std::vector<int> > g(n + 1), g1(n + 1);
    std::stack<std::pair<int, int> > st;
    for (int i = 1; i <= n; ++i) {
        for (int j = 0; j < a[i].first; ) {
            g[st.top().second].push_back(i);
            g1[st.top().second].push_back(i);
            g1[i].push_back(st.top().second);
            flag = false;
            int to = std::min(st.top().first, a[i].first - j);
            j += to, st.top().first -= to;
            if (st.top().first == 0)
                st.pop();
        }
        if (a[i].second)
            st.emplace(a[i].second, i);
    }
    if (flag == true) {
        std::cout << 1 << '\n';
        for (int i = 1; i <= n; ++i)
            std::cout << 1 << ' ';
        std::cout << '\n';
        return 0;
    }
    std::vector<int> col(n + 1);
    std::function<bool(int, int)> DFS = [&](int x, int now) {
        col[x] = now;
        for (auto i : g1[x])
            if (!col[i]) {
                if (!DFS(i, 3 - now))
                    return false;
            }
            else if (col[i] == now)
                return false;
        return true;
    };
    for (int i = 1; i <= n; ++i)
        if (!col[i] && !DFS(i, 1))
            goto nosol;
    std::cout << 2 << '\n';
    for (int i = 1; i <= n; ++i)
        std::cout << col[i] << ' ';
    std::cout << '\n';
    return 0;
nosol: ;
    col[1] = 1, col[n] = 2;
    std::function<void(int, int)> calc = [&](int l, int r) {
        if (r - l + 1 <= 2)
            return;
        if (!g[l].empty() && g[l].back() == r)
            g[l].pop_back();
        int pos = g[l].empty() ? l + 1 : g[l].back();
        col[pos] = 6 - col[l] - col[r];
        calc(l, pos), calc(pos, r);
        return;
    };
    calc(1, n);
    std::cout << 3 << '\n';
    for (int i = 1; i <= n; ++i)
        std::cout << col[i] << ' ';
    std::cout << '\n';
#ifndef ONLINE_JUDGE
    std::cerr << std::fixed << std::setprecision(6) << std::chrono::duration<double> (std::chrono::steady_clock::now() - stime).count() << "s\n";
#endif
    return 0;
}

C. 适格者

https://www.becoder.com.cn/contest/6666/problem/3

给定长度为 \(2^n\) 的数组 \(a_{0,\cdots,2^n-1}\),维护 \(q\) 次操作:

  • 修改:单点加;
  • 修改:高维前缀加;
  • 修改:高维后缀加;
  • 询问:高维前缀和,对 \(998244353\) 取模;

\(n\le 20,q\le 10^5\)

  • 单点加 + 高维前缀和会反应出 ARC205E 的 trick
  • 考虑怎么做后缀加,会反应过来和 bit 的区间修改 有一致性,修改相当于单点修改差分数组。考虑类似的推导:

    \[ \begin{aligned} \sum_{s\subseteq S}a_s&=\sum_{s\subseteq S} \sum_{i\subseteq s} d_i\\ &=\sum_{i\subseteq S} 2^{|S|-|i|}\times d_i \end{aligned} \]

    故维护 \(2^{-|i|}\cdot d_i\) 即可(原来取模是拿来搞这个的)。类似地可以推导前缀加:

    \[ \begin{aligned} \sum_{s\subseteq S}a_s&=\sum_{s\subseteq S} \sum_{i\supseteq s} d_i\\ &=\sum_{i\subseteq S} 2^{|S \text{ bitand } i|}\times d_i \end{aligned} \]

    发现这个 bitand 不太好搞,不过 bitand 是对位的,所以依然可以套在分块上

    同时,因为问的是前缀和,所以单点的 \(S\text{ bitand } i\) 其实就是 \(i\)。这里有点囿于 fmt 的思维定势了,实际上可以直接认为 \(f_{s_1, s_2}\) 维护的是这个点处的答案,每次更新对 \(f_{s_1,*}\) 的贡献即可。

不知为何很慢

#include <bits/stdc++.h>
const int mod = 998244353;
const int inv2 = (mod + 1) >> 1;
int main() {
#ifdef ONLINE_JUDGE
    std::ios::sync_with_stdio(false);
    std::cin.tie(nullptr), std::cout.tie(nullptr);
    std::freopen("gene.in", "r", stdin);
    std::freopen("gene.out", "w", stdout);
#else
    // std::freopen("./test/20251022/gene/gene5.in", "r", stdin);
    std::freopen(".in", "r", stdin);
    std::freopen(".out", "w", stdout);
    const auto stime = std::chrono::steady_clock::now();
#endif
    int n, q;
    std::cin >> n >> q;
    const int N1 = n / 2, N2 = n - N1, lim = 1 << N2;
    std::vector<long long> inv(n + 1);
    inv[0]= 1ll;
    for (int i = 1; i <= n; ++i)
        inv[i] = inv[i - 1] * inv2 % mod;
    std::vector<std::vector<long long> > s(1 << N1, std::vector<long long> (lim));
    auto s1(s), s2(s);
    for (int op, x; q--; ) {
        std::cin >> op >> x;
        int X1 = x >> N2, X2 = x & (lim - 1);
        if (op == 0) {
            auto res = 0ll, res1 = 0ll, res2 = 0ll;
            for (int i = 0; i < (1 << N1); ++i)
                res2 += (1 << __builtin_popcount(i & X1)) * s2[i][X2] % mod;
            for (int i = X1; ; i = X1 & (i - 1)) {
                res += s[i][X2];
                res1 += s1[i][X2] * (1 << __builtin_popcount(x)) % mod;
                if (i == 0)
                    break;
            }
            std::cout << (res + res1 + res2) % mod << '\n';
        }
        else if (op == 1) {
            int v;
            std::cin >> v;
            int X3 = (lim - 1) ^ X2;
            for (int i = X3; ; i = X3 & (i - 1)) {
                (s[X1][(lim - 1) ^ i] += v) %= mod;
                if (i == 0)
                    break;
            }
        }
        else if (op == 2) {
            long long v;
            std::cin >> v, (v *= inv[__builtin_popcount(x)]) %= mod;
            int X3 = (lim - 1) ^ X2;
            for (int i = X3; ; i = X3 & (i - 1)) {
                (s1[X1][(lim - 1) ^ i] += v) %= mod;
                if (i == 0)
                    break;
            }
        }
        else {
            long long v;
            std::cin >> v;
            for (int i = 0; i < lim; ++i)
                (s2[X1][i] += v * (1 << __builtin_popcount(i & X2))) %= mod;
        }
    }
#ifndef ONLINE_JUDGE
    std::cerr << std::fixed << std::setprecision(6) << std::chrono::duration<double> (std::chrono::steady_clock::now() - stime).count() << "s\n";
#endif
    return 0;
}

CF1784C Monsters (hard version)

https://www.luogu.com.cn/problem/CF1784C

  • 通过若干次 1 操作,把数列的值域变成包含 1 且连续的,然后执行 2 操作,一定不劣

  • 考虑怎么快速维护这个东西,发现只关心初始序列和目标序列(从计算代价的式子可以看出来),故考虑这样一个构造最终序列的方式:

    • 从前到后遍历值域上的每个空位,找到第一个大于之的元素,并放过来,重复执行直到上述步骤不能执行为止。

    直接考虑一次加点带来的影响似乎非常复杂,但如果时光倒流,发现有贪心策略,直接把最后一次操作挪到这个新的空位即可。

  • 电波题!用 set 维护即可。

#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);
    const auto stime = std::chrono::steady_clock::now();
#endif
    int T;
    for (std::cin >> T; T--; ) {
        int n;
        std::cin >> n;
        std::set<std::pair<int, int> > t;
        std::vector<long long> res(n + 1);
        std::vector<int> a(n + 1), to(n + 1), at(n + 1);
        for (int i = 1; i <= n; ++i)
            std::cin >> a[i], t.emplace(a[i], i);
        int now;
        for (now = 1; ; ++now) {
            auto p(t.lower_bound({ now, 0 }));
            if (p == t.end())
                break;
            at[now] = p->second;
            to[p->second] = now;
            res[n] += p->first - now;
            t.erase(p);
        }
        --now;
        for (int i = n - 1; i; --i) {
            res[i] = res[i + 1];
            if (to[i + 1]) {
                res[i] -= a[i + 1] - to[i + 1];
                auto p = t.lower_bound({ to[i + 1], 0 });
                if (p != t.end()) {
                    at[to[i + 1]] = p->second;
                    to[p->second] = to[i + 1];
                    res[i] += a[p->second] - to[i + 1];
                    t.erase(p);
                }
                else {
                    res[i] -= a[at[now]] - to[at[now]];
                    res[i] += a[at[now]] - to[i + 1];
                    to[at[now]] = to[i + 1];
                    at[to[i + 1]] = at[now];
                    --now;
                }
            }
            else
                t.erase({ a[i + 1], i + 1 });
        }
        for (int i = 1; i <= n; ++i)
            std::cout << res[i] << ' ';
        std::cout << '\n';
    }
#ifndef ONLINE_JUDGE
    std::cerr << std::fixed << std::setprecision(6) << std::chrono::duration<double> (std::chrono::steady_clock::now() - stime).count() << "s\n";
#endif
    return 0;
}

CF891C Envy

https://www.luogu.com.cn/problem/CF891C

mobai ddxrS

  • 经典结论:在做完权值 \(<w\) 的 Kruskal 后,权值 \(=w\) 的边以任意顺序加入 Kruskal 后,并查集连通性完全一致。

  • Another 经典结论:一条权值为 \(w\) 的边可以属于 mst,当且仅当在做完权值 \(<w\) 的 Kruskal 后,这条边加入不形成环。

  • 重要事实:把并查集的 merge 操作打乱,连通性不变。

  • 结合起来得到这个神秘做法:按照边权做 Kruskal 检验每条边是否可以加入 mst,如果可以则记录合并的两个根,作为新边(不带权)。

    重要事实:对于 \(n-1\)新边,要么有环,要么形成 mst。

    推广:对于任意条新边,要么有环,要么可以通过某种选取其他新边的方式形成 mst。

    故 check 给定边的新边是否成环即可。算是一个 trick 吗?

#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);
    const auto stime = std::chrono::steady_clock::now();
#endif
    const int M = 5e5;
    int n, m;
    std::cin >> n >> m;
    std::vector<std::pair<int, int> > ne(m + 1);
    std::vector<std::vector<std::tuple<int, int, int> > > e(M + 1); 
    for (int i = 1, x, y, w; i <= m; ++i) {
        std::cin >> x >> y >> w;
        e[w].emplace_back(x, y, i);
    }
    std::vector<int> f(n + 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]);
    };
    for (int i = 1; i <= M; ++i) {
        for (auto [u, v, id] : e[i])
            ne[id] = { find(u), find(v) };
        for (auto [u, v, id] : e[i])
            f[find(u)] = find(v);
    }
    std::iota(f.begin() + 1, f.end(), 1);
    int q;
    for (std::cin >> q; q--; ) {
        int k;
        std::cin >> k;
        std::vector<int> pos;
        bool flag = true;
        for (int id; k--; ) {
            std::cin >> id;
            if (find(ne[id].first) == find(ne[id].second))
                flag = false;
            f[find(ne[id].first)] = find(ne[id].second);
            pos.push_back(ne[id].first);
            pos.push_back(ne[id].second);
        }
        std::cout << (flag ? "YES" : "NO") << '\n';
        for (auto i : pos)
            f[i] = i;
    }
#ifndef ONLINE_JUDGE
    std::cerr << std::fixed << std::setprecision(6) << std::chrono::duration<double> (std::chrono::steady_clock::now() - stime).count() << "s\n";
#endif
    return 0;
}

C. 山月记

https://www.becoder.com.cn/problem/52034

给定一棵大小为 \(n\) 的带权无根树 \(T\),再给定 \(m\) 条额外的边,和 \(T\) 一起形成一个图 \(G\)

定义一个点 \(u\) 是好的,当且仅当对于每一个 \(1\le v\le n\)\(T\) 上的简单路径 \((u,v)\) 都可以被 \(G\) 的某个 MST 完整包含。

输出任意一个好的点。无解输出 \(0\)。有 SPJ。

\(n,m\le 10^5\)\(w\le 10^9\)

  • 参考上一题,朴素做法是 \(O(n^2)\) 枚举某个 \(x\) 开头的路径,然后 \(O(len)\) 地 check;

    注意到这个结构能很好地和 DFS + 回溯结合,用可撤销并查集维护即可在 \(O(n\log n)\) 内完成对一个 \(x\) 所有路径的 check。

  • 发现:如果一条路径 \((u,v)\) 是非法的,进一步所有完整包含 \((u,v)\) 的路径都是非法的。

    从路径反映到点上,会发现 \((u,v)\) 两侧的点都是非法的。下意识考虑不在 \((u,v)\) 两侧,但在 \((u,v)\) 之外的点,发现它们仍然有可能合法,只要满足如下情况:

  • 也就是说,对于 \(x\),如果它出发的所有路径都合法,那么 \(x\) 自己就是答案;

    如果存在 \(\ge 2\) 条非法路径,那么无解;

    否则,存在恰好一条边,在非法边所在的子树中递归求解,发现这很点分治,故可以 \(O(n\log^2 n)\) 解决。log 分别来自点分治和撤销并查集。

  • 注:这里唐唐的,check 写成点分树子树内 check 了。点分治在这里只起保证递归层数的作用。

怎么跑得这么快?推测因为写的是按秩合并。

#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("./test/20251024/beast/beast1.in", "r", stdin);
    std::freopen(".out", "w", stdout);
    const auto stime = std::chrono::steady_clock::now();
#endif
    int T;
    for (std::cin >> T; T--; ) {
        int n, m;
        std::cin >> n >> m;
        std::vector<int> l(1);
        std::vector<std::pair<int, int> > ne(m + 1);
        std::vector<std::tuple<int, int, int> > se(m + 1);
        std::vector<std::vector<std::pair<int, int> > > g(n + 1);
        std::vector<std::vector<std::tuple<int, int, int> > > e(m + 1); 
        for (int i = 1, x, y, w; i <= m; ++i) {
            std::cin >> x >> y >> w;
            se[i] = { x, y, w }, l.push_back(w);
            if (i < n)
                g[x].emplace_back(y, i), g[y].emplace_back(x, i);
        }
        std::sort(l.begin() + 1, l.end());
        l.erase(std::unique(l.begin() + 1, l.end()), l.end());
        for (int i = 1; i <= m; ++i) {
            auto [x, y, w] = se[i];
            w = std::lower_bound(l.begin() + 1, l.end(), w) - l.begin();
            e[w].emplace_back(x, y, i);
        }
        std::vector<int> f(n + 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]);
        };
        for (int i = 1; i <= m; ++i) {
            for (auto [u, v, id] : e[i])
                ne[id] = { find(u), find(v) };
            for (auto [u, v, id] : e[i])
                f[find(u)] = find(v);
        }
        std::vector<int> tag(n + 1), p, siz(n + 1), mx(n + 1);
        std::function<void(int, int)> DFS1 = [&](int x, int fa) {
            p.push_back(x);
            siz[x] = 1, mx[x] = 0;
            for (auto [i, id] : g[x])
                if (i != fa && !tag[i]) {
                    DFS1(i, x);
                    siz[x] += siz[i];
                    mx[x] = std::max(mx[x], siz[i]);
                }
            return;
        };
        auto findrt = [&](int x) {
            p.clear(), DFS1(x, -1);
            int n = (int)p.size();
            for (auto i : p)
                if (mx[i] <= n / 2 && n - siz[i] <= n / 2)
                    return i;
            assert(0);
            return -1;
        };
        std::vector<int> rank(n + 1);
        std::stack<std::tuple<int, int, int> > st;
        find = [&](int x) {
            return x == f[x] ? x : find(f[x]);
        };
        auto merge = [&](int x, int y) {
            x = find(x), y = find(y);
            if (x == y)
                return true;
            if (rank[x] <= rank[y])
                st.emplace(x, y, rank[y]), f[x] = y;
            else 
                st.emplace(y, x, rank[x]), f[y] = x;
            if (rank[x] == rank[y])
                ++rank[y];
            return false;
        };
        auto undo = [&](void) {
            assert(!st.empty());
            auto [x, y, ry] = st.top();
            st.pop();
            f[x] = x, rank[y] = ry;
            return;
        };
        std::function<bool(int, int)> DFS2 = [&](int x, int fa) {
            for (auto [i, id] : g[x])
                if (i != fa && (merge(ne[id].first, ne[id].second) || DFS2(i, x))) {
                    undo();
                    return true;
                }
            undo();
            return false;
        };
        std::function<bool(int)> DFS = [&](int x) {
            x = findrt(x);
            int to = 0;
            for (auto [i, id] : g[x])
                if (merge(ne[id].first, ne[id].second) || DFS2(i, x)) {
                    if (!to) to = i;
                    else {
                        std::cout << 0 << '\n';
                        return true;
                    }
                }
            assert(st.empty());
            tag[x] = 1;
            if (to) {
                if (tag[to]) {
                    std::cout << 0 << '\n';
                    return true;
                }
                return DFS(to);
            }
            std::cout << x << '\n';
            return true;
        };
        std::iota(f.begin() + 1, f.end(), 1);
        assert(DFS(1));
    }
#ifndef ONLINE_JUDGE
    std::cerr << std::fixed << std::setprecision(6) << std::chrono::duration<double> (std::chrono::steady_clock::now() - stime).count() << "s\n";
#endif
    return 0;
}

B. 电报 / A. 神灵庙 / CF2068D Morse Code

https://www.becoder.com.cn/contest/6676/problem/2 / https://www.becoder.com.cn/contest/5527/problem/1 / https://www.luogu.com.cn/problem/CF2068D

一个无限大的完全二叉树,根节点权值为 \(0\),一个点的左儿子权值是这个点的权值 \(+1\),右儿子为 \(+2\)

给定 \(c_1,\cdots, c_n\),挑选 \(n\) 个互不为祖孙的点,最终代价为点权 \(\times\) 对应的 \(c\) 之和。

  • 一个基本的观察:权值对应相邻两种值的结点数总是构成类似斐波那契的结构。显然应该把权值和点权逆大小对应。

    朴素 DP:令 \(f_{i,j,a,b}\) 表示当前枚举到权值 \(i\),已经选了 \(j\) 个点,\(i-1\)\(i\) 分别剩余 \(a,b\) 个点的最小代价,可以得到 \(n^5\) 的惊人转移。

  • 经典 trick:最后一层循环的枚举是无必要的,可以一个一个转移(类似完全背包)。这样就少了一个 \(n\)。如果观察到最深取到 1.5 log(斐波那契)能砍成 \(O(n^3\log n)\)

  • 接着,发现可以费用提前计算(这里我觉得更像是算贡献),少枚举一个 \(i\),这样就是 \(O(n^3)\) 的了。

    发现没什么实现的头绪。目前有:

    \[ f_{j,a,b}\to f_{j+1,a,b-1}\\ f_{j,a,b}+suf_{j+1}\to f_{j,b,a+b} \]

    以前没有注意到过,其实枚举顺序是由转移方程决定的

    • 第一种转移中,\(j\) 是递增的,\(a\) 水平,\(b\) 递减;
    • 第二种转移中,\(j\) 水平,\(a\) 无法确定,\(b\) 不递减;

    可以想到一个转移顺序:正序枚举 \(j\),正序枚举 \(b\);正序枚举 \(a\)

    此时第二种会有点问题,\(j\) 水平且 \(b\) 水平时,若 \(a\) 变小怎么办?发现这要求 \(b<a\),则 \(a+b>b\) 恒成立,\(b\) 一定不水平。挺牛的这个。

  • 卡空间了,滚动一下即可。复杂度 \(O(n^3)\)。绅士写了神秘玄学做法,瞻仰无果。

#include <bits/stdc++.h>
int main() {
#ifdef ONLINE_JUDGE
    std::ios::sync_with_stdio(false);
    std::cin.tie(nullptr), std::cout.tie(nullptr);
    std::freopen("telegram.in", "r", stdin);
    std::freopen("telegram.out", "w", stdout);
#else
    std::freopen(".in", "r", stdin);
    std::freopen(".out", "w", stdout);
    const auto stime = std::chrono::steady_clock::now();
#endif
    int n;
    std::cin >> n;
    std::vector<int> c(n + 1);
    for (int i = 1; i <= n; ++i)
        std::cin >> c[i];
    std::sort(c.begin() + 1, c.end(), std::greater<int> ());
    std::partial_sum(c.rbegin(), c.rend(), c.rbegin());
    using arr = std::vector<int>;
    using brr = std::vector<arr>;
    using crr = std::vector<brr>;
    crr f(2, brr(n + 1, arr(n + 1))), tag(2, brr(n + 1, arr(n + 1, -1)));
    f[0][1][1] = c[1], tag[0][1][1] = 0;
    auto upd = [&](int i, int &t, int &x, int y) {
        if (t == i)
            x = std::min(x, y);
        else
            x = y, t = i;
        return;
    };
    for (int j = 0, k = 0; j < n; ++j, k ^= 1) {
        for (int b = 0; b <= n; ++b)
            for (int a = 0; a <= n; ++a)
                if (tag[k][b][a] == j) {
                    if (b)
                        upd(j + 1, tag[k ^ 1][b - 1][a], f[k ^ 1][b - 1][a], f[k][b][a]);
                    if (a + b <= n)
                        upd(j, tag[k][a + b][b], f[k][a + b][b], f[k][b][a] + c[j + 1]);
                }
    }
    auto res = 0x3f3f3f3f;
    for (int b = 0; b <= n; ++b)
        for (int a = 0; a <= n; ++a)
            if (tag[n & 1][b][a] == n)
                res = std::min(res, f[n & 1][b][a]);
    std::cout << res << '\n';
#ifndef ONLINE_JUDGE
    std::cerr << std::fixed << std::setprecision(6) << std::chrono::duration<double> (std::chrono::steady_clock::now() - stime).count() << "s\n";
#endif
    return 0;
}

[COI 2019] TENIS

https://www.luogu.com.cn/problem/P11340

  • 观察一:\(x\) 能获胜,当且仅当存在一个 \(y\),使得 \(x\) 能战胜 \(y\),且 \(y\) 能在剩下 \(n-1\) 个人中获胜。

    发现这是一个递归,或者说链式的关系。

  • 观察二:不妨把三个数组视为三条链,那么发现任意一个链首在任意局面总是能获胜。如果一个人能战胜链首,显然这个人能获胜。反过来,若不能通过任何方式战胜链首,显然无法获胜。

    然后就变成连通性问题了。但很容易猜到带删边连通性没什么优化的前途。

    只有加边的带修连通性问题:整体二分

  • 观察三:链首不能被 \(x\) 战胜,当且仅当存在一个 \(i\),三条链的前 \(i\) 个元素集相同,且 \(x\) 位于最小的 \(i\) 之后。

    这个看起来和 Puuding Monsters / Good Subsegments 比较像,实际上思路上也有共通之处:如果合法区间的值域是连续的,可以把条件转化为 min / max 问题。

    考虑先使其询问区间内值域连续以套用这样的方法。可以对链 1 重标号为 \(1\cdots n\),维护链 2、3 的答案;也可以维护三个 rank 上的答案。法一实现起来有很多不必要的令人讨厌的地方,故写法二。

  • 相当于求一个最小的 \(i\) 使得链 1、2、3 在 \(i\) 之前的所有元素 rank 都不能比 \(i\) 大。主体为 \(i\) 时,(因为最小这个限制,和依次改动会带来很大的偏移),所以不太好维护;

    但是正确的维护方式很难想到!令 \(f_i,g_i\) 分别表示链 2、3 的每个 \(1\le j\le n\),满足 \(\min(r_{1/2/3,j})\)\(\max(r_{1/2/3,j})= i\)\(j\) 的数量。如果二者在 \(i\) 处的前缀和相等,那么 \(i\) 就是合法的。

    Fractures(新角色):这是个很常见的 trick。

    Fractures:这个和 Pudding Monsters 哪里像了。是你唐。

    维护 \(f_i-g_i\) 的前缀和;线段树维护区间修改和全局 min 即可。

莫名很慢。和 TTpandaS 相比多了一次建树(但不应该有这么大的影响才对)。

#include <bits/stdc++.h>
const int maxn = 1e5 + 5;
struct { int l, r, mn, at, d; } t[maxn << 2];
#define lt (p << 1)
#define rt (lt | 1)
void pushup(int p) {
    t[p].mn = std::min(t[lt].mn, t[rt].mn);
    if (t[lt].mn == t[p].mn)
        t[p].at = t[lt].at;
    else
        t[p].at = t[rt].at;
    return;
}
void pushdown(int p) {
    if (t[p].d) {
        t[lt].d += t[p].d, t[lt].mn += t[p].d;
        t[rt].d += t[p].d, t[rt].mn += t[p].d;
        t[p].d = 0;
    }
    return;
}
void bld(int p, int l, int r) {
    t[p].l = l, t[p].r = r;
    if (l == r) {
        t[p].at = l;
        return;
    }
    int mid = (l + r) >> 1;
    bld(lt, l, mid), bld(rt, mid + 1, r);
    pushup(p);
    return;
}
void add(int p, int l, int r, int v) {
    if (l <= t[p].l && t[p].r <= r) {
        t[p].d += v, t[p].mn += v;
        return;
    }
    pushdown(p);
    int mid = (t[p].l + t[p].r) >> 1;
    if (l <= mid)
        add(lt, l, r, v);
    if (r > mid)
        add(rt, l, r, v);
    pushup(p);
    return;
}
#undef lt
#undef rt
int main() {
#ifdef ONLINE_JUDGE
    std::ios::sync_with_stdio(false);
    std::cin.tie(nullptr), std::cout.tie(nullptr);
    std::freopen("match.in", "r", stdin);
    std::freopen("match.out", "w", stdout);
#else
    std::freopen(".in", "r", stdin);
    std::freopen(".out", "w", stdout);
    const auto stime = std::chrono::steady_clock::now();
#endif
    int n, q;
    std::cin >> n >> q;
    std::vector<std::vector<int> > p(3, std::vector<int> (n + 1));
    for (int k = 0; k < 3; ++k)
        for (int i = 1, x; i <= n; ++i)
            std::cin >> x, p[k][x] = i;
    bld(1, 1, n);
    auto upd = [&](int i, int k) {
        add(1, std::min({ p[0][i], p[1][i], p[2][i] }), n, k);
        add(1, std::max({ p[0][i], p[1][i], p[2][i] }), n, -k);
        return;
    };
    for (int i = 1; i <= n; ++i)
        upd(i, 1);
    int now = t[1].at;
    // printf("now = %d \n", now);
    for (int op; q--; ) {
        std::cin >> op;
        if (op == 1) {
            int x;
            std::cin >> x;
            std::cout << (std::min({ p[0][x], p[1][x], p[2][x] }) <= now ? "Yes" : "No") << '\n';
        }
        else {
            int k, i, j;
            std::cin >> k >> i >> j, --k;
            upd(i, -1), upd(j, -1);
            std::swap(p[k][i], p[k][j]);
            upd(i, 1), upd(j, 1);
            now = t[1].at;
            // printf("now = %d \n", now);
        }
    }
#ifndef ONLINE_JUDGE
    std::cerr << std::fixed << std::setprecision(6) << std::chrono::duration<double> (std::chrono::steady_clock::now() - stime).count() << "s\n";
#endif
    return 0;
}

言论