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 给出的方法:
- 给每条非树边随机一个权值
- 树边的权值设为覆盖它的所有非树边权值的异或
- 性质:\(\{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;
}