关于图中的环相关问题
判断无向图是否有环:#
无向图中当顶点的数量和边的数量很大的时候,使用dfs存在大量的递归,会导致栈溢出。使用下面的方法可以有效的避免。
判断无向图中是否存在回路(环)的算法描述
如果存在回路,则必存在一个子图,是一个环路。环路中所有顶点的度>=2。
算法:
第一步:删除所有度<=1的顶点及相关的边,并将另外与这些边相关的其它顶点的度减一。
第二步:将度数变为1的顶点排入队列,并从该队列中取出一个顶点重复步骤一。
如果最后还有未删除顶点,则存在环,否则没有环。

由于有m条边,n个顶点。如果m>=n,则根据图论知识可直接判断存在环路。 (证明:如果没有环路,则该图必然是k棵树 k>=1。根据树的性质,边的数目m = n-k。k>=1,所以:m<n) 如果m<n 则按照上面的算法每删除一个度为0的顶点操作一次(最多n次),或每删除一个度为1的顶点(同时删一条边)操作一次(最多m次)。这两种操作的总数不会超过m+n。由于m<n,所以算法复杂度为O(n)
判断有向图是否有环:#
拓扑排序,将出队元素存入数组,判断存储数组里的元素是否等于 n 。
求无向图的最小环花费:#
floyd算法 O (N3)#
给出一张无向图,求一个最小环并输出路径。
说说我的感觉:
包含点 i 和点 j 的最小环,我们可以看成是 i 到 j 之间的最短路和次短路的组合,通过 floyd 可求任意两点之间的最短距离,
那么我们只要找到最短路径外的一条最短路来保证 i 和 j 之间可达即可。在做 floyd 循环的同时,我们以 环权值 最小(最短路权值+次短路权值=最小环权值)为标准,
一直更新每个点的前驱,也就是记录 i 到 j 的最短路径,以及,能够松弛 i 和 j 的点 k (k 不在 i 到 j 的最短路径中)中代价最小的那个(也就是 i 到 j 之间的次短路),
然后按环的自然顺序输出即可。

#include<cstdio> #include<cstring> #define find_min(a,b) a<b?a:b const int N = 101; const int INF = 0x7ffffff; int mat[N][N],dist[N][N],pre[N][N],path[N],n; int main() { int i,j,k,m,a,b,c; int num; while(~scanf("%d%d",&n,&m)){ for(i=1;i<=n;i++){ for(j=1;j<=n;j++){ mat[i][j]=dist[i][j]=INF; pre[i][j]=i; } } while(m--){ scanf("%d%d%d",&a,&b,&c); mat[a][b]=mat[b][a]=dist[a][b]=dist[b][a]=find_min(mat[a][b],c); } int min=INF; for(k=1;k<=n;k++){//最短路径外一点将最短路首尾链接,那么就得到一个最小环 for(i=1;i<k;i++){ for(j=i+1;j<k;j++){ //求最小环不能用两点间最短路松弛,因为(i,k)之间的最短路,(k,j)之间的最短路可能有重合的部分 //所以mat[][]其实是不更新的,这里和单纯的floyd最短路不一样 //dist[i][j]保存的是 i 到 j 的最短路权值和 int tmp=dist[i][j]+mat[i][k]+mat[k][j];//这里 k 分别和 i 还有 j 在mat中直接相连 if(tmp<min){ min=tmp; num=0; int p=j; while(p!=i){//回溯 path[num++]=p; p=pre[i][p];//pre[i][j]表示 i 到 j 最短路径上 j 前面的一个点 } path[num++]=i; path[num++]=k; } } } for(i=1;i<=n;i++){ for(j=1;j<=n;j++){ if(dist[i][j]>dist[i][k]+dist[k][j]){ dist[i][j]=dist[i][k]+dist[k][j];//dist[][]保存两点间最短距离 pre[i][j]=pre[k][j]; } } } } if(min==INF)puts("No solution."); else{ printf("%d",path[0]); for(i=1;i<num;i++) printf(" %d",path[i]); puts(""); } } return 0; }
Dijkstra算法 O(M*Mlog)#
任意一个环的权值,我们都可以看成两个有边相连的结点i、j的直接距离加上i、j间不包含边(边i->j)的最短路径。求最短路径我们第一个想到的就是Dijkstra算法。
而Dijkstra所求的是一个点到所有点的最短距离。
用Dijkstra所求的i、j的最短距离一定是i、j的直接距离(如果i,j连通),所以我们需要先将i、j的边从图中删除(若i,j不连通,则不用删除),
再用Dijkstra求新图中i、j的最短距离即可。所以我们每次在图中选取一条边,把它从图中删掉.
然后对删掉的那条边所对应的2点进行Dijkstra,也就是m次Dijkstra
给出题目:http://acm.hdu.edu.cn/showproblem.php?pid=6005
给你m条边,每条边给你两个城市的坐标,还有两个城市道路之间有成本
让你求一个最小成本的周期,至少包含三个城市。

#include <bits/stdc++.h> #define pii pair<int,int> using namespace std; const int inf = 0x3f3f3f3f; const int maxn = 1e4+15; const int maxm = 8e3+43; int t,cnt,m,x,y,w,u,v,p,res,head[maxm]; struct edge{ int to,nxt,w; }e[maxm<<1]; struct cmp{ bool operator()(const pii a,const pii b){ return a.second > b.second; } }; map<pii,int> vis; inline void add(int x,int y,int _w){ e[++cnt].to = y;e[cnt].w = _w; e[cnt].nxt = head[x];head[x] = cnt; } int dis[maxm]; void init(){ vis.clear(); cnt = 0;res = inf;p = 0; memset(head,0,sizeof(head)); } void dijkstra(int x,int y){ memset(dis,inf,sizeof(dis)); priority_queue<pii,vector<pii>,cmp> q; dis[x] = 0; q.push(pii(x,0)); while(!q.empty()){ pii now = q.top();q.pop(); int uu = now.first; if(uu == y || now.second > res) return ; //剪枝 if(dis[uu] < now.second) continue; for(int i = head[uu];i;i = e[i].nxt){ int vv = e[i].to; if((uu==x&&vv==y)||(uu==y&&vv==x)) continue; //这条边是删除的边 if(dis[vv] > dis[uu]+e[i].w){ dis[vv] = dis[uu] + e[i].w; q.push(pii(vv,dis[vv])); } } } } int main(){ scanf("%d",&t); for(int Case = 1; Case <= t; ++Case){ init(); scanf("%d",&m); for(int i = 1;i <= m; ++i){ scanf("%d %d",&x,&y); if(!vis[pii(x,y)]) u = ++p,vis[pii(x,y)] = u; else u = vis[pii(x,y)]; scanf("%d %d %d",&x,&y,&w); if(!vis[pii(x,y)]) v = ++p,vis[pii(x,y)] = v; else v = vis[pii(x,y)]; add(u,v,w); add(v,u,w); } for(int i = 1;i <= p; ++i){ for(int j = head[i]; j ;j = e[j].nxt){ if(i >= e[j].to) continue; dijkstra(i,e[j].to); res = min(res,dis[e[j].to]+e[j].w); } } printf("Case #%d: %d\n",Case,res==inf ? 0:res); } return 0; }
求有向图的最小环花费:#
floyd算法 O (N3) (可以求边带权的)#
对于每个i 比较 dist[ i ][ i ] 的大小即可。

#include <iostream> #include <cstring> #include <cstdio> #include <queue> #define mmin(a,b) a<b? a:b using namespace std; const int INF=0x7ffffff; int mp[105][105]; int n,m; int floyd() { int i,j,k; for(k=0; k<n; k++) for(i=0; i<n; i++) for(j=0; j<n; j++) if(mp[i][j] > mp[i][k]+mp[k][j]) mp[i][j]=mp[i][k]+mp[k][j]; } int main() { // printf("%d\n",INF); int cases; scanf("%d",&cases); while(cases--) { int a,b,c; scanf("%d %d",&n,&m); for(int i=0; i<n; i++) for(int j=0; j<n; j++) mp[i][j]=INF; for(int i=0; i<m; i++) { scanf("%d %d %d",&a,&b,&c); if(c<mp[a][b]) mp[a][b]=c; } floyd(); int result=INF; /* for(int i=0; i<n; i++) { for(int j=0; j<n; j++) printf("%d ",mp[i][j]); printf("\n"); }*/ for(int i=0; i<n; i++) { // printf("%d\n",mp[i][i]); if(result>mp[i][i]) result=mp[i][i]; } if(result == INF) printf("-1\n"); else printf("%d\n",result); } return 0; }
带权并查集 O(N)(当n比较大时,且边权为1)#

#include<cstdio> #include<iostream> using namespace std; int f[200002],d[200002],n,minn,last; //f保存祖先节点,d保存到其祖先节点的路径长。 int fa(int x) { if (f[x]!=x) //查找时沿途更新祖先节点和路径长。 { int last=f[x]; //记录父节点(会在递归中被更新)。 f[x]=fa(f[x]); //更新祖先节点。 d[x]+=d[last]; //更新路径长(原来连在父节点上)。 } return f[x]; } void check(int a,int b) { int x=fa(a),y=fa(b); //查找祖先节点。 if (x!=y) {f[x]=y; d[a]=d[b]+1;} //若不相连,则连接两点,更新父节点和路径长。 else minn=min(minn,d[a]+d[b]+1); //若已连接,则更新最小环长度。 return; } int main() { int i,t; scanf("%d",&n); for (i=1;i<=n;i++) f[i]=i; //祖先节点初始化为自己,路径长为0。 minn=0x7777777; for (i=1;i<=n;i++) { scanf("%d",&t); check(i,t); //检查当前两点是否已有边相连接。 } printf("%d",minn); return 0; }
拓扑排序 O(N+M) + DFS (可能会爆)(这只是我的口胡而已)#
具体做法就是先跑一边拓扑标记,之后跑dfs就好,注意dfs找到环的时候不能直接结束整个dfs,因为需要统计所有环的大小(边权为1
如果带权的话,那么也是一样的,只不过把层数改成了目前经过的权值。
(上述算法应该仅限于每个点只有一条出边的题目。)
spfa 题目链接:https://blog.csdn.net/qq_43408238/article/details/102641472#

#include<iostream> #include<cstring> #include<cmath> #include<algorithm> #include<map> #include<cstdio> #include<queue> #include<stack> using namespace std; const int INF = 0x3f3f3f3f; int cost[305][305]; int dis[305]; int n; bool vis[305]; void spfa( int start ){ stack<int> Q; for( int i = 1; i <= n; i++ ){ dis[i] = cost[start][i]; if( i != start ){ Q.push( i ); vis[i] = true; } else{ vis[i] = false; } } dis[start] = INF; while( !Q.empty() ){ int x = Q.top(); Q.pop(); vis[x] = false; for( int y = 1; y <= n; y++ ){ if( x == y ) continue; if( dis[x] + cost[x][y] < dis[y] ){ dis[y] = dis[x] + cost[x][y]; if( !vis[y] ){ vis[y] = true; Q.push( y ); } } } } } int main(){ ios::sync_with_stdio( false ); while( cin >> n ){ for( int i = 1; i <= n; i++ ){ for( int j = 1; j <= n; j++ ){ cin >> cost[i][j]; } } int ans, c1, cn; spfa( 1 ); ans = dis[n]; c1 = dis[1]; spfa( n ); cn = dis[n]; cout << min( ans, c1 + cn ) << endl; } return 0; }
(上述算法应该仅限于每个点只有一条出边的题目。)
求所有环的数量(口胡)#
好像只能用暴力dfs,不能用tajan因为不是求强联通分量。
有向图就是裸dfs, 无向图就是需要不能访问fa节点。
O(n(n+m)log
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· 开发者必知的日志记录最佳实践
· SQL Server 2025 AI相关能力初探
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· winform 绘制太阳,地球,月球 运作规律
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 上周热点回顾(3.3-3.9)
· 超详细:普通电脑也行Windows部署deepseek R1训练数据并当服务器共享给他人