题解: QOJ #7324. Eulerian Orientation

题解: QOJ #7324. Eulerian Orientation

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

题意

给定一个 \(n\) 个顶点、\(m\) 条边的无向图(顶点编号 \(1 \sim n\))。初始所有边都是蓝色。现在考虑所有 \(2^m\) 种给边染红/蓝的方案。对每种方案,如果红色边构成的子图是欧拉图(即每个顶点的度数都是偶数),就将红色边的数量 \(x\) 的平方加到计数器上。求最终计数器的值模 \(10^9+7\)。输入包含多组测试数据,保证所有数据的 \(n\) 之和、\(m\) 之和均不超过 \(2\times 10^5\)

做法

题目要求的是:

\[\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] \]

即对每一对边 \((e,f)\)(有序对),我们要求出同时包含 \(e\)\(f\) 的欧拉子图个数,然后累加。


欧拉子图计数公式

首先,整个图有多少个欧拉子图?有一个经典结论:

对于一个有 \(n\) 个顶点、\(m\) 条边、\(c\) 个连通块的无向图,它的欧拉子图个数为 \(2^{\,m-n+c}\)

直观解释:先取一棵生成树(包含 \(n-c\) 条边),树本身只有一个欧拉子图(空图)。每加入一条非树边,就会与树边形成唯一的一个环,这个环可以独立地选择“选”或“不选”。因为一旦确定了所有非树边的选择(每个非树边对应一个环的选取状态),整个图的边集就唯一确定了(树边的选择由环的选取决定,以保证每个点度数为偶)。所以自由度等于非树边的数量 \(m-(n-c)=m-n+c\),总方案数为 \(2^{m-n+c}\)

严格证明可以用线性代数:图的圈空间维数为 \(m-n+c\),基就是基本环。每个欧拉子图对应圈空间中的一个向量,而圈空间的大小就是 \(2^{\,m-n+c}\)。(此证明来自网络)


分类讨论 \((e,f)\)

\(d = m-n+c\),即自由度数。我们分类计算包含给定边对的欧拉子图个数。

  1. \(e = f\)
    强制选这条边,相当于添加一个线性约束,自由度减少 1。所以个数为 \(2^{d-1}\)

  2. \(e \neq f\)\(\{e,f\}\) 不是割集
    两条边独立,强制选两者会减少两个自由度,个数为 \(2^{d-2}\)

  3. \(e \neq f\)\(\{e,f\}\) 是割集
    此时两条边“绑定”在一起:任何包含 \(e\) 的环必然也包含 \(f\),反之亦然。因此强制选 \(e\)\(f\) 本质上只增加了一个约束(两个约束线性相关),自由度只减少 1,个数为 \(2^{d-1}\)

为什么 \(\{e,f\}\) 是割集会导致约束相关?
因为若 \(\{e,f\}\) 是割集,删除它们会使图不连通。在圈空间中,\(e\)\(f\) 对应的坐标总是同时出现或同时不出现,即它们的取值是相同的,所以两个约束等价。


合并结果

记:

  • \(k\) = 非割边的数量(即权值非零的边数,后面会解释);
  • \(X\) = 构成割集的无序对 \(\{e,f\}\) 的数量。

则:

  • \(e=f\) 的情况贡献 \(m\cdot 2^{d-1}\)
  • \(e\neq f\) 的情况:
    • 非割集对:总无序对数为 \(\binom{k}{2} = \frac{k(k-1)}{2}\),但要注意 \(\{e,f\}\) 是割集的对数为 \(X\),且每条割集对在有序对中会被计算两次,因此非割集的有序对数为 \(k(k-1)-2X\)
    • 非割集对贡献 \((k(k-1)-2X)\cdot 2^{d-2}\)
    • 割集对贡献 \(X\cdot 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}\big[2m + k(k-1)\big] \\ &= 2^{d-2}\left[m + \frac{k(k-1)}{2} + X\right]. \end{aligned} \]

于是问题转化为求 \(k\)\(X\)。其中 \(k\) 容易在后续过程中得到,关键是 \(X\):有多少对边 \(\{e,f\}\) 构成割集?


随机化技巧求 \(X\)

一个经典而巧妙的做法(来自 jiangly):

  1. 给每条非树边随机赋一个 \(64\) 位无符号整数权值。
  2. 对于每条树边,定义它的权值为所有覆盖它的非树边权值的异或。这里“覆盖”指的是:非树边与树边在同一个环中(即该非树边对应的基本环包含这条树边)。
  3. 结论:两条边 \(e,f\) 构成割集当且仅当 \(\text{val}[e] = \text{val}[f]\)

为什么?

  • \(\{e,f\}\) 是割集,那么任何包含 \(e\) 的环必然包含 \(f\),所以 \(e\)\(f\) 被完全相同的非树边集合覆盖(因为每个非树边对应一个基本环,若一个基本环包含 \(e\) 就必包含 \(f\),反之亦然)。因此它们的权值相等。
  • 反之,若 \(\text{val}[e] = \text{val}[f]\),由于随机权值冲突概率极低,几乎可以确定它们被相同的非树边集合覆盖,从而同时出现在所有相同的环中,故 \(\{e,f\}\) 是割集。

因此,我们只需统计每种权值出现的次数 \(c\),那么 \(X = \sum \binom{c}{2}\)


算法实现

  1. DFS 求生成树,同时计算边的权值

    • 维护 xor_sum[u] 表示从 \(u\) 的子树出发的“悬空”非树边的权值异或(即那些一端在子树内、一端在子树外的非树边)。
    • 遇到非树边 \((u,v)\)\(v\) 是祖先),给这条边随机赋值 \(r\),并更新 xor_sum[u] ^= rxor_sum[v] ^= r
    • 回溯时,对于树边 \((parent[u], u)\),它的权值就是 xor_sum[u],同时将 xor_sum[u] 异或到父节点的 xor_sum 中。
    • 注意处理自环:自环也是非树边,直接随机赋值,但不影响 xor_sum(因为自环两端相同,我们通常不把它加入树中,但它的权值参与统计)。

    最终每条边(包括树边和非树边)的权值都存储在 edge_val[] 中。

  2. 统计

    • 统计权值非零的边的个数 \(k\)(权值为零的边是割边,因为没有任何非树边覆盖它)。
    • 用哈希表(如 unordered_map)统计每种权值出现次数,计算 \(X\)
  3. 计算结果

    • 连通块个数 \(c\) 可以在 DFS 时得到。
    • \(d = m - n + c\)
    • 答案 \(= 2^{d-2} \times \big(m + \binom{k}{2} + X\big) \bmod (10^9+7)\)
    • 注意当 \(d<2\) 时需要处理逆元:\(2^{d-2}\) 即乘以 \(2^d\) 再乘以 \(2^{-2}\)。实际上在模意义下,我们直接计算 \(2^{d-2} \equiv 2^d \cdot \text{inv}(4) \pmod{M}\),或者直接用快速幂处理指数可能为负的情况。

代码

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

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

复杂度

  • 时间复杂度 \(O(n+m)\) 每组数据。
  • 空间复杂度 \(O(n+m)\)
  • 随机化使用 \(64\) 位,碰撞概率可忽略。

这样就解决了问题,这个随机化确实牛!

posted @ 2026-02-26 22:11  Super_lollipop  阅读(155)  评论(3)    收藏  举报