题解: 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\)。
做法
题目要求的是:
其中 \(|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\),即自由度数。我们分类计算包含给定边对的欧拉子图个数。
-
\(e = f\)
强制选这条边,相当于添加一个线性约束,自由度减少 1。所以个数为 \(2^{d-1}\)。 -
\(e \neq f\) 且 \(\{e,f\}\) 不是割集
两条边独立,强制选两者会减少两个自由度,个数为 \(2^{d-2}\)。 -
\(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}\)。
总结果:
于是问题转化为求 \(k\) 和 \(X\)。其中 \(k\) 容易在后续过程中得到,关键是 \(X\):有多少对边 \(\{e,f\}\) 构成割集?
随机化技巧求 \(X\)
一个经典而巧妙的做法(来自 jiangly):
- 给每条非树边随机赋一个 \(64\) 位无符号整数权值。
- 对于每条树边,定义它的权值为所有覆盖它的非树边权值的异或。这里“覆盖”指的是:非树边与树边在同一个环中(即该非树边对应的基本环包含这条树边)。
- 结论:两条边 \(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}\)。
算法实现
-
DFS 求生成树,同时计算边的权值
- 维护
xor_sum[u]表示从 \(u\) 的子树出发的“悬空”非树边的权值异或(即那些一端在子树内、一端在子树外的非树边)。 - 遇到非树边 \((u,v)\)(\(v\) 是祖先),给这条边随机赋值 \(r\),并更新
xor_sum[u] ^= r,xor_sum[v] ^= r。 - 回溯时,对于树边 \((parent[u], u)\),它的权值就是
xor_sum[u],同时将xor_sum[u]异或到父节点的xor_sum中。 - 注意处理自环:自环也是非树边,直接随机赋值,但不影响
xor_sum(因为自环两端相同,我们通常不把它加入树中,但它的权值参与统计)。
最终每条边(包括树边和非树边)的权值都存储在
edge_val[]中。 - 维护
-
统计
- 统计权值非零的边的个数 \(k\)(权值为零的边是割边,因为没有任何非树边覆盖它)。
- 用哈希表(如
unordered_map)统计每种权值出现次数,计算 \(X\)。
-
计算结果
- 连通块个数 \(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\) 位,碰撞概率可忽略。
这样就解决了问题,这个随机化确实牛!

浙公网安备 33010602011771号