[CF19E]Fairy 题解
[CF19E]Fairy 题解
给出一张无向图,求删除这边后此图变成二分图的所有边。
思路
首先考虑二分图的真谛是什么,可以发现,如果一个图里面没有奇环,那么他就是一个二分图,实际上,这是充分必要的。
接着结合 DFS 树思考,可以发现:
-
对于树上的所有回边,他能产生贡献,当且仅当这棵树里只有一个奇环。
-
对于一条树边,他能产生贡献的一个必要条件是这棵树里面的所有奇环都经过它,但是这不是充分的。要想让他是充分的,还需要加上一个必要条件:没有偶环经过。
证明:
所有奇环经过且没有偶环经过的可以构成二分图。
考虑当前边连接的上半部分和下半部分,所有的奇环都跨越两部分,因此所有的奇环边两端都是同色的,删除这条边之后把下半部分的所有点颜色取反,那么原本合法的边依旧合法,不合法的奇环边也合法。
边删掉之后可以构成二分图必须要所有奇环经过且没有偶环经过。
必须要经过所有奇环显然,假设有偶环经过且构成二分图。有一个偶环和一个奇环同时经过当前边,那么这个边删掉之后偶环变成奇环,奇环变成偶环,它们连起来形成了一个奇环,矛盾,假设不成立。
既然如此,那么只需要判断一条边是否被所有奇环经过,且没有被任何偶环经过,这个显然可以用树上差分轻松解决,不过另一种巧妙的方法是类比 Tarjan 算法中的 low 函数。
令
偶环可以类似转移。
再次梳理算法步骤:
- 判断是否有至少两个奇环在不同的连通分量,如果是则无解
- 判断是否没有奇环,如果是直接输出所有边
- 对于拥有所有奇环的那个连通分量,计算每条边被覆盖的次数,对于回边,判断它是否是唯一的一条奇环回边;对于树边,判断它是否所有奇环经过且没有偶环经过。
代码实现有一些难,不过时间复杂度为极为优秀的线性
// Problem: Fairy
// Contest: Luogu
// Author: Moyou
// Copyright (c) 2023 Moyou All rights reserved.
// Date: 2023-09-20 00:16:38
#include <algorithm>
#include <cstring>
#include <iostream>
#include <queue>
using namespace std;
const int N = 1e4 + 10, M = 2e4 + 10;
int n, m;
int h[N], ne[M], e[M], idx, cnt0[N], cnt1[N], dep[N];
int back[M], from[M];
void add(int a, int b) {
e[idx] = b, from[idx] = a, ne[idx] = h[a], h[a] = idx++;
}
int tot[2][N], ance[N], only;
void dfs(int u, int lst, int fa, int anc) {
dep[u] = dep[fa] + 1;
ance[u] = anc;
for(int i = h[u]; ~i; i = ne[i]) {
int v = e[i];
if(!dep[v]) back[i] = 2, dfs(v, i, u, anc), cnt0[u] += cnt0[v], cnt1[u] += cnt1[v]; // back i = 2 树边
else if(i != (lst ^ 1) && dep[v] < dep[u]) {
back[i] = 1; // 回边
if(dep[v] + dep[u] & 1) tot[1][anc] ++, cnt1[u] ++, cnt1[v] --; // 偶环
else only = i, cnt0[u] ++, cnt0[v] --, tot[0][anc] ++; // 奇环
}
}
}
int root[N], cnt;
vector<int> ans;
signed main() {
ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
cin >> n >> m;
memset(h, -1, sizeof h);
for(int i = 1, a, b; i <= m; i ++) {
cin >> a >> b;
add(a, b), add(b, a);
}
for(int i = 1; i <= n; i ++)
if(!dep[i]) dfs(i, -1, i, i), root[++ cnt] = i;
int flg = 0;
for(int i = 1; i <= cnt; i ++) {
if(flg && tot[0][root[i]]) return cout << 0 << '\n', 0;
if(tot[0][root[i]]) flg = root[i];
} // flg => 那个特殊连通分量的根
if(flg == 0) {
cout << m << '\n';
for(int i = 1; i <= m; i ++) cout << i << ' ';
return 0;
}
for(int i = 0; i < idx; i ++) {
if(ance[e[i]] == flg) { // 找所有那个连通分量里的边
int u = e[i];
if(tot[0][flg] == 1 && i == only) ans.push_back(i / 2 + 1); // 回边
else if(back[i] == 2 && cnt0[u] == tot[0][flg] && (cnt1[u] == 0)) ans.push_back(i / 2 + 1); // 树边
}
}
sort(ans.begin(), ans.end());
cout << ans.size() << '\n';
for(auto x : ans) cout << x << ' ';
return 0;
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 无需6万激活码!GitHub神秘组织3小时极速复刻Manus,手把手教你使用OpenManus搭建本
· Manus爆火,是硬核还是营销?
· 终于写完轮子一部分:tcp代理 了,记录一下
· 别再用vector<bool>了!Google高级工程师:这可能是STL最大的设计失误
· 单元测试从入门到精通