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;
}