C. Black Intervals

考虑翻转前后对答案有什么影响

代码实现
#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> c(n+2);
    
    int ans = 0;
    auto add = [&](int i, int x) {
        if (c[i] == 0 and c[i+1] == 1) ans += x;
    };
    
    rep(qi, q) {
        int a;
        cin >> a;
        
        add(a-1, -1);
        add(a, -1);
        
        c[a] ^= 1;
        
        add(a-1, 1);
        add(a, 1);
        
        cout << ans << '\n';
    }
    
    return 0;
}

D. Conflict 2

从后往前处理操作,追踪服务器的最终字符串是由哪个 \(PC\) 的字符串决定的(通过 \(pc\) 变量),并且只记录那些在形成这个 \(PC\) 字符串的过程中追加的操作 \(2\)。同时,当我们遇到操作 \(1\)(将PC p覆盖为服务器的字符串)时,如果这个 \(p\) 正好是我们正在追踪的\(PC\),那么我们就不能继续追踪这个 \(PC\) 了(因为它的内容被服务器覆盖了,而服务器当时的内容我们还没有确定来源?实际上,在反向处理中,我们还没有处理到更早的操作,所以这个覆盖操作会打断我们当前追踪的链,因此我们将 \(pc\)\(0\),表示这个 \(PC\) 之后的操作不再影响最终结果)。

服务器的最终字符串是由最后一次操作 \(3\) 决定的(或者如果没有操作 \(3\) 则为空)。然后,我们反向追踪这个 \(PC\) 的字符串是如何形成的。在反向处理中,当我们遇到一个操作 \(3\)(将服务器设为某个 \(PC\) 的字符串)时,如果此时 \(pc\)\(0\)(表示还没有确定来源),我们就设置 \(pc\) 为这个 \(PC\),表示我们要追踪这个 \(PC\)。然后,在追踪过程中,我们只记录那些对这个 \(PC\) 的追加操作 \(2\),并且当遇到一个覆盖操作 \(1\) 将这个 \(PC\) 的内容覆盖为服务器的字符串时,我们就停止追踪(因为此时这个 \(PC\) 的内容不再由之前的追加操作决定,而是由当时服务器的内容决定,而当时服务器的内容我们还没有反向追踪到,所以需要中断)。

代码实现
#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<tuple<int, int, string>> qs;
    rep(qi, q) {
        int type, p; string s;
        cin >> type >> p;
        if (type == 2) cin >> s;
        qs.emplace_back(type, p, s);
    }
    
    int pc = 0;
    vector<string> adds;
    ranges::reverse(qs);
    for (auto [type, p, s] : qs) {
        if (type == 1) {
            if (p == pc) pc = 0;
        }
        else if (type == 2) {
            if (pc == p) adds.push_back(s);
        }
        else {
            if (pc == 0) pc = p;
        }
    }
    
    string ans;
    ranges::reverse(adds);
    for (string s : adds) ans += s;
    cout << ans << '\n';
    
    return 0;
}

E. E [max]

贡献法
\(E[\max] = \sum\limits_{x=1}^{10^9} P[\max \geqslant x] = \sum\limits_{x=1}^{10^9} (1 - P[\max < x])\) (期望的积分形式)
发现外面有 \(10^9\) 显然超时,事实上,我们不需要用到这么多数,而只需要用到 \(a\) 中的数,可以将 \(a\) 排序后再按顺序遍历 \(a\) 中的每个值 \(x\),然后利用连续性计算差值,那么 \(x\) 对答案的贡献就是这个概率乘以个数 \(x-pre\)
这样一来,外层循环就退化到了 \(6N\)
但这样还是超时
可以考虑动态维护乘积
cnt[i] 表示每个骰子已经处理过的面数,prod 表示之前已经处理过的 \(cnt[i]\) 的乘积,zero 表示尚未处理的骰子数

代码实现
#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 P = pair<int, int>;
using mint = modint998244353;

int main() {
    int n;
    cin >> n;
    int m = 6;
    
    vector a(n, vector<int>(m));
    rep(i, n)rep(j, m) cin >> a[i][j];
    
    vector<P> xs;
    rep(i, n)rep(j, m) xs.emplace_back(a[i][j], i);
    sort(xs.begin(), xs.end());
    
    mint ans;
    int pre = 0;
    
    mint prod = 1; int zero = n;
    vector<int> cnt(n);
    
    for (auto [x, i] : xs) {
        // mint p = 1;
        // rep(i, n) {
        //     int cnt = 0;
        //     rep(j, m) if (a[i][j] < x) cnt++;
        //     p *= cnt;
        // }
        // p /= mint(m).pow(n);
        
        mint p;
        if (zero == 0) p = prod / mint(m).pow(n);
        ans += (mint(1)-p) * (x-pre);
        pre = x;
        
        if (cnt[i] == 0) zero--; else prod /= cnt[i];
        cnt[i]++;
        prod *= cnt[i];
    }
    
    cout << ans.val() << '\n';
    
    return 0;
}

F. Contraction

启发式合并
当合并顶点时,遵循以下原则:

  • 始终将“棋子数量+邻边数”较少的一方合并到较多的一方
代码实现
#include <bits/stdc++.h>
#define rep(i, n) for (int i = 0; i < (n); ++i)

using namespace std;
using P = pair<int, int>;

int main() {
    int n, m;
    cin >> n >> m;
    
    vector<P> es;
    vector<set<int>> to(n);
    rep(i, m) {
        int a, b;
        cin >> a >> b;
        --a; --b;
        es.emplace_back(a, b);
        to[a].insert(b);
        to[b].insert(a);
    }
    
    vector<int> vid(n);
    vector<vector<int>> vs(n);
    rep(i, n) vid[i] = i, vs[i].push_back(i);
    
    int q;
    cin >> q;
    int ans = m;
    rep(qi, q) {
        int ei;
        cin >> ei;
        --ei;
        auto [a, b] = es[ei];
        a = vid[a]; b = vid[b];
        if (a != b) {
            if (vs[a].size() > vs[b].size()) swap(a, b);
            
            to[a].erase(b);
            to[b].erase(a);
            ans--;
            
            for (int c : to[a]) {
                if (to[b].count(c)) ans--;
                to[c].erase(a);
                to[c].insert(b);
            } 
            
            to[b].merge(to[a]);
            to[a].clear();
            
            for (int v : vs[a]) {
                vid[v] = b;
                vs[b].push_back(v);
            }
            vs[a].clear();
        }
        
        cout << ans << '\n';
    }
    
    return 0;
}

G. Count Cycles

状压dp
考虑删除环里编号最大顶点的一条相邻边,破环城链
dp[S][v] 表示路径上包含的点集为 \(S\) 且路径非最大顶点侧的端点为 \(v\) 的路径数
时间复杂度为 \(O(2^NN^2)\)
注意最后还需对答案除以 \(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;
    cin >> n >> m;
    
    vector g(n, vector<int>(n));
    rep(i, m) {
        int a, b;
        cin >> a >> b;
        --a; --b;
        g[a][b]++; g[b][a]++;
    }
    
    mint ans;
    rep(x, n) {
        int x2 = 1<<x;
        vector dp(x2, vector<mint>(x));
        rep(v, x) dp[1<<v][v] = g[x][v];
        rep(s, x2)rep(v, x) if (s>>v&1) {
            rep(u, x) if (~s>>u&1) {
                dp[s|1<<u][u]+=dp[s][v]*g[v][u];
            }
        }
        rep(s, x2)rep(v, x) if (s>>v&1) {
            if (s == (1<<v)) continue; 
            mint now = dp[s][v]*g[v][x];
            ans += now;
        }
        rep(v, x) {
            ans += mint(g[v][x]-1)*g[v][x];
        }
    }
    ans /= 2;
    
    cout << ans.val() << '\n';
    
    return 0;
}