C. Inc, Dec, Xor
维护数组 \(a\) 与当前异或值 \(\text{ans}\),对单点加一或对所有 \(a_i \geqslant 1\) 减一时分别用 \(\text{ans}←\text{ans}⊕u⊕v\) 局部更新,并只遍历保存 \(a_i>0\) 的下标集合以保证高效。
代码实现
#include <bits/stdc++.h>
#define rep(i, n) for (int i = 0; i < (n); ++i)
using namespace std;
int main() {
int n, q;
cin >> n >> q;
vector<int> a(n);
int ans = 0;
set<int> is;
auto add = [&](int i, int x) {
ans ^= a[i];
a[i] += x;
ans ^= a[i];
};
rep(qi, q) {
int type;
cin >> type;
if (type == 1) {
int x;
cin >> x;
--x;
add(x, 1);
is.insert(x);
}
else {
set<int> nis;
for (int i : is) {
add(i, -1);
if (a[i] > 0) nis.insert(i);
}
is = nis;
}
cout << ans << '\n';
}
return 0;
}
D. Inverse and Swap
维护两个排列 \(P\) 和 \(P'\),其中 \(P'\) 就是记录 \(P\) 对应的下标
代码实现
#include <bits/stdc++.h>
#define rep(i, n) for (int i = 0; i < (n); ++i)
using namespace std;
int main() {
int n, Q;
cin >> n >> Q;
vector<int> p(n), q(n);
rep(i, n) cin >> p[i], --p[i];
rep(i, n) q[p[i]] = i;
rep(qi, Q) {
int type;
cin >> type;
if (type == 1) {
int i, j;
cin >> i >> j;
--i; --j;
swap(p[i], p[j]);
swap(q[p[i]], q[p[j]]);
}
else {
swap(p, q);
}
}
rep(i, n) cout << p[i]+1 << " \n"[i == n-1];
return 0;
}
E. Concentration
由于初始时所有卡牌是完全随机打乱的,任何一对卡牌被成功消除的概率,与它上面的具体数值 \(A_i\) 完全无关。
因此,我们可以把“得分”和“翻牌过程”解耦:
- 最终的期望总得分,等于 “期望成功配对的对数” \(\times\) “所有卡牌数值的平均值 \(\bar{A}\)”。
- 问题直接简化为:在生命值为 \(L\) 时,玩这个记忆翻牌游戏,最优策略下期望最多能成功消除多少对牌?
在最优策略下,只要玩家在桌面上记住了同一对牌的两张位置,就一定会“免费”将它们翻开消除(不消耗生命值)。因此,我们不需要在状态里记录这种“成对已知”的卡牌。
在任意时刻,桌面上的牌对只分为两种状态:
- 完全未知对(设为 \(i_2\)):两张牌都处于未翻开状态。
- 单张已知对(设为 \(i_1\)):之前翻错时记下了其中 1 张的位置和数值,另 1 张完全未知。
由此,我们可以设计出 \(\text{dp}\) 状态:
记 dp[l][i2][i1] 表示在剩余生命为 \(l\)、有 \(i_2\) 对完全未知牌、\(i_1\) 对单张已知牌时,后续期望还能获得的成功配对数。
然后跑记忆化搜索即可
代码实现
#include <bits/stdc++.h>
#define rep(i, n) for (int i = 0; i < (n); ++i)
using namespace std;
bool done[201][201][201];
double dp[201][201][201];
int main() {
int n, l;
cin >> n >> l;
vector<int> a(n);
rep(i, n) cin >> a[i];
double ave = 0;
rep(i, n) ave += a[i];
ave /= n;
auto f = [&](this auto f, int l, int i2, int i1) -> double {
if (done[l][i2][i1]) return dp[l][i2][i1];
if (l == 0) return 0;
if (i2 == 0 and i1 == 0) return 0;
double res = 0;
double p = 1./(i2*2+i1), q = 1./(i2*2+i1-1);
if (i1) {// 1
res += (p*i1)*(f(l, i2, i1-1)+1);
}
if (i2 and i1) {// 2,1
res += (p*i2*2)*(q*i1)*(f(l-1, i2-1, i1)+(l==1?0:1));
}
if (i2) {// 2,2 =
res += (p*i2*2)*(q*1)*(f(l, i2-1, i1)+1);
}
if (i2 >= 2) {// 2,2 !=
res += (p*i2*2)*(q*(i2*2-2))*f(l-1, i2-2, i1+2);
}
done[l][i2][i1] = true;
return dp[l][i2][i1] = res;
};
double ans = f(l, n, 0)*ave;
printf("%.10f\n", ans);
return 0;
}
F. Googol Swaps
将可以相互交换的位置连无向边。根据置换群性质,只要一个点集在图上是连通的,并且进行足够多次交换(\(10^{100}\) 远大于 \(N\)),该连通块内的所有字符就可以被任意重排。
因此,字符串 \(S\) 被分割成了多个独立的“字符池”(连通块),块与块之间的字符无法互换,而块内部的字符可以任意重排。
如果没有任何限制,对于每个连通块 \(C\):
- 设其包含的节点总数为 \(\vert{}C\vert{}\);
- 块内不同字符 \(c\) 出现的次数为 \(\text{cnt}(c)\)。
该连通块内部能生成的不重复字符串数量为:$$P(C) = \frac{\vert{}C\vert{}!}{\prod \text{cnt}(c)!}$$所有连通块相互独立,总的可能字符串数为各块之积:\(\prod P(C)\)。
题目要求恰好执行 \(10^{100}\) 次交换。由于 \(10^{100}\) 是一个偶数,每次交换(对换)都会改变置换的奇偶性,因此最终通过偶数次交换达到的置换必须是一个偶置换。
这会对最终能看到的字符串产生什么影响?
-
情况 A:存在至少一个连通块包含相同的字符(即存在 \(\text{cnt}(c) \ge 2\))
- 我们可以对这两个相同的字符进行一次交换。
- 这次交换消耗了 \(1\) 次操作(将置换的奇偶性翻转),但导出的可见字符串完全没变。
- 这意味着:任何奇置换生成的字符串,都可以通过一次“无意义的相同字符交换”转化为偶置换生成。
- 结论:所有的多重集排列均可被偶数次交换达到,答案就是 \(\prod P(C)\)。
-
情况 B:所有连通块内部的字符都互不相同(所有 \(\text{cnt}(c) \le 1\))
- 此时每一次交换都会改变可见的字符串,且必定翻转置换奇偶性。
- 在所有可能的 \(P(C)\) 种排列中,奇置换与偶置换构成的可见字符串数量恰好各占一半。
- 由于我们只能通过偶数次交换达到偶置换,因此一半的排列是无法到达的。
- 结论:答案为 \(\frac{\prod P(C)}{2}\)。
代码实现
#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, m;
string s;
cin >> n >> m >> s;
dsu uf(n);
rep(i, m) {
int a, b;
cin >> a >> b;
--a; --b;
uf.merge(a, b);
}
vector<map<char, int>> mp(n);
rep(i, n) mp[uf.leader(i)][s[i]]++;
vector<mint> facs(n+1, 1);
rep(i, n) facs[i+1] = facs[i]*(i+1);
mint ans = 1;
int div = 2;
rep(i, n) {
auto& d = mp[i];
if (d.size() == 0) continue;
int num = 0;
for (auto [c, x] : d) {
if (x >= 2) div = 1;
ans /= facs[x];
num += x;
}
ans *= facs[num];
}
ans /= div;
cout << ans.val() << '\n';
return 0;
}
G. ΣШX
固定左端点 \(l=1\):先计算出所有右端点 \(r \in [1, N]\) 的 \(\text{mex}(A_1 \dots A_r)\) 值,记为数组 \(M[r]\)。注意:\(M[r]\) 随 \(r\) 的增加是单调递增的。
移动左端点 \(l\):当左端点从 \(l\) 移动到 \(l+1\) 时,相当于从区间中删除了元素 \(x = A_l\)。
- \(x\) 的删除只会影响右端点在 \([l, \text{nxt}[l] - 1]\) 范围内的 \(\text{mex}\) 值(其中 \(\text{nxt}[l]\) 为下一个值为 \(x\) 的位置)。
- 对于该区间内原先 \(M[r] > x\) 的位置,\(M[r]\) 会降低为 \(x\)。
- 由于 \(M[r]\) 的单调性,区间内大于 \(x\) 的位置必定是一段连续的区间。
使用线段树维护数组 \(M\) 的区间和与最大值。每次移动 \(l\) 时,在线段树上二分找到第一个 \(> x\) 的位置,然后执行区间修改(把大于 \(x\) 的项全部赋值为 \(x\))。
其实也可以用珂朵莉树实现
ps:这题有一个小彩蛋,赛时的标题是 \(\sum ex\),这里的 \(\sum\) 恰好是把 \(M\) 逆时针旋转 \(90°\) 得到的。由于管理员审核的时候没注意到这个,赛后有人指出了,然后就有了现在这个标题。
代码实现
#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;
struct S {
ll sum;
int mx, w;
};
S op(S a, S b) {
return {a.sum+b.sum, max(a.mx, b.mx), a.w+b.w};
}
S e() {
return {0, -1, 0};
}
S mapping(int f, S x) {
if (f == -1) return x;
return {(ll)f*x.w, f, x.w};
}
int composition(int f, int g) {
if (f == -1) return g;
return f;
}
int id() {
return -1;
}
int main() {
int n;
cin >> n;
vector<int> a(n);
rep(i, n) cin >> a[i];
vector<int> m(n);
vector<bool> vis(n+2);
int mex = 0;
rep(i, n) {
vis[a[i]] = true;
while (vis[mex]) ++mex;
m[i] = mex;
}
vector<int> nxt(n);
vector<int> last(n+2, n);
for (int i = n-1; i >= 0; --i) {
nxt[i] = last[a[i]];
last[a[i]] = i;
}
vector<S> init(n);
rep(i, n) init[i] = {m[i], m[i], 1};
lazy_segtree<S, op, e, int, mapping, composition, id> seg(init);
ll ans = 0;
rep(l, n) {
ans += seg.prod(l, n).sum;
int x = a[l];
int r = nxt[l]-1;
int p = seg.max_right(l, [&](S s) { return s.mx <= x; });
if (p <= r) {
seg.apply(p, r+1, x);
}
}
cout << ans << '\n';
return 0;
}
代码实现2
#include <bits/stdc++.h>
#define rep(i, n) for (int i = 0; i < (n); ++i)
using namespace std;
using ll = long long;
map<int, int> mp;
auto split(int x) {
auto it = mp.upper_bound(x);
--it;
if (it->first == x) return it;
return mp.insert({x, it->second}).first;
}
int get(int x) {
return prev(mp.upper_bound(x))->second;
}
int main() {
int n;
cin >> n;
vector<int> a(n);
rep(i, n) cin >> a[i];
vector<int> m(n);
vector<bool> vis(n+2);
int mex = 0;
rep(i, n) {
vis[a[i]] = true;
while (vis[mex]) ++mex;
m[i] = mex;
}
vector<int> nxt(n);
vector<int> last(n+2, n);
for (int i = n-1; i >= 0; --i) {
nxt[i] = last[a[i]];
last[a[i]] = i;
}
ll now = 0;
rep(i, n) {
now += m[i];
if (i == 0 or m[i] != m[i-1]) {
mp[i] = m[i];
}
}
mp[n] = 0;
ll ans = 0;
rep(l, n) {
ans += now;
int x = a[l];
int r = nxt[l]-1;
int ac = r+1, wa = l-1;
while (ac-wa > 1) {
int wj = (ac+wa)/2;
if (get(wj) > x) ac = wj; else wa = wj;
}
int k = ac;
if (k <= r) {
auto itr = split(r+1);
auto itl = split(k);
for (auto it = itl; it != itr; ++it) {
int nl = it->first;
int nr = next(it)->first-1;
int nx = it->second;
now -= ll(nr-nl+1)*nx;
}
mp.erase(itl, itr);
mp[k] = x;
now += ll(r-k+1)*x;
}
now -= get(l);
}
cout << ans << '\n';
return 0;
}
浙公网安备 33010602011771号