图论找环
因为ABC472-E-Odd Cycle差点翻车所以有了这篇博客
C++ 图论找环完全指南
目录
基础概念
1.1 图的基本定义
图(Graph) 是由顶点集合 \(V\) 和边集合 \(E\) 组成的数学结构,表示为 \(G = (V, E)\)。
- 顶点(Vertex):图中的基本元素,也称为节点(Node),记作 \(v \in V\)
- 边(Edge):连接两个顶点的线,表示它们之间的关系,记作 \(e = (u, v) \in E\)
- 路径(Path):顶点序列 \(v_0, v_1, v_2, ..., v_k\),其中对于所有 \(i \in [0, k-1]\),\((v_i, v_{i+1}) \in E\)
- 环(Cycle):从某个顶点出发,经过一系列不重复的边,最终回到起始顶点的路径,即 \(v_0 = v_k\),且 \(v_0, v_1, ..., v_{k-1}\) 互不相同
- 简单环(Simple Cycle):除了起点和终点相同外,没有重复顶点的环
1.2 环的数学表示
一个环可以用顶点序列表示为:
其中 \(k \geq 3\),且对于所有 \(i \in [0, k-1]\),边 \((v_i, v_{i+1}) \in E\)(下标模 \(k\))。
环的长度定义为边的数量,即 \(|C| = k\)。
1.3 环的分类
- 奇数环(Odd Cycle):长度为奇数的环,即 \(k \equiv 1 \pmod{2}\)
- 偶数环(Even Cycle):长度为偶数的环,即 \(k \equiv 0 \pmod{2}\)
- 自环(Self-loop):连接顶点到自身的边,即 \((v, v) \in E\)
- 重边(Multiple Edge):两个顶点之间有多条边
1.4 图论中的基本定理
定理1(二分图判定定理):一个图是二分图当且仅当它不包含奇数环。
证明:
- 充分性:假设图 \(G\) 不包含奇数环,则可以用二染色法给所有顶点染色,使得相邻顶点颜色不同,因此 \(G\) 是二分图。
- 必要性:假设 \(G\) 是二分图,则顶点可以分成两个集合 \(A\) 和 \(B\),所有边都在 \(A\) 和 \(B\) 之间。任何环在 \(A\) 和 \(B\) 之间交替,因此长度必为偶数。
定理2(Kőnig定理):在二分图中,最大匹配数等于最小顶点覆盖数。
定理3(欧拉公式):对于连通平面图,\(V - E + F = 2\),其中 \(V\) 为顶点数,\(E\) 为边数,\(F\) 为面数。
定理4(握手定理):图中所有顶点的度数之和等于边数的两倍。
定理5(Menger定理):图中连接两个不相邻顶点的最小点割集的大小等于这两个顶点之间不相交路径的最大数量。
定理6(环空间定理):图 \(G\) 的环空间维数等于 \(|E| - |V| + c\),其中 \(c\) 为连通分量个数。
证明:对于连通图,生成树有 \(|V|-1\) 条边,非树边有 \(|E|-|V|+1\) 条,每条非树边对应一个基本环,这些基本环构成环空间的一组基。
定理7(奇偶环定理):一个图是二分图当且仅当它不包含奇数环。
推论1:如果一个图包含奇数环,则它不是二分图。
推论2:任何无向图都可以分解为若干个环和树的并集。
推论3:图 \(G\) 的色数 \(\chi(G) \geq 3\) 当且仅当 \(G\) 包含奇数环。
定理8(Tutte定理):图 \(G\) 有完美匹配当且仅当对于任意顶点子集 \(S\),\(G-S\) 的奇连通分量数 \(\leq |S|\)。
定理9(Erdős–Rényi随机图):在随机图 \(G(n,p)\) 中,当 \(p > \frac{1}{n}\) 时,几乎必然存在环。
定理10(无标度网络):BA模型中,随着网络增长,环的数量呈幂律分布。
图的基本类型与表示
2.1 图的基本类型
- 无向图(Undirected Graph):边没有方向,\((u, v) = (v, u)\)
- 有向图(Directed Graph):边有方向,\((u, v) \neq (v, u)\)
- 加权图(Weighted Graph):边带有权值 \(w: E \rightarrow \mathbb{R}\)
- 简单图(Simple Graph):没有自环和重边
- 完全图(Complete Graph):任意两个顶点之间都有边,\(|E| = \binom{n}{2}\)
- 稀疏图(Sparse Graph):\(|E| = O(|V|)\)
- 稠密图(Dense Graph):\(|E| = O(|V|^2)\)
2.2 图的存储方式
2.2.1 邻接矩阵
邻接矩阵 \(A\) 是一个 \(n \times n\) 的矩阵,其中:
const int MAXN=1005;
bool adj[MAXN][MAXN];
// 或者使用vector
vector<vector<int>> adj(n, vector<int>(n, 0));
时间复杂度:
- 查询边:\(O(1)\)
- 遍历邻接点:\(O(n)\)
- 空间复杂度:\(O(n^2)\)
2.2.2 邻接表
邻接表存储每个顶点的邻接点列表。
vector<int> G[N]; // 无向图
vector<pair<int,int>> G[N]; // 带权图
时间复杂度:
- 查询边:\(O(\deg(u))\)
- 遍历邻接点:\(O(\deg(u))\)
- 空间复杂度:\(O(n + m)\)
2.2.3 边集数组
struct Edge{
int u,v,w;
}edges[M];
时间复杂度:
- 遍历所有边:\(O(m)\)
- 空间复杂度:\(O(m)\)
找环的基础算法(含输出环)
3.1 DFS找环(无向图)
算法原理
使用深度优先搜索(DFS)遍历图。在DFS过程中,顶点有三种状态:
- 未访问(0):还未被DFS访问
- 访问中(1):当前正在DFS栈中
- 已访问(2):已完成DFS
当从当前顶点 \(u\) 探索到邻接点 \(v\) 时:
- 如果 \(v\) 是父节点,跳过
- 如果 \(v\) 未访问,继续DFS
- 如果 \(v\) 正在访问中,说明找到了一个环
找到环后,通过父指针数组 \(parent\) 回溯,可以重构出环的具体路径。
算法步骤
- 从任意顶点开始DFS
- 标记当前顶点为访问中
- 遍历所有邻接点
- 如果邻接点未访问,设置父指针并递归
- 如果邻接点在访问中且不是父节点,找到环
- 通过父指针回溯重构环
时间复杂度
- 每个顶点访问一次:\(O(V)\)
- 每条边检查一次:\(O(E)\)
- 总时间复杂度:\(O(V + E)\)
- 空间复杂度:\(O(V)\)
const int N=2e5+5;
vector<int> G[N];
int vis[N]; // 0:未访问, 1:访问中, 2:已访问
int parent[N];
vector<int> cycle;
bool dfs_find_cycle(int u){
vis[u]=1;
for(int v:G[u]){
if(v==parent[u]) continue;
if(vis[v]==0){
parent[v]=u;
if(dfs_find_cycle(v)) return true;
}else if(vis[v]==1){
// 找到环,重构路径
cycle.clear();
int cur=u;
while(cur!=v){
cycle.push_back(cur);
cur=parent[cur];
}
cycle.push_back(v);
reverse(cycle.begin(), cycle.end());
// 添加起始顶点完成环
cycle.push_back(cycle[0]);
return true;
}
}
vis[u]=2;
return false;
}
// 输出找到的环
void print_cycle(){
if(cycle.empty()){
cout<<"图中没有环"<<endl;
return;
}
cout<<"找到环:";
for(int i=0;i<cycle.size();i++){
cout<<cycle[i];
if(i<cycle.size()-1) cout<<" -> ";
}
cout<<endl;
cout<<"环的长度为:"<<cycle.size()-1<<endl;
}
3.2 拓扑排序找环(有向图)
算法原理
拓扑排序是DAG(有向无环图)的线性排序。对于有向图,如果存在环,则无法完成拓扑排序。
算法维护每个顶点的入度 \(in\_degree[v]\):
- 将所有入度为0的顶点入队
- 从队列中取出顶点 \(u\),将其从图中移除
- 对于每个邻接点 \(v\),\(in\_degree[v]--\)
- 如果 \(in\_degree[v]\) 变为0,入队
- 如果处理完所有可达顶点后,还有顶点未被处理,则存在环
环的识别:未被处理的顶点一定在环中。从环中任意顶点出发,沿着未删除的边走,可以重构出环。
时间复杂度
- 每个顶点和边处理一次:\(O(V + E)\)
- 空间复杂度:\(O(V)\)
vector<int> topo_find_cycle(int n){
vector<int> in_degree(n+1,0);
queue<int> q;
vector<int> removed(n+1,0);
for(int i=1;i<=n;i++){
for(int v:G[i]) in_degree[v]++;
}
for(int i=1;i<=n;i++){
if(in_degree[i]==0){
q.push(i);
removed[i]=1;
}
}
vector<int> topo;
while(!q.empty()){
int u=q.front();q.pop();
topo.push_back(u);
for(int v:G[u]){
in_degree[v]--;
if(in_degree[v]==0){
q.push(v);
removed[v]=1;
}
}
}
// 找出环中的顶点
vector<int> cycle_vertices;
for(int i=1;i<=n;i++){
if(!removed[i]) cycle_vertices.push_back(i);
}
if(!cycle_vertices.empty()){
// 重构环
vector<int> result_cycle;
int start=cycle_vertices[0];
int cur=start;
do{
result_cycle.push_back(cur);
for(int v:G[cur]){
if(!removed[v]){
cur=v;
break;
}
}
}while(cur!=start);
result_cycle.push_back(start);
return result_cycle;
}
return vector<int>();
}
void print_directed_cycle(vector<int>& cycle){
if(cycle.empty()){
cout<<"有向图中没有环"<<endl;
return;
}
cout<<"有向图中找到环:";
for(int i=0;i<cycle.size();i++){
cout<<cycle[i];
if(i<cycle.size()-1) cout<<" -> ";
}
cout<<endl;
}
3.3 并查集找环
算法原理
并查集(Union-Find)是一种维护集合的数据结构。对于无向图,当添加边 \((u, v)\) 时:
- 如果 \(u\) 和 \(v\) 不在同一集合中,合并它们
- 如果 \(u\) 和 \(v\) 已经在同一集合中,说明添加这条边会形成环
环的重构:在添加环边时,记录这最后一条边。在此之前,并查集维护了一个生成森林。在生成森林中,从 \(u\) 到 \(v\) 的路径加上边 \((u, v)\) 就构成了一个环。
并查集优化
- 路径压缩:在查找时,将路径上的节点直接连接到根节点
- 按秩合并:将小的树合并到大的树上,保持树的高度平衡
时间复杂度
- 并查集操作:\(O(\alpha(V))\),其中 \(\alpha\) 是阿克曼函数的反函数
- 总时间复杂度:\(O(E \cdot \alpha(V))\)
- 空间复杂度:\(O(V)\)
struct DSU{
vector<int> fa;
vector<int> sz;
DSU(int n){
fa.resize(n+1);
sz.resize(n+1,1);
iota(fa.begin(), fa.end(), 0);
}
int find(int x){
return fa[x]==x?x:fa[x]=find(fa[x]);
}
bool merge(int x,int y){
int fx=find(x), fy=find(y);
if(fx==fy) return false;
if(sz[fx]<sz[fy]) swap(fx,fy);
fa[fy]=fx;
sz[fx]+=sz[fy];
return true;
}
};
pair<bool, vector<int>> find_cycle_dsu(int n, vector<pair<int,int>>& edges){
DSU dsu(n);
vector<int> parent_tree(n+1, -1);
vector<vector<int>> tree(n+1);
// 构建生成树并记录环边
int cycle_u=-1, cycle_v=-1;
for(auto [u,v]:edges){
if(!dsu.merge(u,v)){
cycle_u=u;
cycle_v=v;
break;
}else{
tree[u].push_back(v);
tree[v].push_back(u);
}
}
if(cycle_u==-1) return {false, {}};
// 在树中找路径
vector<int> par(n+1, -1);
queue<int> q;
q.push(cycle_u);
par[cycle_u]=0;
while(!q.empty()){
int u=q.front();q.pop();
if(u==cycle_v) break;
for(int v:tree[u]){
if(par[v]==-1){
par[v]=u;
q.push(v);
}
}
}
// 重构环
vector<int> cycle_path;
int cur=cycle_v;
while(cur!=0){
cycle_path.push_back(cur);
cur=par[cur];
}
reverse(cycle_path.begin(), cycle_path.end());
cycle_path.push_back(cycle_path[0]);
return {true, cycle_path};
}
找奇数环(含输出环)
4.1 染色法判定并输出奇数环
算法原理
二分图的判定可以通过二染色法实现。对于图 \(G\),用两种颜色给顶点染色,使得相邻顶点颜色不同。
核心思想:如果染色过程中发现相邻顶点颜色相同,则存在奇数环。
数学依据:在二分图中,任意环的长度都是偶数。因此,如果染色失败(即存在颜色冲突),则图中必然存在奇数环。
环的构造:当发现冲突边 \((u, v)\) 时(两者颜色相同),通过父指针从 \(u\) 和 \(v\) 分别回溯到LCA,两条路径加上冲突边就构成了一个奇数环。
算法步骤
- 初始化所有顶点颜色为 -1(未染色)
- 从每个未染色顶点开始DFS
- 对当前顶点染色为 \(c\),对邻接点染色为 \(c \oplus 1\)
- 如果邻接点已染色且颜色与 \(c \oplus 1\) 不同,则存在奇数环
- 通过父指针重构奇数环
时间复杂度
- 每个顶点访问一次:\(O(V)\)
- 每条边检查一次:\(O(E)\)
- 总时间复杂度:\(O(V + E)\)
- 空间复杂度:\(O(V)\)
vector<int> color_arr; // 0或1
vector<int> parent_odd;
int cycle_start=-1, cycle_end=-1;
vector<int> odd_cycle;
bool dfs_odd_cycle_find(int u, int c){
color_arr[u]=c;
for(int v:G[u]){
if(v==parent_odd[u]) continue;
if(color_arr[v]==-1){
parent_odd[v]=u;
if(dfs_odd_cycle_find(v, c^1)) return true;
}else if(color_arr[v]==c){
// 找到奇数环
cycle_start=v;
cycle_end=u;
// 重构环
odd_cycle.clear();
int cur=u;
while(cur!=v){
odd_cycle.push_back(cur);
cur=parent_odd[cur];
}
odd_cycle.push_back(v);
reverse(odd_cycle.begin(), odd_cycle.end());
odd_cycle.push_back(odd_cycle[0]);
return true;
}
}
return false;
}
bool find_and_print_odd_cycle(int n){
color_arr.assign(n+1, -1);
parent_odd.assign(n+1, -1);
odd_cycle.clear();
for(int i=1;i<=n;i++){
if(color_arr[i]==-1){
if(dfs_odd_cycle_find(i, 0)){
cout<<"找到奇数环:";
for(int j=0;j<odd_cycle.size();j++){
cout<<odd_cycle[j];
if(j<odd_cycle.size()-1) cout<<" -> ";
}
cout<<endl;
cout<<"奇数环长度:"<<odd_cycle.size()-1<<endl;
return true;
}
}
}
cout<<"图中没有奇数环(图是二分图)"<<endl;
return false;
}
4.2 基于深度优先搜索的奇数环检测与输出
算法原理
在DFS过程中,记录每个顶点的深度 \(depth\)。当发现回边 \((u, v)\) 时,环的长度可以通过深度差计算:
如果 \(len\) 是奇数,则找到了一个奇数环。
优势:这种方法不需要染色,直接利用DFS树的深度信息来判断环的奇偶性。
环的重构
从 \(u\) 沿着父指针回溯到 \(v\),加上回边 \((u, v)\),即可重构出环。
时间复杂度
- 每个顶点访问一次:\(O(V)\)
- 每条边检查一次:\(O(E)\)
- 总时间复杂度:\(O(V + E)\)
- 空间复杂度:\(O(V)\)
int depth_odd[N];
int parent_odd_dfs[N];
int vis_odd[N]; // 0:未访问, 1:访问中, 2:已访问
vector<int> odd_cycle_dfs;
bool dfs_odd_cycle_depth(int u){
vis_odd[u]=1;
for(int v:G[u]){
if(v==parent_odd_dfs[u]) continue;
if(vis_odd[v]==0){
depth_odd[v]=depth_odd[u]+1;
parent_odd_dfs[v]=u;
if(dfs_odd_cycle_depth(v)) return true;
}else if(vis_odd[v]==1){
// 找到环
int cycle_len=depth_odd[u]-depth_odd[v]+1;
if(cycle_len%2==1){
// 构造奇数环
odd_cycle_dfs.clear();
int cur=u;
while(cur!=v){
odd_cycle_dfs.push_back(cur);
cur=parent_odd_dfs[cur];
}
odd_cycle_dfs.push_back(v);
reverse(odd_cycle_dfs.begin(), odd_cycle_dfs.end());
odd_cycle_dfs.push_back(odd_cycle_dfs[0]);
return true;
}
}
}
vis_odd[u]=2;
return false;
}
bool find_and_print_odd_cycle_dfs(int n){
fill(vis_odd, vis_odd+n+1, 0);
fill(depth_odd, depth_odd+n+1, 0);
fill(parent_odd_dfs, parent_odd_dfs+n+1, -1);
odd_cycle_dfs.clear();
for(int i=1;i<=n;i++){
if(vis_odd[i]==0){
if(dfs_odd_cycle_depth(i)){
cout<<"DFS找到奇数环:";
for(int j=0;j<odd_cycle_dfs.size();j++){
cout<<odd_cycle_dfs[j];
if(j<odd_cycle_dfs.size()-1) cout<<" -> ";
}
cout<<endl;
cout<<"奇数环长度:"<<odd_cycle_dfs.size()-1<<endl;
return true;
}
}
}
cout<<"图中没有奇数环"<<endl;
return false;
}
4.3 带权并查集找奇数环并输出
算法原理
带权并查集维护每个节点到根节点的距离奇偶性。对于每条边 \((u, v)\):
定义 \(dis[x]\) 表示 \(x\) 到根节点的距离奇偶性(0表示偶数,1表示奇数)。
当添加边 \((u, v)\) 时:
- 如果 \(u\) 和 \(v\) 在同一集合中,检查 \(dis[u] \oplus dis[v]\) 是否等于1
- 如果 \(dis[u] \oplus dis[v] \neq 1\),则存在奇数环
- 如果 \(u\) 和 \(v\) 不在同一集合中,合并它们
数学原理:在二分图中,任意两个顶点之间的距离奇偶性是确定的。如果两条路径的奇偶性矛盾,则存在奇数环。
带权并查集操作
find(x):返回根节点,同时更新 \(dis[x]\) 为到根节点的距离奇偶性merge(x, y):添加边 \((x, y)\),返回是否矛盾
时间复杂度
- 并查集操作:\(O(\alpha(V))\)
- 总时间复杂度:\(O(E \cdot \alpha(V))\)
- 空间复杂度:\(O(V)\)
struct DSU_Odd_Cycle{
vector<int> fa, dis; // dis[i]表示i到fa[i]的奇偶性(0:偶,1:奇)
vector<int> sz;
vector<pair<int,int>> edges;
DSU_Odd_Cycle(int n){
fa.resize(n+1);
dis.resize(n+1,0);
sz.resize(n+1,1);
iota(fa.begin(), fa.end(), 0);
}
int find(int x){
if(fa[x]==x) return x;
int root=find(fa[x]);
dis[x]^=dis[fa[x]];
return fa[x]=root;
}
bool merge(int x,int y){
edges.push_back({x,y});
int fx=find(x), fy=find(y);
if(fx==fy){
// 如果在同一集合,检查奇偶性
return (dis[x]^dis[y])==1;
}
if(sz[fx]<sz[fy]) swap(fx,fy);
fa[fy]=fx;
dis[fy]=dis[x]^dis[y]^1;
sz[fx]+=sz[fy];
return true;
}
vector<int> get_odd_cycle(){
// 使用找到的矛盾边重构奇数环
// 具体实现需要记录所有边
return vector<int>();
}
};
找偶数环(含输出环)
5.1 基于BFS的偶数环检测与输出
算法原理
使用广度优先搜索(BFS)对图进行分层。在BFS过程中,每条边要么连接相邻层,要么连接同一层或相差更多层。
关键观察:
- BFS树中的树边连接相邻层
- 非树边(回边)连接的两个顶点,其深度差 \(d = |depth[u] - depth[v]|\)
对于非树边 \((u, v)\),它构成的环长度为:
如果 \(len\) 是偶数,则找到了一个偶数环。
环的重构
当找到满足条件的非树边 \((u, v)\) 时:
- 分别从 \(u\) 和 \(v\) 沿父指针回溯到LCA
- 两条路径合并,加上非树边 \((u, v)\),构成完整的环
时间复杂度
- BFS遍历:\(O(V + E)\)
- 环重构:\(O(V)\)
- 总时间复杂度:\(O(V + E)\)
- 空间复杂度:\(O(V)\)
int dist_even[N];
int parent_even[N];
vector<int> even_cycle;
bool find_even_cycle_bfs_print(int start){
queue<int> q;
vector<int> dist(N, -1);
vector<int> par(N, -1);
q.push(start);
dist[start]=0;
while(!q.empty()){
int u=q.front();q.pop();
for(int v:G[u]){
if(v==par[u]) continue;
if(dist[v]==-1){
dist[v]=dist[u]+1;
par[v]=u;
q.push(v);
}else if(dist[v]>=dist[u]){
int cycle_len=dist[u]+dist[v]+1;
if(cycle_len%2==0){
// 找到偶数环,重构路径
even_cycle.clear();
// 从u到v找路径
vector<int> path1, path2;
int cur=u;
while(cur!=-1){
path1.push_back(cur);
cur=par[cur];
}
reverse(path1.begin(), path1.end());
cur=v;
while(cur!=-1){
path2.push_back(cur);
cur=par[cur];
}
reverse(path2.begin(), path2.end());
// 合并路径
int lca=-1;
int i=0,j=0;
while(i<path1.size()&&j<path2.size()&&path1[i]==path2[j]){
lca=path1[i];
i++;j++;
}
for(int k=i;k<path1.size();k++){
even_cycle.push_back(path1[k]);
}
for(int k=j;k<path2.size();k++){
even_cycle.push_back(path2[k]);
}
even_cycle.push_back(path1[i-1]);
reverse(even_cycle.begin(), even_cycle.end());
even_cycle.push_back(even_cycle[0]);
return true;
}
}
}
}
return false;
}
5.2 无向图中找最小偶数环并输出
算法原理
对于图中的每条边 \((u, v)\),将其删除后,在剩余图中找 \(u\) 到 \(v\) 的最短路径。如果存在路径 \(P\),则 \(P \cup \{(u, v)\}\) 构成了一个环。
最小环:枚举所有边,取 \(dist(u, v) + 1\) 的最小值,其中 \(dist(u, v)\) 是删除边 \((u, v)\) 后 \(u\) 到 \(v\) 的最短距离。
对于偶数环,只需要检查 \(dist(u, v) + 1\) 是否为偶数。
算法步骤
- 枚举每条边 \((u, v)\)
- 从图中临时删除这条边
- 使用BFS找 \(u\) 到 \(v\) 的最短路径
- 如果存在路径,计算环长 \(len = dist(u, v) + 1\)
- 如果 \(len\) 是偶数且小于当前最优值,更新最优环
时间复杂度
- 枚举每条边:\(O(E)\)
- 每次BFS:\(O(V + E)\)
- 总时间复杂度:\(O(E \cdot (V + E))\)
- 空间复杂度:\(O(V + E)\)
vector<int> shortest_path_print(int s, int t, int skip_u, int skip_v, vector<int>& path){
vector<int> dist(N, inf);
vector<int> par(N, -1);
queue<int> q;
q.push(s);
dist[s]=0;
while(!q.empty()){
int u=q.front();q.pop();
if(u==t) break;
for(int v:G[u]){
if((u==skip_u&&v==skip_v)||(u==skip_v&&v==skip_u)) continue;
if(dist[v]>dist[u]+1){
dist[v]=dist[u]+1;
par[v]=u;
q.push(v);
}
}
}
if(dist[t]>=inf) return vector<int>();
// 重构路径
path.clear();
int cur=t;
while(cur!=-1){
path.push_back(cur);
cur=par[cur];
}
reverse(path.begin(), path.end());
return path;
}
vector<int> find_smallest_even_cycle_print(int n){
int ans=inf;
vector<int> best_cycle;
for(int u=1;u<=n;u++){
for(int v:G[u]){
if(u<v){ // 每条边只处理一次
vector<int> path;
if(!shortest_path_print(u,v,u,v,path).empty()){
int len=path.size();
if((len+1)%2==0 && len+1<ans){
ans=len+1;
best_cycle=path;
best_cycle.push_back(best_cycle[0]);
}
}
}
}
}
if(!best_cycle.empty()){
cout<<"最小的偶数环:";
for(int i=0;i<best_cycle.size();i++){
cout<<best_cycle[i];
if(i<best_cycle.size()-1) cout<<" -> ";
}
cout<<endl;
cout<<"偶数环长度:"<<best_cycle.size()-1<<endl;
}else{
cout<<"图中没有偶数环"<<endl;
}
return best_cycle;
}
5.3 有向图中的偶数环检测与输出
算法原理
在有向图中,偶数环的检测可以使用状态搜索。维护状态 \((v, p)\),其中 \(v\) 是当前顶点,\(p\) 是路径长度的奇偶性。
状态转移:从 \((u, p)\) 转移到 \((v, p \oplus 1)\),其中 \((u, v) \in E\)。
环的检测:如果从 \((v, p)\) 通过另一条路径再次到达 \((v, p)\),则找到了一个长度为偶数的环。
算法步骤
- 初始化所有状态为未访问
- 从每个顶点开始DFS
- 对每个状态 \((u, p)\),遍历所有出边 \((u, v)\)
- 转移到状态 \((v, p \oplus 1)\)
- 如果状态 \((v, p \oplus 1)\) 正在访问中,说明找到环
- 通过状态父指针重构环
时间复杂度
- 每个状态访问一次:\(O(2V)\)
- 每条边检查一次:\(O(E)\)
- 总时间复杂度:\(O(V + E)\)
- 空间复杂度:\(O(V)\)
int state_dir[N][2]; // 0:未访问, 1:访问中, 2:已访问
int parent_dir[N][2];
vector<int> even_cycle_dir;
bool dfs_even_directed_print(int u, int parity){
if(state_dir[u][parity]==1){
// 找到环
even_cycle_dir.clear();
int cur=u;
do{
even_cycle_dir.push_back(cur);
cur=parent_dir[cur][parity];
}while(cur!=u);
reverse(even_cycle_dir.begin(), even_cycle_dir.end());
even_cycle_dir.push_back(even_cycle_dir[0]);
return true;
}
if(state_dir[u][parity]==2) return false;
state_dir[u][parity]=1;
for(int v:G[u]){
parent_dir[v][parity^1]=u;
if(dfs_even_directed_print(v, parity^1)){
return true;
}
}
state_dir[u][parity]=2;
return false;
}
bool find_even_cycle_directed_print(int n){
memset(state_dir, 0, sizeof(state_dir));
memset(parent_dir, -1, sizeof(parent_dir));
for(int i=1;i<=n;i++){
if(dfs_even_directed_print(i,0)){
cout<<"有向图中找到偶数环:";
for(int j=0;j<even_cycle_dir.size();j++){
cout<<even_cycle_dir[j];
if(j<even_cycle_dir.size()-1) cout<<" -> ";
}
cout<<endl;
cout<<"偶数环长度:"<<even_cycle_dir.size()-1<<endl;
return true;
}
}
cout<<"有向图中没有偶数环"<<endl;
return false;
}
高级应用与变形(含输出环)
6.1 找所有环
算法原理
使用DFS枚举所有简单环。对于每个起始顶点,进行DFS搜索,不允许重复访问顶点(除了回到起始点)。
算法特点:
- 适用于小图(\(n \leq 20\))
- 可以找到图中所有简单环
- 时间复杂度为 \(O(2^n)\)
剪枝优化
- 限制环的最小长度(至少3)
- 使用位掩码标记已访问顶点
- 按顶点编号顺序搜索,避免重复
时间复杂度
- 最坏情况:\(O(2^n)\)
- 空间复杂度:\(O(n)\)
vector<vector<int>> all_cycles_list;
vector<int> path_all;
bool used_all[N];
void find_all_cycles_print(int start, int u, int parent_node){
if(u==start && path_all.size()>2){
// 找到一个环
vector<int> cycle_copy=path_all;
cycle_copy.push_back(start);
all_cycles_list.push_back(cycle_copy);
return;
}
for(int v:G[u]){
if(v==parent_node) continue;
if(!used_all[v] || (v==start && path_all.size()>2)){
used_all[v]=true;
path_all.push_back(v);
find_all_cycles_print(start, v, u);
path_all.pop_back();
used_all[v]=false;
}
}
}
void enumerate_and_print_all_cycles(int n){
all_cycles_list.clear();
for(int i=1;i<=n;i++){
fill(used_all, used_all+n+1, false);
path_all.clear();
path_all.push_back(i);
used_all[i]=true;
find_all_cycles_print(i, i, -1);
}
if(all_cycles_list.empty()){
cout<<"图中没有环"<<endl;
return;
}
cout<<"找到的所有环:"<<endl;
for(int i=0;i<all_cycles_list.size();i++){
cout<<"环 "<<i+1<<": ";
for(int j=0;j<all_cycles_list[i].size();j++){
cout<<all_cycles_list[i][j];
if(j<all_cycles_list[i].size()-1) cout<<" -> ";
}
cout<<" (长度:"<<all_cycles_list[i].size()-1<<")"<<endl;
}
}
6.2 基环树找环
算法原理
基环树(Pseudoforest)是一个有 \(n\) 个节点和 \(n\) 条边的无向连通图。它由一个环和若干棵附着在环上的树组成。
核心思想:使用拓扑排序删除所有叶子节点(度为1的顶点),剩余的节点就是环上的节点。
算法步骤:
- 计算每个顶点的度
- 将所有度为1的顶点入队
- 从队列中取出顶点,标记为已删除
- 对于每个邻接点,度减1
- 如果邻接点度变为1,入队
- 循环结束后,未被删除的顶点构成环
环的重构
从环上任意顶点出发,沿着环(度为2的顶点)遍历,可以得到环的顺序。
时间复杂度
- 拓扑排序:\(O(V + E)\)
- 环重构:\(O(V)\)
- 总时间复杂度:\(O(V + E)\)
- 空间复杂度:\(O(V)\)
vector<int> find_cycle_in_base_tree_print(int n){
vector<int> deg(n+1,0);
queue<int> q;
vector<int> removed(n+1,0);
for(int i=1;i<=n;i++){
deg[i]=G[i].size();
if(deg[i]==1){
q.push(i);
removed[i]=1;
}
}
while(!q.empty()){
int u=q.front();q.pop();
for(int v:G[u]){
if(!removed[v]){
deg[v]--;
if(deg[v]==1){
q.push(v);
removed[v]=1;
}
}
}
}
// 剩余的节点构成环
vector<int> cycle;
for(int i=1;i<=n;i++){
if(!removed[i]) cycle.push_back(i);
}
// 按照环的顺序排列
if(!cycle.empty()){
vector<int> ordered_cycle;
int start=cycle[0];
int cur=start;
int prev=-1;
do{
ordered_cycle.push_back(cur);
int next=-1;
for(int v:G[cur]){
if(!removed[v] && v!=prev){
next=v;
break;
}
}
prev=cur;
cur=next;
}while(cur!=start && cur!=-1);
ordered_cycle.push_back(start);
cout<<"基环树中的环:";
for(int i=0;i<ordered_cycle.size();i++){
cout<<ordered_cycle[i];
if(i<ordered_cycle.size()-1) cout<<" -> ";
}
cout<<endl;
cout<<"环的长度:"<<ordered_cycle.size()-1<<endl;
return ordered_cycle;
}
cout<<"图中没有环(不是基环树)"<<endl;
return vector<int>();
}
6.3 负环检测与输出(Bellman-Ford)
算法原理
负环(Negative Cycle)是边权之和为负数的环。在带权有向图中,负环检测非常重要。
Bellman-Ford算法:
- 初始化距离数组 \(dist\),起点距离为0,其他为 \(\infty\)
- 重复 \(n-1\) 次:对每条边 \((u, v, w)\),如果 \(dist[u] + w < dist[v]\),更新 \(dist[v]\)
- 第 \(n\) 次:如果还能松弛,则存在负环
原理:在 \(n\) 个顶点的图中,最短路径最多包含 \(n-1\) 条边。如果第 \(n\) 次还能松弛,说明存在负环。
环的重构:记录每个节点的前驱节点,从最后被松弛的节点开始回溯,可以找到负环。
时间复杂度
- Bellman-Ford:\(O(VE)\)
- 环重构:\(O(V)\)
- 总时间复杂度:\(O(VE)\)
- 空间复杂度:\(O(V)\)
struct Edge{
int u,v,w;
};
vector<int> find_negative_cycle_print(int n, int m, vector<Edge>& edges){
vector<int> dist(n+1, inf);
vector<int> parent_neg(n+1, -1);
dist[1]=0;
int last_relaxed=-1;
for(int i=1;i<=n;i++){
last_relaxed=-1;
for(auto& e:edges){
if(dist[e.u]+e.w<dist[e.v]){
dist[e.v]=dist[e.u]+e.w;
parent_neg[e.v]=e.u;
last_relaxed=e.v;
}
}
if(last_relaxed==-1) break;
}
if(last_relaxed==-1){
cout<<"图中没有负环"<<endl;
return vector<int>();
}
// 找到负环中的一个顶点
int cycle_start=last_relaxed;
for(int i=1;i<=n;i++){
cycle_start=parent_neg[cycle_start];
}
// 重构负环
vector<int> negative_cycle;
int cur=cycle_start;
do{
negative_cycle.push_back(cur);
cur=parent_neg[cur];
}while(cur!=cycle_start);
negative_cycle.push_back(cycle_start);
reverse(negative_cycle.begin(), negative_cycle.end());
cout<<"找到负环:";
for(int i=0;i<negative_cycle.size();i++){
cout<<negative_cycle[i];
if(i<negative_cycle.size()-1) cout<<" -> ";
}
cout<<endl;
cout<<"负环长度:"<<negative_cycle.size()-1<<endl;
return negative_cycle;
}
6.4 SPFA找负环并输出
算法原理
SPFA(Shortest Path Faster Algorithm)是Bellman-Ford的队列优化版本。
负环检测:如果某个顶点入队次数超过 \(n\) 次,则存在负环。
SPFA vs Bellman-Ford:
- SPFA在大多数情况下更快
- 最坏时间复杂度相同:\(O(VE)\)
- SPFA在负环检测中更高效
算法步骤
- 初始化距离数组,起点入队
- 从队列取出顶点 \(u\)
- 对每条出边 \((u, v, w)\),如果 \(dist[u] + w < dist[v]\),更新并入队
- 记录每个顶点的入队次数
- 如果某个顶点入队次数 \(\geq n\),存在负环
环的重构
记录每个顶点的前驱节点,从入队次数超过 \(n\) 的顶点开始回溯,可以找到负环。
int cnt_spfa[N];
bool in_queue_spfa[N];
vector<int> spfa_negative_cycle_print(int n){
vector<int> dist(n+1, inf);
vector<int> parent(n+1, -1);
queue<int> q;
dist[1]=0;
q.push(1);
in_queue_spfa[1]=true;
while(!q.empty()){
int u=q.front();q.pop();
in_queue_spfa[u]=false;
for(int v:G[u]){
if(dist[v]>dist[u]+1){
dist[v]=dist[u]+1;
parent[v]=u;
if(!in_queue_spfa[v]){
q.push(v);
in_queue_spfa[v]=true;
cnt_spfa[v]++;
if(cnt_spfa[v]>=n){
// 找到负环
vector<int> negative_cycle;
int cur=v;
for(int i=1;i<=n;i++){
cur=parent[cur];
}
int start=cur;
do{
negative_cycle.push_back(cur);
cur=parent[cur];
}while(cur!=start);
negative_cycle.push_back(start);
reverse(negative_cycle.begin(), negative_cycle.end());
cout<<"SPFA找到负环:";
for(int i=0;i<negative_cycle.size();i++){
cout<<negative_cycle[i];
if(i<negative_cycle.size()-1) cout<<" -> ";
}
cout<<endl;
return negative_cycle;
}
}
}
}
}
cout<<"图中没有负环"<<endl;
return vector<int>();
}
6.5 找所有奇环和偶环的分解
算法原理
DFS树分解:对图进行DFS,得到DFS树。DFS树中的边分为两类:
- 树边:在DFS树中的边
- 回边:连接祖先和后代但不属于树的边
每条回边 \((u, v)\)(\(v\) 是 \(u\) 的祖先)与树路径 \(u \rightarrow v\) 构成一个环。
环的长度 = \(depth[u] - depth[v] + 1\)
根据长度的奇偶性,可以分类为奇环或偶环。
算法步骤
- 从每个未访问顶点开始DFS
- 记录每个顶点的深度和前驱
- 当遇到回边 \((u, v)\) 时,重构环
- 根据长度奇偶性分类
时间复杂度
- DFS遍历:\(O(V + E)\)
- 环重构:\(O(E \cdot V)\)(最坏情况)
- 总时间复杂度:\(O(V + E)\)(如果只计数)
- 空间复杂度:\(O(V)\)
int depth_decomp[N];
int parent_decomp[N];
vector<pair<int,int>> back_edges_decomp;
vector<pair<int,vector<int>>> odd_cycles_decomp;
vector<pair<int,vector<int>>> even_cycles_decomp;
void dfs_decompose_print(int u){
vis[u]=1;
for(int v:G[u]){
if(v==parent_decomp[u]) continue;
if(vis[v]==0){
parent_decomp[v]=u;
depth_decomp[v]=depth_decomp[u]+1;
dfs_decompose_print(v);
}else if(vis[v]==1){
back_edges_decomp.push_back({u,v});
}
}
vis[u]=2;
}
void classify_and_print_cycles(int n){
fill(vis, vis+n+1, 0);
fill(depth_decomp, depth_decomp+n+1, 0);
fill(parent_decomp, parent_decomp+n+1, -1);
back_edges_decomp.clear();
odd_cycles_decomp.clear();
even_cycles_decomp.clear();
for(int i=1;i<=n;i++){
if(vis[i]==0) dfs_decompose_print(i);
}
for(auto [u,v]:back_edges_decomp){
int len=depth_decomp[u]-depth_decomp[v]+1;
vector<int> cycle_path;
int cur=u;
while(cur!=v){
cycle_path.push_back(cur);
cur=parent_decomp[cur];
}
cycle_path.push_back(v);
reverse(cycle_path.begin(), cycle_path.end());
cycle_path.push_back(cycle_path[0]);
if(len%2==1){
odd_cycles_decomp.push_back({len, cycle_path});
}else{
even_cycles_decomp.push_back({len, cycle_path});
}
}
cout<<"找到的奇环:"<<endl;
for(auto [len, path]:odd_cycles_decomp){
cout<<"长度 "<<len<<": ";
for(int i=0;i<path.size();i++){
cout<<path[i];
if(i<path.size()-1) cout<<" -> ";
}
cout<<endl;
}
cout<<"找到的偶环:"<<endl;
for(auto [len, path]:even_cycles_decomp){
cout<<"长度 "<<len<<": ";
for(int i=0;i<path.size();i++){
cout<<path[i];
if(i<path.size()-1) cout<<" -> ";
}
cout<<endl;
}
}
6.6 强连通分量中的环
算法原理
强连通分量(Strongly Connected Component, SCC)是有向图中任意两个顶点互相可达的极大子图。
Tarjan算法:使用DFS,维护两个数组:
- \(dfn[u]\):顶点 \(u\) 的DFS序号
- \(low[u]\):顶点 \(u\) 能回溯到的\(dfn\)最小的顶点
当 \(low[u] == dfn[u]\) 时,栈中的顶点构成一个SCC。
SCC与环的关系:
- 每个SCC中至少包含一个环(除非是单个顶点且无自环)
- 可以在SCC内用DFS找环
时间复杂度
- Tarjan算法:\(O(V + E)\)
- SCC内找环:\(O(V + E)\)
- 总时间复杂度:\(O(V + E)\)
- 空间复杂度:\(O(V)\)
int dfn_scc[N], low_scc[N], tim_scc;
stack<int> st_scc;
bool in_stack_scc[N];
vector<vector<int>> scc_list;
void tarjan_find_cycle(int u){
dfn_scc[u]=low_scc[u]=++tim_scc;
st_scc.push(u);
in_stack_scc[u]=true;
for(int v:G[u]){
if(!dfn_scc[v]){
tarjan_find_cycle(v);
low_scc[u]=min(low_scc[u], low_scc[v]);
}else if(in_stack_scc[v]){
low_scc[u]=min(low_scc[u], dfn_scc[v]);
}
}
if(low_scc[u]==dfn_scc[u]){
vector<int> comp;
while(true){
int x=st_scc.top();st_scc.pop();
in_stack_scc[x]=false;
comp.push_back(x);
if(x==u) break;
}
scc_list.push_back(comp);
}
}
void find_cycle_in_scc_print(int n){
for(int i=1;i<=n;i++){
if(!dfn_scc[i]) tarjan_find_cycle(i);
}
cout<<"强连通分量中的环:"<<endl;
for(auto& comp:scc_list){
if(comp.size()>1){
cout<<"SCC大小为"<<comp.size()<<",包含环:";
// 在SCC中找一个环
vector<int> cycle_path;
// 使用DFS在SCC中找环
unordered_set<int> scc_set(comp.begin(), comp.end());
vector<int> parent_temp(n+1, -1);
vector<int> vis_temp(n+1, 0);
for(int start:comp){
if(vis_temp[start]) continue;
queue<int> q;
q.push(start);
vis_temp[start]=1;
while(!q.empty()){
int u=q.front();q.pop();
for(int v:G[u]){
if(scc_set.find(v)==scc_set.end()) continue;
if(!vis_temp[v]){
vis_temp[v]=1;
parent_temp[v]=u;
q.push(v);
}else{
// 找到环
int cur=u;
while(cur!=v){
cycle_path.push_back(cur);
cur=parent_temp[cur];
}
cycle_path.push_back(v);
reverse(cycle_path.begin(), cycle_path.end());
cycle_path.push_back(cycle_path[0]);
goto found;
}
}
}
}
found:
if(!cycle_path.empty()){
for(int i=0;i<cycle_path.size();i++){
cout<<cycle_path[i];
if(i<cycle_path.size()-1) cout<<" -> ";
}
cout<<" (长度:"<<cycle_path.size()-1<<")"<<endl;
}
}
}
}
6.7 带权环的最大/最小值
算法原理
Floyd-Warshall算法求最小环:
- 初始化距离矩阵 \(dist[i][j]\) 为边权
- 枚举中间顶点 \(k\)
- 在更新 \(dist\) 之前,检查经过 \(k\) 的环
- 环的长度 = \(dist[i][j] + w[i][k] + w[k][j]\)
环的重构:维护路径矩阵 \(path[i][j]\),记录 \(i\) 到 \(j\) 最短路径上 \(i\) 的下一个顶点。
数学推导
对于每个中间顶点 \(k\),环 \(i \rightarrow j \rightarrow k \rightarrow i\) 的权重为:
其中 \(dist[i][j]\) 是不经过 \(k\) 的最短路径。
时间复杂度
- Floyd-Warshall:\(O(V^3)\)
- 环重构:\(O(V)\)
- 总时间复杂度:\(O(V^3)\)
- 空间复杂度:\(O(V^2)\)
int w[N][N];
int dist_w[N][N];
int path_w[N][N];
vector<int> min_cycle_floyd_print(int n){
// 初始化
for(int i=1;i<=n;i++){
for(int j=1;j<=n;j++){
dist_w[i][j]=w[i][j];
path_w[i][j]=i;
}
}
int ans=inf;
vector<int> best_cycle;
for(int k=1;k<=n;k++){
// 检测经过k的环
for(int i=1;i<k;i++){
for(int j=1;j<k;j++){
if(i!=j && dist_w[i][j]+w[i][k]+w[k][j]<ans){
ans=dist_w[i][j]+w[i][k]+w[k][j];
// 重构路径
best_cycle.clear();
best_cycle.push_back(i);
// 从i到j的路径
int cur=i;
while(cur!=j){
cur=path_w[cur][j];
best_cycle.push_back(cur);
}
best_cycle.push_back(k);
best_cycle.push_back(i);
}
}
}
// Floyd
for(int i=1;i<=n;i++){
for(int j=1;j<=n;j++){
if(dist_w[i][k]+dist_w[k][j]<dist_w[i][j]){
dist_w[i][j]=dist_w[i][k]+dist_w[k][j];
path_w[i][j]=path_w[i][k];
}
}
}
}
if(ans>=inf){
cout<<"图中没有环"<<endl;
return vector<int>();
}
cout<<"最小环权重:"<<ans<<endl;
cout<<"环路径:";
for(int i=0;i<best_cycle.size();i++){
cout<<best_cycle[i];
if(i<best_cycle.size()-1) cout<<" -> ";
}
cout<<endl;
return best_cycle;
}
6.8 最小环检测(BFS版本)
算法原理
从每个顶点出发执行BFS,记录到达每个顶点的距离。当发现已经访问过的顶点时,说明存在环。
最小环:\(ans = \min(ans, dist[u] + dist[v] + 1)\),其中 \((u, v)\) 是非树边。
环的重构:从 \(u\) 和 \(v\) 分别回溯到LCA,合并路径。
时间复杂度
- 从每个顶点执行BFS:\(O(V \cdot (V + E))\)
- 环重构:\(O(V)\)
- 总时间复杂度:\(O(V \cdot (V + E))\)
- 空间复杂度:\(O(V)\)
vector<int> min_cycle_bfs_print(int n){
int ans=inf;
vector<int> best_cycle;
for(int start=1;start<=n;start++){
vector<int> dist(n+1, inf);
vector<int> par(n+1, -1);
queue<int> q;
dist[start]=0;
q.push(start);
while(!q.empty()){
int u=q.front();q.pop();
for(int v:G[u]){
if(v==par[u]) continue;
if(dist[v]==inf){
dist[v]=dist[u]+1;
par[v]=u;
q.push(v);
}else{
int cycle_len=dist[u]+dist[v]+1;
if(cycle_len<ans){
ans=cycle_len;
// 重构环
best_cycle.clear();
vector<int> path1, path2;
int cur=u;
while(cur!=-1){
path1.push_back(cur);
cur=par[cur];
}
reverse(path1.begin(), path1.end());
cur=v;
while(cur!=-1){
path2.push_back(cur);
cur=par[cur];
}
reverse(path2.begin(), path2.end());
// 合并路径
int i=0,j=0;
while(i<path1.size()&&j<path2.size()&&path1[i]==path2[j]){
i++;j++;
}
i--;j--;
for(int k=i;k<path1.size();k++){
best_cycle.push_back(path1[k]);
}
for(int k=j+1;k<path2.size();k++){
best_cycle.push_back(path2[k]);
}
best_cycle.push_back(path1[i]);
reverse(best_cycle.begin(), best_cycle.end());
best_cycle.push_back(best_cycle[0]);
}
}
}
}
}
if(best_cycle.empty()){
cout<<"图中没有环"<<endl;
return vector<int>();
}
cout<<"BFS找到的最小环:";
for(int i=0;i<best_cycle.size();i++){
cout<<best_cycle[i];
if(i<best_cycle.size()-1) cout<<" -> ";
}
cout<<endl;
cout<<"最小环长度:"<<best_cycle.size()-1<<endl;
return best_cycle;
}
6.9 生成树与环的关系
算法原理
基本环:对于生成树 \(T\) 中的每条非树边 \((u, v)\),\(T\) 中 \(u\) 到 \(v\) 的唯一路径加上边 \((u, v)\) 构成一个环。
环空间:所有基本环构成了图 \(G\) 的环空间的一组基。
环空间维数:\(dim = |E| - |V| + c\),其中 \(c\) 是连通分量数。
算法步骤
- 对图进行DFS,生成一棵DFS树
- 识别非树边
- 对每条非树边,在树中找路径
- 路径加上非树边构成环
时间复杂度
- DFS生成树:\(O(V + E)\)
- 每条非树边找路径:\(O(V)\)
- 总时间复杂度:\(O(E \cdot V)\)(最坏情况)
- 空间复杂度:\(O(V)\)
int parent_tree[N];
int depth_tree[N];
vector<pair<int,int>> non_tree_edges_tree;
void dfs_tree_build(int u){
vis[u]=1;
for(int v:G[u]){
if(v==parent_tree[u]) continue;
if(vis[v]==0){
parent_tree[v]=u;
depth_tree[v]=depth_tree[u]+1;
dfs_tree_build(v);
}else{
non_tree_edges_tree.push_back({u,v});
}
}
}
vector<int> get_cycle_from_tree_print(int u, int v){
vector<int> path_u, path_v;
while(u!=v){
if(depth_tree[u]>depth_tree[v]){
path_u.push_back(u);
u=parent_tree[u];
}else if(depth_tree[v]>depth_tree[u]){
path_v.push_back(v);
v=parent_tree[v];
}else{
path_u.push_back(u);
path_v.push_back(v);
u=parent_tree[u];
v=parent_tree[v];
}
}
path_u.push_back(u);
reverse(path_v.begin(), path_v.end());
path_u.insert(path_u.end(), path_v.begin(), path_v.end());
path_u.push_back(path_u[0]);
return path_u;
}
void print_tree_cycles(int n){
fill(vis, vis+n+1, 0);
fill(parent_tree, parent_tree+n+1, -1);
fill(depth_tree, depth_tree+n+1, 0);
non_tree_edges_tree.clear();
for(int i=1;i<=n;i++){
if(vis[i]==0) dfs_tree_build(i);
}
cout<<"生成树中的非树边形成的环:"<<endl;
for(auto [u,v]:non_tree_edges_tree){
vector<int> cycle_path = get_cycle_from_tree_print(u, v);
cout<<"环:";
for(int i=0;i<cycle_path.size();i++){
cout<<cycle_path[i];
if(i<cycle_path.size()-1) cout<<" -> ";
}
cout<<" (长度:"<<cycle_path.size()-1<<")"<<endl;
}
}
6.10 欧拉环的构建与输出
算法原理
欧拉环(Eulerian Cycle)是经过图中每条边恰好一次的环。
判定条件(无向图):
- 所有顶点的度数都是偶数
- 图是连通的
Hierholzer算法:
- 从任意顶点开始DFS
- 当到达死路时,记录顶点
- 返回时,将顶点加入路径
- 最终得到欧拉环
算法步骤
- 检查所有顶点度数是否为偶数
- 从任意顶点开始Hierholzer算法
- 递归遍历每条边
- 最后得到欧拉路径
时间复杂度
- Hierholzer算法:\(O(E)\)
- 度数检查:\(O(V)\)
- 总时间复杂度:\(O(V + E)\)
- 空间复杂度:\(O(V + E)\)
multiset<int> G_euler[N];
vector<int> euler_path_cycle;
void hierholzer_euler(int u){
while(!G_euler[u].empty()){
int v=*G_euler[u].begin();
G_euler[u].erase(G_euler[u].begin());
G_euler[v].erase(G_euler[v].find(u));
hierholzer_euler(v);
}
euler_path_cycle.push_back(u);
}
bool find_euler_cycle_print(int n){
// 检查所有度数是否为偶数
for(int i=1;i<=n;i++){
if(G_euler[i].size()%2) return false;
}
euler_path_cycle.clear();
hierholzer_euler(1);
if(euler_path_cycle.size()==m+1){
cout<<"欧拉环路径:";
for(int i=0;i<euler_path_cycle.size();i++){
cout<<euler_path_cycle[i];
if(i<euler_path_cycle.size()-1) cout<<" -> ";
}
cout<<endl;
return true;
}
return false;
}
6.11 哈密顿环的查找与输出
算法原理
哈密顿环(Hamiltonian Cycle)是经过图中每个顶点恰好一次的环。
判定:NP完全问题,目前没有多项式时间算法。
回溯法:
- 从顶点1开始
- 尝试访问所有邻接点
- 如果访问了所有顶点且能回到起点,找到哈密顿环
- 回溯尝试其他路径
时间复杂度
- 最坏情况:\(O(n!)\)
- 空间复杂度:\(O(n)\)
int hamiltonian_path[N];
bool used_hamiltonian[N];
bool found_hamiltonian=false;
vector<int> hamiltonian_cycle_result;
void hamiltonian_cycle_search(int u, int count, int n, vector<vector<int>>& adj){
if(count==n){
if(adj[u][1]){
found_hamiltonian=true;
hamiltonian_cycle_result.clear();
for(int i=1;i<=n;i++) hamiltonian_cycle_result.push_back(hamiltonian_path[i]);
hamiltonian_cycle_result.push_back(1);
}
return;
}
for(int v=1;v<=n;v++){
if(adj[u][v]&&!used_hamiltonian[v]){
used_hamiltonian[v]=true;
hamiltonian_path[count+1]=v;
hamiltonian_cycle_search(v, count+1, n, adj);
used_hamiltonian[v]=false;
if(found_hamiltonian) return;
}
}
}
void find_hamiltonian_cycle_print(int n, vector<vector<int>>& adj){
found_hamiltonian=false;
fill(used_hamiltonian, used_hamiltonian+n+1, false);
hamiltonian_path[1]=1;
used_hamiltonian[1]=true;
hamiltonian_cycle_search(1, 1, n, adj);
if(found_hamiltonian){
cout<<"找到哈密顿环:";
for(int i=0;i<hamiltonian_cycle_result.size();i++){
cout<<hamiltonian_cycle_result[i];
if(i<hamiltonian_cycle_result.size()-1) cout<<" -> ";
}
cout<<endl;
}else{
cout<<"图中不存在哈密顿环"<<endl;
}
}
6.12 竞赛图找环
算法原理
竞赛图(Tournament)是完全有向图,其中每对顶点之间恰有一条有向边。
性质:
- 竞赛图有哈密顿路径
- 竞赛图有哈密顿环当且仅当它是强连通的
- 竞赛图中没有环当且仅当它是全序图(传递竞赛图)
检测方法:使用拓扑排序。如果拓扑排序后所有顶点都被处理,则没有环。
环的重构:未被拓扑排序处理的顶点构成环。
时间复杂度
- 拓扑排序:\(O(V^2)\)
- 总时间复杂度:\(O(V^2)\)
- 空间复杂度:\(O(V)\)
vector<int> find_cycle_in_tournament(int n, vector<vector<int>>& adj){
vector<int> in_degree(n+1,0);
queue<int> q;
vector<int> removed(n+1,0);
for(int i=1;i<=n;i++){
for(int j=1;j<=n;j++){
if(adj[i][j]) in_degree[j]++;
}
}
for(int i=1;i<=n;i++){
if(in_degree[i]==0){
q.push(i);
removed[i]=1;
}
}
while(!q.empty()){
int u=q.front();q.pop();
for(int v=1;v<=n;v++){
if(adj[u][v]){
in_degree[v]--;
if(in_degree[v]==0){
q.push(v);
removed[v]=1;
}
}
}
}
// 找出环中的顶点
vector<int> cycle_vertices;
for(int i=1;i<=n;i++){
if(!removed[i]) cycle_vertices.push_back(i);
}
if(cycle_vertices.empty()){
cout<<"竞赛图中没有环(是全序图)"<<endl;
return vector<int>();
}
// 重构环
vector<int> result_cycle;
int start=cycle_vertices[0];
int cur=start;
do{
result_cycle.push_back(cur);
for(int v:cycle_vertices){
if(adj[cur][v]&&!removed[v]){
cur=v;
break;
}
}
}while(cur!=start);
result_cycle.push_back(start);
cout<<"竞赛图中找到环:";
for(int i=0;i<result_cycle.size();i++){
cout<<result_cycle[i];
if(i<result_cycle.size()-1) cout<<" -> ";
}
cout<<endl;
return result_cycle;
}
完整代码实现
#include <bits/stdc++.h>
#define int long long
#define COUT cout<<"\n--------------------------------------------------------------\n"
#define fi first
#define se second
#define be begin()
#define en end()
using namespace std;
const int N=2e5+5;
const int mod=998244353;
const int inf=1e18;
// ==================== 全局变量 ====================
vector<int> G[N];
int vis[N], parent[N], depth[N], color[N];
vector<int> cycle;
// ==================== 1. 基础DFS找环(无向图) ====================
bool dfs_find_cycle(int u){
vis[u]=1;
for(int v:G[u]){
if(v==parent[u]) continue;
if(vis[v]==0){
parent[v]=u;
if(dfs_find_cycle(v)) return true;
}else if(vis[v]==1){
cycle.clear();
int cur=u;
while(cur!=v){
cycle.push_back(cur);
cur=parent[cur];
}
cycle.push_back(v);
reverse(cycle.begin(), cycle.end());
cycle.push_back(cycle[0]);
return true;
}
}
vis[u]=2;
return false;
}
void print_cycle_basic(){
if(cycle.empty()){
cout<<"图中没有环"<<endl;
return;
}
cout<<"找到环:";
for(int i=0;i<cycle.size();i++){
cout<<cycle[i];
if(i<cycle.size()-1) cout<<" -> ";
}
cout<<endl;
cout<<"环的长度为:"<<cycle.size()-1<<endl;
}
// ==================== 2. 拓扑排序找环(有向图) ====================
vector<int> topo_find_cycle(int n){
vector<int> in_degree(n+1,0);
queue<int> q;
vector<int> removed(n+1,0);
for(int i=1;i<=n;i++){
for(int v:G[i]) in_degree[v]++;
}
for(int i=1;i<=n;i++){
if(in_degree[i]==0){
q.push(i);
removed[i]=1;
}
}
while(!q.empty()){
int u=q.front();q.pop();
for(int v:G[u]){
in_degree[v]--;
if(in_degree[v]==0){
q.push(v);
removed[v]=1;
}
}
}
vector<int> cycle_vertices;
for(int i=1;i<=n;i++){
if(!removed[i]) cycle_vertices.push_back(i);
}
if(cycle_vertices.empty()) return vector<int>();
vector<int> result_cycle;
int start=cycle_vertices[0];
int cur=start;
do{
result_cycle.push_back(cur);
for(int v:G[cur]){
if(!removed[v]){
cur=v;
break;
}
}
}while(cur!=start);
result_cycle.push_back(start);
return result_cycle;
}
void print_topo_cycle(vector<int>& cyc){
if(cyc.empty()){
cout<<"有向图中没有环"<<endl;
return;
}
cout<<"有向图中找到环:";
for(int i=0;i<cyc.size();i++){
cout<<cyc[i];
if(i<cyc.size()-1) cout<<" -> ";
}
cout<<endl;
}
// ==================== 3. 并查集找环 ====================
struct DSU{
vector<int> fa, sz;
DSU(int n){
fa.resize(n+1);
sz.resize(n+1,1);
iota(fa.begin(), fa.end(), 0);
}
int find(int x){
return fa[x]==x?x:fa[x]=find(fa[x]);
}
bool merge(int x,int y){
int fx=find(x), fy=find(y);
if(fx==fy) return false;
if(sz[fx]<sz[fy]) swap(fx,fy);
fa[fy]=fx;
sz[fx]+=sz[fy];
return true;
}
};
pair<bool, vector<int>> find_cycle_dsu(int n, vector<pair<int,int>>& edges){
DSU dsu(n);
vector<int> parent_tree(n+1,-1);
vector<vector<int>> tree(n+1);
int cycle_u=-1, cycle_v=-1;
for(auto [u,v]:edges){
if(!dsu.merge(u,v)){
cycle_u=u;
cycle_v=v;
break;
}else{
tree[u].push_back(v);
tree[v].push_back(u);
}
}
if(cycle_u==-1) return {false, {}};
vector<int> par(n+1,-1);
queue<int> q;
q.push(cycle_u);
par[cycle_u]=0;
while(!q.empty()){
int u=q.front();q.pop();
if(u==cycle_v) break;
for(int v:tree[u]){
if(par[v]==-1){
par[v]=u;
q.push(v);
}
}
}
vector<int> cycle_path;
int cur=cycle_v;
while(cur!=0){
cycle_path.push_back(cur);
cur=par[cur];
}
reverse(cycle_path.begin(), cycle_path.end());
cycle_path.push_back(cycle_path[0]);
return {true, cycle_path};
}
void print_dsu_cycle(pair<bool, vector<int>>& res){
if(!res.first){
cout<<"图中没有环"<<endl;
return;
}
cout<<"DSU找到环:";
for(int i=0;i<res.second.size();i++){
cout<<res.second[i];
if(i<res.second.size()-1) cout<<" -> ";
}
cout<<endl;
}
// ==================== 4. 染色法找奇数环 ====================
vector<int> color_arr;
vector<int> parent_odd;
vector<int> odd_cycle;
bool dfs_odd_cycle_find(int u, int c){
color_arr[u]=c;
for(int v:G[u]){
if(v==parent_odd[u]) continue;
if(color_arr[v]==-1){
parent_odd[v]=u;
if(dfs_odd_cycle_find(v, c^1)) return true;
}else if(color_arr[v]==c){
odd_cycle.clear();
int cur=u;
while(cur!=v){
odd_cycle.push_back(cur);
cur=parent_odd[cur];
}
odd_cycle.push_back(v);
reverse(odd_cycle.begin(), odd_cycle.end());
odd_cycle.push_back(odd_cycle[0]);
return true;
}
}
return false;
}
bool find_and_print_odd_cycle(int n){
color_arr.assign(n+1, -1);
parent_odd.assign(n+1, -1);
odd_cycle.clear();
for(int i=1;i<=n;i++){
if(color_arr[i]==-1){
if(dfs_odd_cycle_find(i, 0)){
cout<<"找到奇数环:";
for(int j=0;j<odd_cycle.size();j++){
cout<<odd_cycle[j];
if(j<odd_cycle.size()-1) cout<<" -> ";
}
cout<<endl;
cout<<"奇数环长度:"<<odd_cycle.size()-1<<endl;
return true;
}
}
}
cout<<"图中没有奇数环(图是二分图)"<<endl;
return false;
}
// ==================== 5. BFS找偶数环 ====================
int dist_even[N];
int parent_even[N];
vector<int> even_cycle;
bool find_even_cycle_bfs_print(int start){
queue<int> q;
vector<int> dist(N, -1);
vector<int> par(N, -1);
q.push(start);
dist[start]=0;
while(!q.empty()){
int u=q.front();q.pop();
for(int v:G[u]){
if(v==par[u]) continue;
if(dist[v]==-1){
dist[v]=dist[u]+1;
par[v]=u;
q.push(v);
}else if(dist[v]>=dist[u]){
int cycle_len=dist[u]+dist[v]+1;
if(cycle_len%2==0){
even_cycle.clear();
vector<int> path1, path2;
int cur=u;
while(cur!=-1){
path1.push_back(cur);
cur=par[cur];
}
reverse(path1.begin(), path1.end());
cur=v;
while(cur!=-1){
path2.push_back(cur);
cur=par[cur];
}
reverse(path2.begin(), path2.end());
int i=0,j=0;
while(i<path1.size()&&j<path2.size()&&path1[i]==path2[j]){
i++;j++;
}
for(int k=i;k<path1.size();k++){
even_cycle.push_back(path1[k]);
}
for(int k=j;k<path2.size();k++){
even_cycle.push_back(path2[k]);
}
even_cycle.push_back(path1[i-1]);
reverse(even_cycle.begin(), even_cycle.end());
even_cycle.push_back(even_cycle[0]);
return true;
}
}
}
}
return false;
}
bool find_and_print_even_cycle_bfs(int n){
for(int i=1;i<=n;i++){
if(find_even_cycle_bfs_print(i)){
cout<<"BFS找到偶数环:";
for(int j=0;j<even_cycle.size();j++){
cout<<even_cycle[j];
if(j<even_cycle.size()-1) cout<<" -> ";
}
cout<<endl;
cout<<"偶数环长度:"<<even_cycle.size()-1<<endl;
return true;
}
}
cout<<"图中没有偶数环"<<endl;
return false;
}
// ==================== 6. 基环树找环 ====================
vector<int> find_cycle_in_base_tree_print(int n){
vector<int> deg(n+1,0);
queue<int> q;
vector<int> removed(n+1,0);
for(int i=1;i<=n;i++){
deg[i]=G[i].size();
if(deg[i]==1){
q.push(i);
removed[i]=1;
}
}
while(!q.empty()){
int u=q.front();q.pop();
for(int v:G[u]){
if(!removed[v]){
deg[v]--;
if(deg[v]==1){
q.push(v);
removed[v]=1;
}
}
}
}
vector<int> cycle;
for(int i=1;i<=n;i++){
if(!removed[i]) cycle.push_back(i);
}
if(!cycle.empty()){
vector<int> ordered_cycle;
int start=cycle[0];
int cur=start;
int prev=-1;
do{
ordered_cycle.push_back(cur);
int next=-1;
for(int v:G[cur]){
if(!removed[v] && v!=prev){
next=v;
break;
}
}
prev=cur;
cur=next;
}while(cur!=start && cur!=-1);
ordered_cycle.push_back(start);
cout<<"基环树中的环:";
for(int i=0;i<ordered_cycle.size();i++){
cout<<ordered_cycle[i];
if(i<ordered_cycle.size()-1) cout<<" -> ";
}
cout<<endl;
cout<<"环的长度:"<<ordered_cycle.size()-1<<endl;
return ordered_cycle;
}
cout<<"图中没有环(不是基环树)"<<endl;
return vector<int>();
}
// ==================== 7. 负环检测(Bellman-Ford) ====================
struct Edge{
int u,v,w;
};
vector<int> find_negative_cycle_print(int n, int m, vector<Edge>& edges){
vector<int> dist(n+1, inf);
vector<int> parent_neg(n+1, -1);
dist[1]=0;
int last_relaxed=-1;
for(int i=1;i<=n;i++){
last_relaxed=-1;
for(auto& e:edges){
if(dist[e.u]+e.w<dist[e.v]){
dist[e.v]=dist[e.u]+e.w;
parent_neg[e.v]=e.u;
last_relaxed=e.v;
}
}
if(last_relaxed==-1) break;
}
if(last_relaxed==-1){
cout<<"图中没有负环"<<endl;
return vector<int>();
}
int cycle_start=last_relaxed;
for(int i=1;i<=n;i++){
cycle_start=parent_neg[cycle_start];
}
vector<int> negative_cycle;
int cur=cycle_start;
do{
negative_cycle.push_back(cur);
cur=parent_neg[cur];
}while(cur!=cycle_start);
negative_cycle.push_back(cycle_start);
reverse(negative_cycle.begin(), negative_cycle.end());
cout<<"找到负环:";
for(int i=0;i<negative_cycle.size();i++){
cout<<negative_cycle[i];
if(i<negative_cycle.size()-1) cout<<" -> ";
}
cout<<endl;
cout<<"负环长度:"<<negative_cycle.size()-1<<endl;
return negative_cycle;
}
// ==================== 8. SPFA找负环 ====================
int cnt_spfa[N];
bool in_queue_spfa[N];
vector<int> spfa_negative_cycle_print(int n){
vector<int> dist(n+1, inf);
vector<int> parent(n+1, -1);
queue<int> q;
dist[1]=0;
q.push(1);
in_queue_spfa[1]=true;
while(!q.empty()){
int u=q.front();q.pop();
in_queue_spfa[u]=false;
for(int v:G[u]){
if(dist[v]>dist[u]+1){
dist[v]=dist[u]+1;
parent[v]=u;
if(!in_queue_spfa[v]){
q.push(v);
in_queue_spfa[v]=true;
cnt_spfa[v]++;
if(cnt_spfa[v]>=n){
vector<int> negative_cycle;
int cur=v;
for(int i=1;i<=n;i++){
cur=parent[cur];
}
int start=cur;
do{
negative_cycle.push_back(cur);
cur=parent[cur];
}while(cur!=start);
negative_cycle.push_back(start);
reverse(negative_cycle.begin(), negative_cycle.end());
cout<<"SPFA找到负环:";
for(int i=0;i<negative_cycle.size();i++){
cout<<negative_cycle[i];
if(i<negative_cycle.size()-1) cout<<" -> ";
}
cout<<endl;
return negative_cycle;
}
}
}
}
}
cout<<"图中没有负环"<<endl;
return vector<int>();
}
// ==================== 主函数 ====================
signed main()
{
ios::sync_with_stdio(false);
cin.tie(0);
cout.tie(0);
int n,m;
cin>>n>>m;
for(int i=1;i<=m;i++){
int u,v;
cin>>u>>v;
G[u].push_back(v);
G[v].push_back(u);
}
COUT;
cout<<"1. 基础DFS找环:"<<endl;
fill(vis, vis+n+1, 0);
fill(parent, parent+n+1, -1);
if(dfs_find_cycle(1)){
print_cycle_basic();
}else{
cout<<"图中没有环"<<endl;
}
COUT;
cout<<"2. 奇数环检测(染色法):"<<endl;
find_and_print_odd_cycle(n);
COUT;
cout<<"3. 偶数环检测(BFS):"<<endl;
find_and_print_even_cycle_bfs(n);
COUT;
cout<<"4. 基环树找环:"<<endl;
find_cycle_in_base_tree_print(n);
COUT;
return 0;
}
总结
本文详细介绍了C++中图论找环的各种方法和变形,包括:
- 基础找环算法:DFS、拓扑排序、并查集
- 奇数环检测:染色法、DFS深度法、带权并查集
- 偶数环检测:BFS分层、边删除法、有向图状态搜索
- 高级应用:基环树、负环、强连通分量、欧拉环、哈密顿环
- 复杂网络:竞赛图、随机图、生成树分解
每个算法都提供了完整的C++实现,并包含环的输出功能。在实际应用中,应根据图的特点和需求选择合适的算法。a
本文来自博客园,作者:Aries2011,转载请注明原文链接:https://www.cnblogs.com/Aries2011/p/23042279

浙公网安备 33010602011771号