QOJ #7324. Eulerian Orientation 题解

QOJ #7324. Eulerian Orientation 题解

感觉比较牛的题,可能是我比较菜,学习了官解才学会。

题意

对于一个有 \(n\) 个顶点、\(m\) 条边的无向图,考虑所有 \(2^m\) 种给边染色(红/蓝)的方案。对于每种方案,如果红色边构成的子图是欧拉图(即每个顶点的度数都是偶数),那么就将红色边的数量 \(x\) 的平方加到计数器上。最后输出所有方案的总和模 \(10^9+7\)。

做法

题目其实让求的就是

\[\sum_{F \subseteq E} [F \text{ 是欧拉子图}] \cdot |F|^2 \]

其中 \(|F|\) 是红色边的数量。

\[|F|^2 = \left(\sum_{e \in E} [e \in F]\right)^2 = \sum_{e \in E}\sum_{f \in E} [e \in F][f \in F] \]

总和就变成:

\[\sum_{e \in E}\sum_{f \in E} \sum_{F \subseteq E} [F \text{ 是欧拉子图} \land e \in F \land f \in F] \]

交换一下求和顺序就变成:

\[\sum_{e \in E}\sum_{f \in E} (\text{包含 } e \text{ 和 } f \text{ 的欧拉子图数量}) \]

好的,问题变成怎么求一个图的欧拉子图数量?

考虑对于一棵树,他的欧拉子图个数是 \(1\),也就是空集,这个比较显然。

我们考虑加上一条非树边。

此时发现会多一个环,变成两个欧拉子图。

再加一条呢?

此时就会有 \(4\) 个欧拉子图。

好的,我们发现规律了。

对于一个连通图来说,他的欧拉子图个数为 \(2^{m-n+1}\) 个。

非连通图的欧拉子图个数为 \(2^{m-n+c}\) 个

考虑证明:

直观来看,每加一条非树边就会使树上加一个新环,这个环是确定的,我们称他会增加一个自由度。

所以可以选择或者不选择这条非树边,有两种方案。每选择一条非树边都有且确认了欧拉图。

好的,证完了。

然后我们开始分讨上式中的 \(e\) 和 \(f\)。

  • 当 \(e = f\) 的时候我们要求必须选 \(e/f\) 这一条边,减少一个自由度,答案为 \(2^{m-n+c-1}\)。

  • 当 \(e \neq f\) 且 \(\left\{e, f\right\}\) 不是割集,强制选这两个边只会减少两个自由度。

  • 等 \(e \neq f\) 且 \(\left\{e, f\right\}\) 是割集,此时 \(e\) 和 \(f\) 在同一个环上,他俩会同时选,强制选他只会使自由度减少一。

合并一下结果:

令:

  • \(d = m - n + c\)
  • \(k =\) 非割边的数量(即环上的边)
  • \(X =\) 构成割集的无序对 \(\{e,f\}\) 的数量

那么:

  • \(e = f\) 的情况贡献:\(m \cdot 2^{d-1}\)
  • \(e \neq f\) 的情况:
    • 非割集对 \((e,f)\) 的数量 \(= k(k-1) - 2X\)
    • 这些对贡献:\(2^{d-2}\)
    • 割集对 \((e,f)\) 的数量 \(= X\)
    • 这些对贡献:\(2^{d-1}\)

总结果:

\[\begin{aligned} \text{总和} &= m \cdot 2^{d-1} + (k(k-1)-2X) \cdot 2^{d-2} + X \cdot 2^{d-1} \\ &= 2^{d-2}[2m + k(k-1)] \\ &= 2^{d-2}\left[m + \frac{k(k-1)}{2} + X\right] \end{aligned}\]

剩下就是怎么求 \(X\) 了。

jiangly 给出的方法:

  1. 给每条非树边随机一个权值
  2. 树边的权值设为覆盖它的所有非树边权值的异或
  3. 性质:\(\{e,f\}\) 是割集当且仅当 \(\text{val}[e] = \text{val}[f]\)

什么叫做一个树边被一个非树边覆盖呢:这个非树边和这个树边在同一个环上。

好了,剩下的就是代码了,我实在懒得写了,让 AI 写的嘻嘻。

cpp 复制代码
#include <bits/stdc++.h>
using namespace std;

using ll = long long;
using ull = unsigned long long;
const int MOD = 1e9 + 7;
const int MAXN = 200005;
const int MAXM = 200005;

int n, m;
vector<pair<int, int>> adj[MAXN];  // (to, edge id)
int u[MAXM], v[MAXM];
ull edge_val[MAXM];
bool vis[MAXN];
int depth[MAXN];
ull xor_sum[MAXN];
int parent[MAXN], pe[MAXN];  // parent vertex and parent edge id
int iter[MAXN];              // current index in adjacency list

mt19937_64 rng(chrono::steady_clock::now().time_since_epoch().count());

ull rand64() {
    return rng();
}

void solve() {
    // input edges
    for (int i = 0; i < m; ++i) {
        cin >> u[i] >> v[i];
        adj[u[i]].emplace_back(v[i], i);
        adj[v[i]].emplace_back(u[i], i);
    }

    // initialize
    for (int i = 1; i <= n; ++i) vis[i] = false;
    for (int i = 0; i < m; ++i) edge_val[i] = 0;

    int components = 0;
    // iterative DFS for each component
    for (int start = 1; start <= n; ++start) {
        if (vis[start]) continue;
        ++components;

        // stack for DFS
        vector<int> stk;
        stk.push_back(start);
        parent[start] = -1;
        pe[start] = -1;
        depth[start] = 0;
        xor_sum[start] = 0;
        vis[start] = true;
        iter[start] = 0;

        while (!stk.empty()) {
            int uu = stk.back();
            int &idx = iter[uu];

            if (idx < (int)adj[uu].size()) {
                auto [ww, eid] = adj[uu][idx];
                ++idx;

                // skip parent edge
                if (eid == pe[uu]) continue;

                // self-loop
                if (ww == uu) {
                    if (edge_val[eid] == 0) edge_val[eid] = rand64();
                    continue;
                }

                if (!vis[ww]) {
                    // tree edge
                    vis[ww] = true;
                    parent[ww] = uu;
                    pe[ww] = eid;
                    depth[ww] = depth[uu] + 1;
                    xor_sum[ww] = 0;
                    iter[ww] = 0;
                    stk.push_back(ww);
                } else {
                    // back edge (only from deeper to shallower)
                    if (depth[ww] < depth[uu]) {
                        ull r = rand64();
                        edge_val[eid] = r;
                        xor_sum[uu] ^= r;
                        xor_sum[ww] ^= r;
                    }
                }
            } else {
                // backtrack
                stk.pop_back();
                if (!stk.empty()) {
                    int p = stk.back();
                    int peid = pe[uu];
                    if (peid != -1) {
                        edge_val[peid] = xor_sum[uu];
                        xor_sum[p] ^= xor_sum[uu];
                    }
                }
            }
        }
    }

    // count non-bridge edges
    int k = 0;
    unordered_map<ull, int> cnt;
    for (int i = 0; i < m; ++i) {
        if (edge_val[i] != 0) {
            ++k;
            ++cnt[edge_val[i]];
        }
    }

    // number of unordered pairs with same hash (X)
    ll X = 0;
    for (auto &p : cnt) {
        ll c = p.second;
        X += c * (c - 1) / 2;
    }

    // d = m - n + components
    ll d = m - n + components;
    if (d == 0) {
        cout << 0 << '\n';
        return;
    }

    // T = 2^d mod MOD
    ll T = 1, base = 2, exp = d;
    while (exp) {
        if (exp & 1) T = T * base % MOD;
        base = base * base % MOD;
        exp >>= 1;
    }

    ll inv2 = (MOD + 1) / 2;
    ll term = (k % MOD + (ll)k * (k - 1) % MOD * inv2 % MOD + X % MOD) % MOD;
    ll ans = T * inv2 % MOD * term % MOD;
    cout << ans << '\n';
}

int main() {
    ios::sync_with_stdio(false);
    cin.tie(0);

    while (cin >> n >> m) {
        solve();
        // clear adjacency for next test case
        for (int i = 1; i <= n; ++i) adj[i].clear();
    }
    return 0;
}