C. Change Schools

选人数最多的班级或比人数最多的班级人数少 \(1\) 人的班级

代码实现
#include <bits/stdc++.h>
#define rep(i, n) for (int i = 0; i < (n); ++i)

using namespace std;

int main() {
    int n, k;
    cin >> n >> k;
    
    vector<int> a(n);
    rep(i, n) cin >> a[i], --a[i];
    
    vector<int> c(k);
    rep(i, n) c[a[i]]++;
    
    int mx = ranges::max(c);
    
    int ans = 0;
    rep(i, k) if (c[i]+1 >= mx) ans++;
    
    cout << ans << '\n';
    
    return 0;
}

D. Coefficient Stair

暴搜

代码实现
#include <bits/stdc++.h>
#define rep(i, n) for (int i = 0; i < (n); ++i)

using namespace std;

int main() {
    int n, k;
    cin >> n >> k;
    
    vector<vector<int>> ans;
    
    auto dfs = [&](this auto dfs, vector<int> a, int rem) -> void {
        int i = n-a.size();
        if (i == 1) {
            a.push_back(rem);
            ranges::reverse(a);
            ans.push_back(a);
            return;
        }
        
        a.push_back(0);
        while (rem >= 0) {
            dfs(a, rem);
            a.back()++;
            rem -= i;
        }
    };
    dfs({}, k);
    
    ranges::sort(ans);
    
    for (auto a : ans) {
        for (int x : a) cout << x << ' ';
        cout << '\n';
    }
    
    return 0;
}

E. K-Divisible Subarrays

\(B_i = \sum\limits_{j=1}^i A_j \bmod K\)
特别地,\(B_0 = 0\)

若存在 \(j < i\)\(B_j = B_i\),则区间 \([j+1, i]\) 的累加和能被 \(K\) 整除。
把数组切成若干连续段,每一段和能被 \(K\) 整除等价于在前缀和序列中选择若干对相同余数的下标作为相邻分段的两端。

dp[r] 表示在已经处理到当前前缀位置(即已知若干前缀),存在一个分割方案,其目前最后一个前缀的余数是 \(r\) 时,所能达到的最大“好段(和可被 K 整除 的段)”数。
可以用 std::map 来维护

代码实现
#include <bits/stdc++.h>
#define rep(i, n) for (int i = 0; i < (n); ++i)

using namespace std;

int main() {
    int n, k;
    cin >> n >> k;
    
    vector<int> a(n);
    rep(i, n) cin >> a[i];
    
    vector<int> b(n+1);
    rep(i, n) b[i+1] = (b[i]+a[i])%k;
    
    map<int, int> dp;
    dp[0] = 0;
    int mx = 0;
    for (int i = 1; i <= n; ++i) {
        int now = mx;
        if (dp.count(b[i])) now = max(now, dp[b[i]]+1);
        mx = max(mx, now);
        dp[b[i]] = now;
    }
    
    cout << mx << '\n';
    
    return 0;
}

F. A/AB Insertion

把字符映射成数值:\(A \longmapsto 1\)\(B \longmapsto -1\)
对一个字符串 \(T\),若其任意前缀的 \(A\) 个数都不少于 \(B\) 个数,则对应前缀和序列的每个前缀和都 \(\geqslant 0\)
插入操作“插入 \(A\)” 和"插入 \(AB\)” 恰好能生成且仅能生成满足“任意前缀和 \(\geqslant 0\)” 的字符串,所以判定问题等价于判断子串 \(T=S[l \dots r]\) 的最小前缀和是否非负。
用线段树维护一下区间和以及最小前缀和即可

代码实现
#include <bits/stdc++.h>
#include <atcoder/all>
using namespace atcoder;
#define rep(i, n) for (int i = 0; i < (n); ++i)

using namespace std;

struct S {
    int sum, b;
};
S op(S a, S b) {
    return S(a.sum+b.sum, min(a.b, b.b+a.sum));
}
S e() { return S(0, 0); }

int main() {
    int n, q;
    string s;
    cin >> n >> s >> q;
    
    segtree<S, op, e> t(n);
    auto f = [&](char c) {
        if (c == 'A') return S(1, 0);
        return S(-1, -1);
    };
    rep(i, n) t.set(i, f(s[i]));
    
    rep(qi, q) {
        int type;
        cin >> type;
        if (type == 1) {
            int i; char c;
            cin >> i >> c;
            --i;
            t.set(i, f(c));
        }
        else {
            int l, r;
            cin >> l >> r;
            --l;
            if (t.prod(l, r).b < 0) puts("No");
            else puts("Yes");
        }
    }
    
    return 0;
}

G. Wipeout

为了使期望操作次数最小,在寻找当前目标数字 \(x\)\(x\)\(1\) 递增到 \(N\))时,应采取如下策略:

已知位置: 若数字 \(x\) 的位置之前翻牌时已记录,则直接翻开并吃掉它,耗费 \(1\) 次操作。
未知位置: 若数字 \(x\) 的位置未知,从剩余未翻开的牌中随机翻开一张:

  • 若恰好是 \(x\):直接吃掉,此牌全过程仅耗费 \(1\) 次操作;
  • 若是更大数字 \(y\)\(y>x\)):记住 \(y\) 的位置并翻回。由于 \(y\) 提前被曝光,未来寻找 \(y\) 时只需直接翻开(\(1\) 次),加上本次翻错(\(1\) 次),数字 \(y\) 全过程累计耗费 \(2\) 次操作。

在整个游戏过程中,每一张牌必然有且仅有一次“首次从未知牌中被翻开”的时刻:

  • 当剩余未翻开的牌数量为 \(r\)\(r = N, N-1, \dots, 1\))时,我们从中随机翻开一张。
  • 由均匀随机性与对称性可知,翻到的这张牌恰好是这 \(r\) 张牌中最小数(即当前目标 \(x\))的概率为 \(\frac{1}{r}\),翻到非最小数的概率为 \(\frac{r-1}{r}\)
  • 若翻到最小数(概率 \(\frac{1}{r}\)),该牌后续无需二次翻开,全过程对总操作数贡献为 \(1\)
  • 若翻到非最小数(概率 \(\frac{r-1}{r}\)),该牌后续还会被直接翻开一次,全过程对总操作数贡献为 \(2\)

不同 \(r\) 阶段的选择在统计上相互独立。


\(g\) 为全过程中贡献为 \(1\) 次操作的牌的张数,则总操作数 \(K = 2N - g\),即 \(g = 2N - K\)。针对每个 \(r \in \{1, 2, \dots, N\}\),定义概率生成函数:$$F(x) = \prod_{r=1}^{N} \left( \frac{r-1}{r} + \frac{1}{r} x \right)$$

那么答案就是 \([x^g]F(x)\)
跑一遍分治 \(\text{NTT}\) 即可

代码实现
#include <bits/stdc++.h>
#include <atcoder/all>
using namespace atcoder;
#define rep(i, n) for (int i = 0; i < (n); ++i)

using namespace std;
using mint = modint998244353;

int main() {
    int n, k;
    cin >> n >> k;
    
    int g = n*2-k;
    if (g < 0 or g > n) {
        cout << 0 << '\n';
        return 0;
    }
    
    queue<vector<mint>> q;
    rep(i, n) {
        mint p = mint(1)/(i+1);
        vector<mint> f = {p*i, p};
        q.emplace(f);
    }
    while (q.size() > 1) {
        auto a = q.front(); q.pop();
        auto b = q.front(); q.pop();
        q.push(convolution(a, b));
    }
    
    cout << q.front()[g].val() << '\n';
    
    return 0;
}