杂题
2025-10-07

近期模拟赛


A. 考试

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

\(m\) 道题,分数是一个 \(m\) 的排列(初始未知),做对得满分,做错得 0 分

给定 \(n\) 个人对于这 \(m\) 个题的过题情况矩阵,且第 \(i\) 个人有一个预期分数 \(x_i\)

求每到题的分数,使得每个人实际得分与期望的分差的绝对值之和最大。输出该排列。

\(n\le 10,m\le 10^4\)

  • 绝对值是贪心一个很大的阻碍
  • 一个典型的『钦定』型贪心,枚举每个人的绝对值是否取反,就可以得到每个题对应的系数。

    因为『就算错了也不会影响最优解』,所以是对的。

#include <bits/stdc++.h>
int main() {
    std::ios::sync_with_stdio(false);
    std::cin.tie(nullptr), std::cout.tie(nullptr);
    std::freopen("test.in", "r", stdin);
    std::freopen("test.out", "w", stdout);
    int T;
    for (std::cin >> T; T--; ) {
        int n, m;
        std::cin >> n >> m;
        std::vector<int> s(n + 1);
        for (int i = 1; i <= n; ++i)
            std::cin >> s[i];
        std::vector<std::vector<int> > pos(n + 1);
        for (int i = 1; i <= n; ++i)
            for (int j = 1; j <= m; ++j) {
                char t;
                std::cin >> t;
                if (t == '1')
                    pos[i].push_back(j);
            }
        auto res = 0ll;
        int siz = 1 << n;
        std::vector<std::vector<int> > id(2 * n + 1);
        std::vector<int> cnt(m + 1), ans(m + 1), p(m + 1);
        for (int i = 0; i < siz; ++i) {
            std::fill(cnt.begin() + 1, cnt.end(), 0);
            for (int j = 0; j <= 2 * n; ++j)
                id[j].clear();
            for (int j = 1; j <= n; ++j)
                if ((i >> (j - 1)) & 1)
                    for (auto k : pos[j])
                        ++cnt[k];
                else
                    for (auto k : pos[j])
                        --cnt[k];
            for (int j = 1; j <= m; ++j)
                id[cnt[j] + n].push_back(j);
            for (int i = 2 * n, k = m; ~i; --i)
                for (auto j : id[i])
                    p[j] = k--;
            auto now = 0ll;
            for (int i = 1; i <= n; ++i) {
                int sum = 0;
                for (auto j : pos[i])
                    sum += p[j];
                now += std::abs(sum - s[i]);
            }
            if (now >= res)
                res = now, ans = p;
        }
        // std::cout << res << '\n';
        for (int i = 1; i <= m; ++i)
            std::cout << ans[i] << ' ';
        std::cout << '\n';
    }
    return 0;
}

B. 围棋

http://222.180.160.110:61235/contest/6624/problem/2

给定 \(n \times n\) 的棋盘,上面有黑棋、白棋和空地,定义白棋的『气』为四个方向空地的数量,若一个白棋四连通块中所有白棋的『气』都为 0,则认为这个白棋四连通块中的所有白棋是『死的』。

现枚举每个棋子,并将其反转颜色,问『死的』白棋数量。操作是独立的。

3s,\(n\le 10^3\)

  • 分讨题,需要想清楚。首先需要一次 floodfill 完成染色,记录哪些连通块死了。
  • 若反转黑棋:

    • 若黑棋自己有气,或和黑棋相连的白棋四连通块有气,那么黑棋自己,以及所有和黑棋相连的白棋四连通块都是活的。

      此时需要找到所有和黑棋相连的,且死了的白棋四连通块的大小,答案需要减去之。

  • 否则,黑棋自身贡献 \(1\) 的答案。

  • 若反转白棋:

    • 若白棋本身是死的:答案 \(-1\)
    • 否则,需要考虑有多少个原本是活的白棋此时与有气的白棋不连通,这种情况发生当且仅当有气的白棋是被反转的白棋自身,或者被反转的白棋是割点。

      很奇妙的一个想法是建立超级源点,向所有空地连边,再建圆方树,这样这个点的所有儿子就是待求。圆方树并不需要显式地建出来。

代码:不会


C. 相互抵消

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

给定 \(a_{1\sim n}\),维护 \(m\) 次操作:

  1. 区间加。
  2. 给定 \(l,r\),查询 \(\left(\sum\limits_{i=l}^r\sum\limits_{j=i}^r ((\sum_{k=i}^j a_k)^2 + (r-l+2)\times(j-i)\times a_i\times a_j)\right) \bmod 998244353\)

\(n,m\le 5\times 10^5\),强制在线。

  • 题目名字是真在降低难度吧,强制在线就排除离线历史和了;猜测询问所求可以化成一个比较简单的东西。
  • 推一下:

    (省略)

    \[ =-\left(\sum_{i=l}^r i\cdot a_i\right)^2 + (1-l)(r+1)\left(\sum_{i=l}^r a_i\right)^2+(l+r)\left(\sum_{i=l}^r i\cdot a_i\right)\left(\sum_{i=l}^r a_i\right) \]

  • 故线段树维护 \(\sum a_i\)\(\sum i\cdot a_i\) 即可。
  • 实际上可以用树状数组维护

喜提最优解,不得不说 bit 的速度优势还是很明显的。

#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("offset.in", "r", stdin);
    std::freopen("offset.out", "w", stdout);
#else
    std::freopen("./test/20251008/offset/ex_offset5.in", "r", stdin);
    std::freopen(".out", "w", stdout);
    const auto stime = std::chrono::steady_clock::now();
#endif
    int T, n, q;
    std::cin >> T >> n >> q;
    std::vector<long long> bit(n + 1), bit1(n + 1), bit2(n + 1);
    auto lowbit = [&](int x) {
        return x & -x;
    };
    auto add = [&](long long x, long long v) {
        auto v1 = x * v % mod, v2 = x * x % mod * v % mod;
        for (; x <= n; x += lowbit(x)) {
            (bit[x] += v) %= mod;
            (bit1[x] += v1) %= mod;
            (bit2[x] += v2) %= mod;
        }
        return;
    };
    auto ask = [&](long long x) {
        auto res = 0ll, res1 = 0ll, res2 = 0ll;
        for (int i = x; i; i -= lowbit(i))
            (res += bit[i]) %= mod, (res1 += bit1[i]) %= mod, (res2 += bit2[i]) %= mod;
        return std::make_pair(((x + 1) * res + mod - res1) % mod, (x * (x + 1) % mod * res % mod + res1 + mod - res2) * inv2 % mod);
    };
    for (int i = 1, x; i <= n; ++i)
        std::cin >> x, add(i, x), add(i + 1, mod - x);
    for (int op; q--; ) {
        static auto la = 0ll;
        std::cin >> op;
        if (op == 1) {
            int l, r, d;
            std::cin >> l >> r >> d;
            if (T == 1)
                l ^= la, r ^= la, d ^= la;
            add(l, d), add(r + 1, mod - d);
        }
        else {
            long long l, r;
            std::cin >> l >> r;
            if (T == 1)
                l ^= la, r ^= la;
            auto t1 = ask(r), t2 = ask(l - 1);
            auto res1 = t1.first + mod - t2.first, res2 = t1.second + mod - t2.second;
            la = (mod - res2 * res2 % mod + mod - (l - 1) * (r + 1) % mod * res1 % mod * res1 % mod + (l + r) * res1 % mod * res2 % mod) % mod;
            std::cout << la << '\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;
}

A. Happy·Lovely·Everyday!

https://www.becoder.com.cn/contest/6639/problem/1

给定一个长为 \(n\) 的 01 序列,可以执行任意次(可以为 \(0\))『把相邻两个数合并为其异或值』的操作,问最终能得到的本质不同的序列个数。

\(n\le 2\times 10^6\)

  • 对于一个最终序列,如果认为它是由某个存活元素『吞并』它左侧的连续死亡元素得到的,会发现最终序列中,此处的前缀异或值就是原序列的前缀异或值。
  • 经典 trick:前缀和可以唯一对应原数组。所以『最终本质不同序列』会和『包含第 \(n\) 个元素的,原序列前缀异或和的本质不同子序列』形成双射。
  • 统计包含第 \(n\) 个元素的,原序列前缀异或和的本质不同子序列即可。复杂度 \(O(n)\)

    How to 本质不同子序列?

    \(f_i\) 表示 DP 到 \(i\),且强制选 \(i\) 的本质不同子序列数量,记 \(pre_i\)\(i\) 的前驱(没有则为 \(0\))。令 \(f_0=1\),那么:

    \[ f_i\gets \sum\limits_{j=pre_i}^{i-1} f_{j} \]

    或,令 \(f_i\) 表示 DP 到 \(i\) 时(不强制选 \(i\))本质不同子序列数量,记 \(pre_i\)\(i\) 的前驱(没有则为 \(0\))。令 \(f_0=0\),那么:

    \[ f_i=\begin{cases} 2\cdot f_{i-1}+1&pre_i= 0\\ 2\cdot f_{i-1}-f_{pre_i-1}&\text{otherwise} \end{cases} \]

    How to 强制选第 \(n\) 个元素的本质不同子序列?

    \(f_i\) 表示 DP 到 \(i\),且强制选 \(i\) 的本质不同子序列数量,则答案为:

    \[ \sum\limits_{a_i=a_n} f_i \]

    或,令 \(f_i\) 表示 DP 到 \(i\),且不强制选 \(i\) 的本质不同子序列数量,则答案为:

    \[ \sum\limits_{a_i=a_n}\begin{cases} f_{i-1}+1&pre_i= 0\\ f_{i-1}-f_{pre_i-1}&\text{otherwise} \end{cases} \]

#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);
    std::freopen("a.in", "r", stdin);
    std::freopen("a.out", "w", stdout);
#else
    std::freopen("./test/20251014/a/a1.in", "r", stdin);
    std::freopen(".out", "w", stdout);
    const auto stime = std::chrono::steady_clock::now();
#endif
    int T;
    for (std::cin >> T; T--; ) {
        std::string s;
        std::cin >> s;
        int n = (int)s.length();
        s = "#" + s;
        std::vector<int> a(n + 1);
        for (int i = 1; i <= n; ++i)
            a[i] = a[i - 1] ^ (s[i] - '0');
        auto res = 0ll;
        std::vector<long long> f(n + 1), la(2);
        for (int i = 1; i <= n; ++i) {
            if (la[a[i]])
                f[i] = (2 * f[i - 1] + mod - f[la[a[i]] - 1]) % mod;
            else
                f[i] = (2 * f[i - 1] + 1) % mod;
            if (a[i] == a[n]) {
                if (la[a[i]])
                    res += (f[i - 1] + mod - f[la[a[i]] - 1]) % mod;
                else
                    res += f[i - 1] + 1;
            }
            la[a[i]] = i;
        }
        std::cout << res % 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/6639/problem/2

定义 \(f_0=f_1=1,f_i=f_{i-1}+f_{i-2}\),给定 \(n,k\),求:

\[ \left(\sum_{i=0}^n f_{i}\cdot [\text{popcount}(i)=k]\right)\bmod 10^9+7 \]

\(n\le 2^{2000},0\le k\le \log_2(n)\)

  • 考虑数位 DP,那么需要能够由 ...xxx...0xxx...1xxx 转移。
  • 显然只关心向 ...1xxx 的转移,那么需要再转移 \(2^{?}\) 次,记录转移矩阵的 \(2^{?}\) 次方即可。

    这一步也可以不用矩阵,而是用斐波那契 \(f(ab)=...\) 的性质,但显然二者是等价的,无甚必要

  • 根据数位 DP 的实现方式,常数上顺序结构肯定是会吊打递归的。然而并不惯写顺序结构的数位 DP,whatever.

#include <bits/stdc++.h>
const int mod = 1e9 + 7;
struct matrix {
    long long a[2][2];
    matrix() {
        a[0][0] = a[0][1] = a[1][0] = a[1][1] = 0ll;
        return;
    }
    long long* operator[] (const int q) {
        return a[q];
    }
    matrix operator* (matrix &q) const {
        matrix res;
        res[0][0] = (a[0][0] * q[0][0] + a[0][1] * q[1][0]) % mod;
        res[0][1] = (a[0][0] * q[0][1] + a[0][1] * q[1][1]) % mod;
        res[1][0] = (a[1][0] * q[0][0] + a[1][1] * q[1][0]) % mod;
        res[1][1] = (a[1][0] * q[0][1] + a[1][1] * q[1][1]) % mod;
        return res;
    }
    matrix& operator+= (matrix q) {
        if ((a[0][0] += q[0][0]) >= mod) a[0][0] -= mod;
        if ((a[0][1] += q[0][1]) >= mod) a[0][1] -= mod;
        if ((a[1][0] += q[1][0]) >= mod) a[1][0] -= mod;
        if ((a[1][1] += q[1][1]) >= mod) a[1][1] -= mod;
        return *this;
    }
};
int main() {
#ifdef ONLINE_JUDGE
    std::ios::sync_with_stdio(false);
    std::cin.tie(nullptr), std::cout.tie(nullptr);
    std::freopen("b.in", "r", stdin);
    std::freopen("b.out", "w", stdout);
#else
    std::freopen("./test/20251014/b/b4.in", "r", stdin);
    std::freopen(".out", "w", stdout);
    const auto stime = std::chrono::steady_clock::now();
#endif
    int n, k;
    std::cin >> n >> k;
    std::vector<int> up(n);
    for (int i = 1; i <= n; ++i) {
        char t;
        std::cin >> t, up[n - i] = t - '0';
    }
    matrix init, zmat;
    init[0][0] = init[0][1] = 1ll;
    std::vector<matrix> mat(n);
    mat[0][0][1] = mat[0][1][0] = mat[0][1][1] = 1ll;
    for (int i = 1; i < n; ++i)
        mat[i] = mat[i - 1] * mat[i - 1];
    std::vector<std::vector<int> > tag(n, std::vector<int> (k + 1));
    std::vector<std::vector<matrix> > f(n, std::vector<matrix> (k + 1));
    std::function<matrix(int, int, int)> DFS = [&](int x, int k, int fl) {
        if (x == -1) {
            if (k == 0)
                return init;
            return zmat;
        }
        if (!fl && tag[x][k])
            return f[x][k];
        matrix res = DFS(x - 1, k, fl && !up[x]);
        if (k && (!fl || up[x]))
            res += DFS(x - 1, k - 1, fl) * mat[x];
        if (!fl)
            tag[x][k] = 1, f[x][k] = res;
        return res;
    };
    std::cout << DFS(n - 1, k, 1)[0][0] << '\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. Lead to shine more

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

\(0\) 次操作时变量 \(x=1\),此后每次操作使 \(x\gets x+\text{popcount}(x)\),给定 \(m\) 次询问:

  • 给定 \(x'\),问 \(x\) 在第几次操作后变为 \(x'\),或报告 \(x'\) 不会被经过。

\(m\le 10^5,x'\le 10^{18}\)

  • 一个朴素的想法是分高低位,则因为每次操作带来的变化量不超过 60,远远低于低位的 \(2^30\),所以一次操作至多给高位带来 1 的变化量。
  • 高位对整体的贡献只取决于其 popcount,所以可以枚举高位 1 的数量,再枚举初始值,找到第一次让高位变化花的次数。

    初始值的可能值有 \(60\) 个,而外层循环有 \(30\) 次,内层循环大概是 \(\dfrac {2^{30}}{60}\) 的,总之无法通过。

  • 题解说方法是对上面的拓展。理解不能。出发点大概是考虑到让第 \(i\) 位变化相当于让 \(i+1\) 位变化两次,所以会有一个递推的关系。

    \(f_{i, s, d}\) 为前 \(i-1\) 位的 popcount 为 \(d\),初始值为 \(s\),让 \(i\) 位变化一次的操作次数;\(g_{i,s,d}\) 为上述变化后 \(x\) 的值。记 \(pos=g_{i-1,s,d}\),那么有:

    \[ g_{i,pos,d}=g_{i-1,pos,d+1}\\ f_{i,pos,d}=f_{i-1,s,d}+f_{i-1,pos,d+1} \]

    其实是一个类似倍增的结构。但恕我实在无法解释怎么想到的。

  • 显然这个式子在 \(i\le 5\) 时是有问题的,这部分我们暴力跑即可。忽略这一部分常数,预处理总复杂度 \(O(\log^3 n)\)
  • 考虑查询,按照类似数位 DP 的方法从高位到低位模拟进位过程即可。复杂度 \(O(\log n)\)

#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("c.in", "r", stdin);
    std::freopen("c.out", "w", stdout);
#else
    std::freopen("./test/20251014/c/c1.in", "r", stdin);
    std::freopen(".out", "w", stdout);
    const auto stime = std::chrono::steady_clock::now();
#endif
    using arr = std::vector<long long>;
    using brr = std::vector<arr>;
    using crr = std::vector<brr>;
    crr f(61, brr(256, arr(61))), g(61, brr(256, arr(61)));
    for (int s = 0; s < 256; ++s)
        for (int d = 0; d <= 60; ++d)
            if (d || s) {
                int x = s, cnt = 0;
                for (; x < 256; x += __builtin_popcount(x) + d, ++cnt);
                f[8][s][d] = cnt, g[8][s][d] = x & 255;
            }
    for (int i = 9; i <= 60; ++i)
        for (int s = 0; s < 256; ++s)
            for (int d = 0; d < 60; ++d) {
                int pos = g[i - 1][s][d];
                g[i][s][d] = g[i - 1][pos][d + 1];
                f[i][s][d] = f[i - 1][s][d] + f[i - 1][pos][d + 1];
            }
    int m;
    std::cin >> m;
    for (long long x; m--; ) {
        std::cin >> x;
        auto pos = 1ll, cnt = 0ll, tot = 0ll;
        for (int i = 60; i >= 8; --i)
            if ((x >> i) & 1) {
                cnt += f[i][pos][tot];
                pos = g[i][pos][tot++];
            }
        for (; pos < (x & 255); pos += __builtin_popcountll(pos) + tot, ++cnt);
        // std::cout << pos << ' ' << cnt << '\n';
        if (pos == (x & 255))
            std::cout << cnt << '\n';
        else
            std::cout << -1 << '\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. Query On A Tree 17

https://codeforces.com/gym/102759/problem/I

给定一个以 \(1\) 为根的树,点权初始为全 \(0\)。维护 \(m\) 次修改:

  • 1 u,表示把 \(u\) 子树内的点权加一。
  • 2 u v,表示把 \(u,v\) 简单路径上的点权加一。

每次修改后,输出树深度最小的带权重心。

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

  • 考虑链上的问题,考虑一点小奥做法,把所有点重复其出现次数次,从左到右排列,发现答案在中位数上。

    (据组题人所言),这是用来提示正解的。事实上,这是一个和带权重心有关的 trick:

    若一个点 \(u\)深度最小的带权重心,则其子树权值和必须严格大于总权值的一半。

    证明显然,但结论不显然。Whatever.

  • 由这一点会得到,(参照链上的做法),把点重复点权次,并按 dfn 排成一行,取中位数,那么取到的一定在深度最小的重心子树内,(这就是组题人说的提示正解)。

    这一点反而没那么显然,推导过程是,深度最小的重心的子树占了大于一半,而其 dfn 又是连续的,(类似滑动窗口),故总能够到中位数。

    考虑非深度最小的重心,此时就从严格大于变为不严格大于,导致没有办法很好地精准找到,需要从中位数左右偏移一位,显然是很丑陋的。

  • 因此可以考虑线段树上二分找到中位数(容易发现线段树的 \(x\) 轴本来就是 dfn),然后从这个中位数往上跳,一直跳到满足『子树权值和严格大于总权值一半』的点,就是最浅的重心。

    这一点采用倍增即可。


言论