Loading

关于图中的环相关问题

判断无向图是否有环:

无向图中当顶点的数量和边的数量很大的时候,使用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;
    }
View Code

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;
} 
View Code

 求有向图的最小环花费:

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;
}
View Code

带权并查集 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;
}
View Code

拓扑排序 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;
}
View Code

 (上述算法应该仅限于每个点只有一条出边的题目。)

求所有环的数量(口胡)

好像只能用暴力dfs,不能用tajan因为不是求强联通分量。

有向图就是裸dfs, 无向图就是需要不能访问fa节点。

O(n(n+m)log)

posted @ 2020-09-19 20:53  ViKyanite  阅读(80)  评论(0编辑  收藏  举报