C. Plumage Palette

维护当前每种颜色的出现次数,考虑它更新前后对答案的影响

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

using namespace std;

int main() {
    int n, m;
    cin >> n >> m;
    
    vector<vector<pair<int, int>>> events(m);
    rep(i, n) {
        int a, d, b;
        cin >> a >> d >> b;
        --a; --d; --b;
        events[0].emplace_back(a, 1);
        events[d].emplace_back(a, -1);
        events[d].emplace_back(b, 1);
    }
    
    vector<int> cnt(n);
    int ans = 0;
    rep(i, m) {
        for (auto [c, e] : events[i]) {
            if (cnt[c] == 0) ans++;
            cnt[c] += e;
            if (cnt[c] == 0) ans--;
        }
        cout << ans << '\n';
    }
    
    return 0;
}

D. Celester

dp[i][j] 表示到第 \(i\) 天为止第 \(i\) 天的天气是 \(j\) 时的最大幸福度
其中 \(j\) 表示天气是 \(S\)\(R\)

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

using namespace std;
using ll = long long;

inline void chmax(ll& a, ll b) { if (a < b) a = b; }

void solve() {
    int n;
    string s;
    cin >> n >> s;
    
    vector<int> x(n), y(n);
    rep(i, n) cin >> x[i];
    rep(i, n-1) cin >> y[i+1];
    
    const ll INF = 1e18;
    vector dp(n+1, vector<ll>(2, -INF));
    dp[0][0] = 0;
    rep(i, n) {
        int c = s[i]=='R';
        rep(j, 2) {
            rep(nj, 2) {
                ll now = dp[i][j];
                if (c != nj) now -= x[i];
                if (j == 1 and nj == 0) now += y[i];
                chmax(dp[i+1][nj], now);
            }
        }
    }
    
    ll ans = ranges::max(dp[n]);
    cout << ans << '\n';
}

int main() {
    int t;
    cin >> t;
    
    while (t--) solve();
    
    return 0;
}

E. Fill-Rect Query

注意到,格子 \((i,j)\) 最终的字母是所有满足 \(R≥i\)\(C≥j\) 的操作中最后一次覆盖它的操作;若没有则为 A
可以把每次操作的编号记录到其右下角格子 \(d[r][c]\) 上;然后从右下向左上做后缀最大化,这样 \(d[i][j]\) 变为能覆盖 \((i,j)\) 的最后一次操作。

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

using namespace std;
using ll = long long;

int main() {
    int h, w, q;
    cin >> h >> w >> q;
    
    vector d(h, vector<int>(w));
    string X = "A";
    rep(i, q) {
        int r, c; char x;
        cin >> r >> c >> x;
        --r; --c;
        d[r][c] = i+1;
        X += x;
    }
    
    for (int i = h-1; i >= 0; --i) {
        for (int j = w-1; j >= 0; --j) {
            if (i+1 < h) d[i][j] = max(d[i][j], d[i+1][j]);
            if (j+1 < w) d[i][j] = max(d[i][j], d[i][j+1]);
        }
    }
    
    rep(i, h) {
        string s;
        rep(j, w) s += X[d[i][j]];
        cout << s << '\n';
    }
    
    return 0;
}

F. Random Vault Heist

根据期望的线性性质,最终获得的总金额期望,等于每个保险箱被盗取的期望金额之和。

一个保险箱 \(v\) 会被强盗盗取,当且仅当它被打开了。而它会被打开的充要条件是:在轮到打开保险箱 \(v\) 之前,前面已经打开的所有保险箱的总金额严格小于 \(X\)

因此,我们可以转而统计:有多少种抽取序列,使得在选到 \(v\) 之前,已选箱子的总和小于 \(X\)

假设在打开保险箱 \(v\) 之前,强盗已经打开了一个包含 \(k\) 个保险箱的集合 \(S\)(显然 \(v \notin S\)),且这个集合的面额之和满足 \(\sum\limits_{u \in S} A_u < X\)

我们可以计算这种特定情况发生的概率:前 \(k\) 步恰好选中集合 \(S\) 的概率:

  1. \(N\) 个箱子里无序地选出 \(k\) 个,一共有 \(\binom{N}{k}\) 种可能,选中特定集合 \(S\) 的概率是 \(\frac{1}{\binom{N}{k}}\)
  2. \(k+1\) 步恰好选中 \(v\) 的概率:此时还剩下 \(N-k\) 个未打开的箱子,由于是等概率随机抽取,选中 \(v\) 的概率是 \(\frac{1}{N-k}\)

因此,对于一个特定的合法前置集合 \(S\) 和一个不在其中的箱子 \(v\),在第 \(k+1\) 步打开 \(v\) 的概率为:$$P = \frac{1}{\binom{N}{k} \cdot (N-k)}$$理论上,我们只需要枚举所有满足“和小于 \(X\)”的子集 \(S\),然后找出所有不在 \(S\) 中的箱子 \(v\),将 \(A_v \times P\) 累加到答案中即可。

虽然数学公式很清晰,但 \(N\) 最大可达 \(40\),我们无法直接枚举 \(2^{40}\) 个子集(这会引发指数爆炸)。为了解决这个问题,需要引入折半搜索:

1. 拆分与预处理

\(40\) 个保险箱平均分成左组(约 \(20\) 个)和右组(约 \(20\) 个)。

  • 分别暴力枚举左组和右组的所有子集。
  • 将这些子集按照“选了多少个元素”进行分类。例如,左组选了 \(i\) 个元素的所有可能和存在一个数组里,右组选了 \(j\) 个元素的所有可能和存在另一个数组里。
  • 将这些分类后的和数组分别进行升序排序。

2. 双指针合并统计

为了算出总大小为 \(k = i+j\) 的合法前置集合 \(S\),我们固定左组选 \(i\) 个元素,右组选 \(j\) 个元素:

  • 由于左组和右组的金额数组都是有序的,我们可以使用双指针。当右组的子集和逐渐增大时,左组能凑出的最大合法和的边界就会单调向左移动。
  • 这样,我们就能在 \(O(\text{数组长度})\) 的线性时间内,快速找出所有满足 \(\text{左组和} + \text{右组和} < X\) 的组合。

3. 贡献的批量计算

我们不需要对每个不在 \(S\) 中的箱子 \(v\) 挨个乘概率。因为概率只与子集大小 \(k\) 有关,所以我们可以直接计算当前所有不在 \(S\) 中的箱子的面额总和(即所有箱子总额减去当前子集 \(S\) 的和),然后整体乘以概率 \(P\) 累加进最终答案。

在具体实现时,为了逻辑清晰,可以分两步走:先假设下一步被打开的箱子 \(v\) 在右组,利用双指针统计贡献;然后将左右两组的角色对调,再统计一次下一步选中的箱子 \(v\) 在左组的情况。两轮贡献相加,即为最终的期望值。

代码实现
#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 ll = long long;
using mint = modint998244353;

struct modinv {
  int n; vector<mint> d;
  modinv(): n(2), d({0,1}) {}
  mint operator()(int i) {
    while (n <= i) d.push_back(-d[mint::mod()%n]*(mint::mod()/n)), ++n;
    return d[i];
  }
  mint operator[](int i) const { return d[i];}
} invs;
struct modfact {
  int n; vector<mint> d;
  modfact(): n(2), d({1,1}) {}
  mint operator()(int i) {
    while (n <= i) d.push_back(d.back()*n), ++n;
    return d[i];
  }
  mint operator[](int i) const { return d[i];}
} facts;
struct modfactinv {
  int n; vector<mint> d;
  modfactinv(): n(2), d({1,1}) {}
  mint operator()(int i) {
    while (n <= i) d.push_back(d.back()*invs(n)), ++n;
    return d[i];
  }
  mint operator[](int i) const { return d[i];}
} ifacts;
mint comb(int n, int k) {
  if (n < k || k < 0) return 0;
  return facts(n)*ifacts(k)*ifacts(n-k);
}

int main() {
    int n; ll x;
    cin >> n >> x;
    
    int nl = n/2, nr = n-nl;
    vector<ll> al(nl), ar(nr);
    rep(i, nl) cin >> al[i];
    rep(i, nr) cin >> ar[i];
    
    auto enums = [&](vector<ll> a) {
        int n = a.size();
        vector<vector<ll>> d(n+1);
        d[0] = {0};
        rep(i, n) {
            for (int j = i; j >= 0; --j) {
                for (ll s : d[j]) d[j+1].push_back(s+a[i]);
            }
        }
        rep(i, n+1) ranges::sort(d[i]);
        return d;
    };
    
    auto dl = enums(al);
    auto dr = enums(ar);
    ll sl = 0, sr = 0;
    for (ll x : al) sl += x;
    for (ll x : ar) sr += x;
    
    mint ans;
    rep(ri, 2) {
        rep(i, nl+1)rep(j, nr) {
            mint p = (comb(n, i+j)*(n-i-j)).inv();
            int li = dl[i].size();
            for (ll s : dr[j]) {
                while (li and dl[i][li-1]+s >= x) li--;
                ans += p*li*(sr-s);
            }
        }
        swap(dl, dr);
        swap(nl, nr);
        swap(sl, sr);
    }
    
    cout << ans.val() << '\n';
    
    return 0;
}

G. Celester 2

在字符串开头拼接 \(S\),结尾拼接 \(R\),这样一来,字符串被规范为 \(S\) 块与 \(R\) 块完全交替的结构。此时初始的 RS 对数量固定为 \(\lfloor \frac{\text{字符交替总次数}}{2} \rfloor\)。修改字符的本质变成了“花费代价合并或打破这些连续块”。

将跨越不同块的修改步长提取为代价数组。当序列中出现连续三项满足 \(z \ge y \le x\) 时,根据不相邻选择的贪心原则:

  • \(1\) 个:必选极小值 \(y\),代价为 \(y\)
  • \(2\) 个:必须放弃 \(y\) 而选两边,代价为 \(z + x\)

反悔机制:我们可以放心地把 \(y\) 收入最终代价池。如果以后后悔了想改选 \(z\)\(x\),只需追加 \(\Delta = z + x - y\) 的代价。

利用单调栈从左到右扫描,一旦发现 \(z \ge y \le x\) 结构,就弹出 \(y\) 计入答案,并将 \(z\)\(x\) 合并为新元素 \(z + x - y\) 压回栈中。这成功将 \(O(N \log N)\) 的全局堆贪心优化为了 \(O(N)\) 线性消除。

关于最后答案的计算:

  • 将单调栈浓缩后的独立代价池从小到大排序并求前缀和,得到每多创造一个 RS 对所需的最小累计预算 \(k\)
  • 利用前缀最大值补齐由于代价跳跃产生的空缺,确保高预算能完美继承低预算的最优解。
  • 最后统一加上初始的基础幸福感。

其实就是隐式地在做闵可夫斯基和。

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

using namespace std;
using ll = long long;

void solve() {
    int n;
    string s;
    cin >> n >> s;
    s = "S"+s+"R";
    n++;
    
    vector<int> a(n);
    rep(i, n) a[i] = s[i]!=s[i+1];
    
    vector<int> zero;
    int one = 0;
    rep(i, n) {
        if (a[i]) one++;
        else zero.push_back(i);
    }
    
    const int INF = 1001001001;
    vector<int> d = {INF};
    if (zero.size()) {
        rep(i, zero.size()-1) d.push_back(zero[i+1]-zero[i]);
        d.push_back(INF);
    }
    
    vector<int> ans(n);
    {
        vector<int> s, st;
        for (int x : d) {
            while (st.size() >= 2) {
                int y = st.back(), z = st.end()[-2];
                if (x < y or y > z) break;
                s.push_back(y);
                st.pop_back(); st.pop_back();
                x += z-y;
            }
            st.push_back(x);
        }
        ranges::sort(s);
        int now = 0;
        rep(i, s.size()) {
            now += s[i];
            ans[now] = i+1;
        }
    }
    
    rep(i, n-1) ans[i+1] = max(ans[i+1], ans[i]);
    rep(i, n) ans[i] += one/2;
    rep(i, n) cout << ans[i] << " \n"[i == n-1];
}

int main() {
    int t;
    cin >> t;
    
    while (t--) solve();
    
    return 0;
}