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\) 的概率:
- 在 \(N\) 个箱子里无序地选出 \(k\) 个,一共有 \(\binom{N}{k}\) 种可能,选中特定集合 \(S\) 的概率是 \(\frac{1}{\binom{N}{k}}\)。
- 第 \(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;
}
浙公网安备 33010602011771号