图论总结(欧拉路+Floyd所有结点最短+Bellman-Ford算法+SPFA+Dijsktra算法+Tarjan算法+最小生成树(prim+kruskal) )

目录

欧拉路

             判断欧拉路是否存在:

​​​​​​​

最短路:

Floyd算法 :

​​​​​​​​​​​​​​ Bellman-Ford:

​​​​​​​

Dijkstra 单源最短路

先附上例题: 

Tarjan算法:

最小生成树 

prim (普里姆算法)

 kruskal (克鲁斯卡尔算法)



​​​​​​​

欧拉路

                概念:从图中的某个点出发遍历整个图,图中的每条边通过且只通过一次

                欧拉回路:起点和终点相同的欧拉路

             判断欧拉路是否存在:

                       1)无向连通图:

如果图中的点都是偶点-》存在欧拉回路;任意一点都可以作为起点和终点;如果有二个奇点-〉存在欧拉路;

                       2)有向连通图:

所有的点的度都是0-》存在欧拉回路;如果存在一个度为1和-1的点其他都是0的图-〉存在欧拉路;

        代码实现:

 for(int i=1;i<=n;i++){
        cin>>u>>v;  //输入图,用G来存图
        degree[u]++;
        degree[v]++; //记录点的度
        G[u][v]++; //0不连接 1连接 >1有重边
        G[v][u]++;
    }
    for(int i=1;i<=n;i++){
        if(d[i]%2) break; //存在奇点 无欧拉回路
    }

        输入欧拉回路 代码实现 :

void print(int u){
    int v;
    for(int v=1;v<=n;v++){ //深度搜索u的所有邻居
        if(G[u][v]){
            G[u][v]--; //可能有重边
            G[v][u]--;
            print(v);
            cout<<v<<" "<<u;
        }
    }
}

最短路:

常见算法:Floyd  Bellman-Ford SPFA  Dijkstra 算法

Floyd算法 :

 优点:

1)可以一次求出所有结点之间的最短路径

2)可以处理有负边权的图

        思想:运用了动态规划的思想,求i到j 到最短路 ,可以分二种情况,即是否经过图中的某个点k,取二者的最短路径;

        判断负边权:

只要在floyd中加入判断是否存在某个group[i][i]<0就行

         例题:

/*
*@Author:   GuoJinlong
*@Language: C++
*/
//#include <bits/stdc++.h>
#include<iostream>
#include<cstdio>
#include<string>
#include<queue>
#include<stack>
#include<map>
#include<vector>
#include<list>
#include<set>
#include<iomanip>
#include<cstring>
#include<cctype>
#include<cmath>
#include<cstdlib>
#include<ctime>
#include<cassert>
#include<sstream>
#include<algorithm>
using namespace std;
const int mod=1e9+7;
typedef long long  ll;
#define ls (rt<<1)
#define rs (rt<<1|1)
#define mid (l+r)/2
#define mms(x, y) memset(x, y, sizeof x)
#define over(i,s,t) for(register long long i=s;i<=t;++i)
#define lver(i,t,s) for(register long long i=t;i>=s;--i)
const int MAXN = 305;
const int INF = 0x3f3f3f3f;
const int N=5e4+7;
const int maxn=1e5+5;
const double EPS=1e-10;
const double Pi=3.1415926535897;
//inline double max(double a,double b){
//    return a>b?a:b;
//}
//inline double min(double a,double b){
//    return a<b?a:b;
//}
 
int xd[8] = {0, 1, 0, -1, 1, 1, -1, -1};
int yd[8] = {1, 0, -1, 0, -1, 1, -1, 1};
 

//start
int G[200][200];
int n,m; //因为floyg是三重循环 所有只能计算很小的图 n<200的情况
void floyd(){
    int s=1;
    for(int k=1;k<=n;k++){
        for(int i=1;i<=n;i++){
            if(G[i][k]!=INF) //一个小优化
                for(int j=1;j<=n;j++){
                    if(G[i][j]>G[i][k]+G[k][j]){
                        G[i][j]=G[i][k]+G[k][j];
                    }
                }
        }
    }
    cout<<G[1][n]<<endl; //1是起点 n是终点 可以写成模版
    //cout<<G[s][E]<<endl; 
}
int main(){
    cin>>n>>m;
    for(int i=1;i<=n;i++){
        for(int j=1;j<n;j++){
            G[i][j]=INF;
        }
    }
    while (m--) {
        int u,v,w;
        cin>>u>>v>>w;
        G[u][v]=G[v][u]=w;
    }
    floyd();
    return 0;
}
//end

                缺点:

因为floyg是三重循环 所有只能计算很小的图 n<200的情况

 Bellman-Ford:

         功能:用来解决单源最短路径问题  给定一个起点s求它到图中所有n个结点的最短路径

这次我们使用结构体来存边 

        代码实现: 

/*
*@Author:   GuoJinlong
*@Language: C++
*/
//#include <bits/stdc++.h>
#include<iostream>
#include<cstdio>
#include<string>
#include<queue>
#include<stack>
#include<map>
#include<vector>
#include<list>
#include<set>
#include<iomanip>
#include<cstring>
#include<cctype>
#include<cmath>
#include<cstdlib>
#include<ctime>
#include<cassert>
#include<sstream>
#include<algorithm>
using namespace std;
const int mod=1e9+7;
typedef long long  ll;
#define ls (rt<<1)
#define rs (rt<<1|1)
#define mid (l+r)/2
#define mms(x, y) memset(x, y, sizeof x)
#define over(i,s,t) for(register long long i=s;i<=t;++i)
#define lver(i,t,s) for(register long long i=t;i>=s;--i)
const int MAXN = 305;
const int INF = 0x3f3f3f3f;
const int N=5e4+7;
const int maxn=1e5+5;
const double EPS=1e-10;
const double Pi=3.1415926535897;
//inline double max(double a,double b){
//    return a>b?a:b;
//}
//inline double min(double a,double b){
//    return a<b?a:b;
//}
 
int xd[8] = {0, 1, 0, -1, 1, 1, -1, -1};
int yd[8] = {1, 0, -1, 0, -1, 1, -1, 1};
 

//start
struct edge{
    int u,v,w;
}e[1005];
int n,m,cnt;
int pre[1005]; //存放路径
void print_path(int a,int b){ //打印路径 递归实现
    if(a==b) cout<<a<<" ";
    print_path(a,pre[b]);
    cout<<b<<" ";
}
void bellman(){
    int s;
    s=1;
    int d[10010];
    mms(d,INF); //初始化为无穷大
    d[s]=0;
    for(int k=1;k<=n;k++){ //一共n轮操作
        for(int i=0;i<cnt;i++){ //检查每条边
            int x=e[i].u;
            int y=e[i].v;
            if (d[x]>d[y]+e[i].w) {
                d[x]=d[y]+e[i].w;
                pre[x]=y;
            }
        }
    }
    cout<<d[n]<<endl;
//    print_path(s,n); //如有需要打印路径
}
int main(){
    cin>>n>>m;
    cnt=0; //记录边数 双向*2
    while (m--) {
        int a,b,c;
        cin>>a>>b>>c;
        e[cnt].u=a;
        e[cnt].v=b;
        e[cnt].w=c;
        cnt++;
        e[cnt].u=b;
        e[cnt].v=a;
        e[cnt].w=c;
        cnt++;
    }
    bellman();
}
//end

判断负圈:

当没有负圈时 只需要n轮就可以结束如果超过n轮 最短路还有变化的话 则存在负圈

1)在二个for结束后 检查所有边 如果d(u)>d(v)+w(u,v)这还不是最短存在

2)优化代码如下:

void bellman(){
    int s;
    s=1;
    int d[10010];
    mms(d,INF);
    d[s]=0;
//    for(int k=1;k<=n;k++){
//        for(int i=0;i<cnt;i++){
//            int x=e[i].u;
//            int y=e[i].v;
//            if (d[x]>d[y]+e[i].w) {
//                d[x]=d[y]+e[i].w;
//                pre[x]=y;
//            }
//        }
//    }
    int k=0;
    bool update=1;
    while (update) {
        k++;
        update=0;
        if(k>n) {
            cout<<"有负圈"<<endl;
            break;
        }
        for(int i=0;i<cnt;i++){
            int x=e[i].u;
            int y=e[i].v;
            if(d[x]>d[y]+e[i].w){
                d[x]=d[y]+e[i].w;
                update=1;
            }
        }
    }
    cout<<d[n]<<endl;
//    print_path(s,n);
}

SPFA:

        思路:

SPFA其实是bellman算法的优化 通过queue来实现,核心部分是每轮更新所有结点到s 的最短距离 我们通过邻接表来存图

注意 :

防止图点过于大 我们采用链式前向星来存图

代码实现: 

/*
*@Author:   GuoJinlong
*@Language: C++
*/
//#include <bits/stdc++.h>
#include<iostream>
#include<cstdio>
#include<string>
#include<queue>
#include<stack>
#include<map>
#include<vector>
#include<list>
#include<set>
#include<iomanip>
#include<cstring>
#include<cctype>
#include<cmath>
#include<cstdlib>
#include<ctime>
#include<cassert>
#include<sstream>
#include<algorithm>
using namespace std;
const int mod=1e9+7;
typedef long long  ll;
#define ls (rt<<1)
#define rs (rt<<1|1)
#define mid (l+r)/2
#define mms(x, y) memset(x, y, sizeof x)
#define over(i,s,t) for(register long long i=s;i<=t;++i)
#define lver(i,t,s) for(register long long i=t;i>=s;--i)
const int MAXN = 305;
const int INF = 0x3f3f3f3f;
const int N=5e4+7;
const int maxn=1e5+5;
const double EPS=1e-10;
const double Pi=3.1415926535897;
//inline double max(double a,double b){
//    return a>b?a:b;
//}
//inline double min(double a,double b){
//    return a<b?a:b;
//}
 
int xd[8] = {0, 1, 0, -1, 1, 1, -1, -1};
int yd[8] = {1, 0, -1, 0, -1, 1, -1, 1};
 

//start
const int NUM=1000005;
struct Edge{
    int to,next,w;
}edge[NUM];
int n,m,cnt;
int head[NUM];
int dis[NUM];
int vis[NUM];
int Neg[NUM];
int pre[NUM];
void print_path(int x,int y){
    //和之前的bellman代码一样
}
void init(){
    mms(head,-1); //初始化
    for(int i=0;i<NUM;i++){
        edge[i].next=-1;
    }
    cnt=0;
}
void addedge(int u,int v,int w){ //前向星存图
    edge[cnt].to=v;
    edge[cnt].w=w;
    edge[cnt].next=head[u];
    head[u]=cnt++;
    
}
int spfa(int s){
    mms(Neg,0); //初始化
    mms(dis,INF);
    mms(vis,0);
    dis[s]=0;
    queue<int> q;
    q.push(s);
    while (!q.empty()) {
        int u=q.front();
        q.pop();
        vis[u]=0; //起点出队
        for(int i=head[u];~i;i=edge[i].next){ //~1== i!=-1
            int v=edge[i].next;
            int w=edge[i].w;
            if(dis[u]+w<dis[v]){ //u的第I个邻居v 它借道u 到s更近
                dis[v]=w+dis[u];  //更新第I个邻居到s的距离
                pre[v]=u; //记录路径
                if(!vis[v]){ //v更新了但不在队列 
                    vis[v]=1;
                    q.push(v);
                    Neg[v]++;
                    if(Neg[v]>n) return 1; //出现负圈
                }
            }
        }
    }
    cout<<dis[n]<<endl;
//    print_path(s,n);
    return 1;
}
int main(){
    int n,m;
    cin>>n>>m;
    while (m--) {
        init();
        int a,b,c;
        addedge(a,b,c);
        addedge(a,b,c);
    }
    spfa(1);
}
//end

Dijkstra 单源最短路

思想用到了贪心:就是抄近路走,肯定可以找到最短路

方法是通过结构体存图 前向星+优先队列

先附上例题: 

武-NC15522(Dijsktra最短路算法)_m0_57006708的博客-CSDN博客

/*
*@Author:   GuoJinlong
*@Language: C++
*/
//#include <bits/stdc++.h>
#include<iostream>
#include<cstdio>
#include<string>
#include<queue>
#include<stack>
#include<map>
#include<vector>
#include<list>
#include<set>
#include<iomanip>
#include<cstring>
#include<cctype>
#include<cmath>
#include<cstdlib>
#include<ctime>
#include<cassert>
#include<sstream>
#include<algorithm>
using namespace std;
const int mod=1e9+7;
typedef long long  ll;
#define ls (rt<<1)
#define rs (rt<<1|1)
#define mid (l+r)/2
#define mms(x, y) memset(x, y, sizeof x)
#define over(i,s,t) for(register long long i=s;i<=t;++i)
#define lver(i,t,s) for(register long long i=t;i>=s;--i)
const int MAXN = 305;
const int INF = 0x3f3f3f3f;
const int N=5e4+7;
const int maxn=1e5+5;
const double EPS=1e-10;
const double Pi=3.1415926535897;
//inline double max(double a,double b){
//    return a>b?a:b;
//}
//inline double min(double a,double b){
//    return a<b?a:b;
//}
 
int xd[8] = {0, 1, 0, -1, 1, 1, -1, -1};
int yd[8] = {1, 0, -1, 0, -1, 1, -1, 1};
 

//start
const int MAX= 1e5+10;
struct node {
    int x,to,next,w;
    bool operator <(const node &a) const{
        return this->w>a.w;
    }
}G[MAX<<1];
int n,k,s;
int cnt;
int dis[MAX];
int vis[MAX];
int head[MAX];
void add(int u,int v,int w){
    G[++cnt].to=v;
    G[cnt].w=w;
    G[cnt].next=head[u];
    head[u]=cnt;
}
void dijkstra(int s){
    mms(dis,INF);
    dis[s]=0;
    priority_queue<node> p;
    node t;
    t.x=s;
    t.w=0;
    p.push(t);
    while (!p.empty()) {
        node u=p.top();
        p.pop();
        int v=u.x;
        if(dis[v]!=u.w) continue;
        for(int i=head[v];i;i=G[i].next){
            int to=G[i].to;
            if(dis[to]>dis[v]+G[i].w){
                dis[to]=dis[v]+G[i].w;
                t.x=to;
                t.w=dis[to];
                p.push(t);
            }
            
        }
    }
}
int main(){
    cin>>n>>s>>k;
    int a,b,c;
    for(int i=1;i<n;i++){
        cin>>a>>b>>c;
        add(a,b,c);
        add(b,a,c);
    }
    dijkstra(s);
    sort(dis+1,dis+1+n);
    cout<<dis[k+1];
}
//end

Tarjan算法:

强连通: 在一个有向图G里,如果有两个点(a、b)可以相互到达,我们就叫这两个顶点(a,b)为强连通。

强连通图: 如果在一个有向图G中,每两个点都强连通(可以相互到达),我们就叫这个图为强连通图。

强连通分量(SCC): 在一个有向图G中,有一个子图,这个子图每2个点都满足强连通,我们就叫这个子图叫做 强连通分量,孤立的点也是一个强连通分量。
 

Tarjan算法

Tarjan算法是一种用来求解有向图强连通分量的线性时间的算法。可以找强连通分量,也可以找缩点、割点等。


算法思路:

Tarjan算法是基于DFS的,每个强连通分量为搜索树的一棵子树,搜索时把当前搜索树中未处理的节点加入一个栈,回溯时判断栈顶到栈中节点是否为强连通分量。

为了使这颗搜索树在遇到强连通分量的节点的时候能顺利进行。在DFS时每个点都需要记录两个数组:

DFN[u]: 代表u点DFS到的时间,即时间戳,简单来说就是 第几个被搜索到的。可知在同一个DFS树的子树中,DFN[u] 越小,则其越浅。

Low[u]: 代表在DFS树中,u或u的子树 能够追溯到的最早的栈中节点的 时间戳。

算法过程:

(1)数组的初始化:对图进行深度优先搜索(DFS),在搜索过程中用 DFN 记录搜索的顺序。当首次搜索到点u 时,DFN 与 Low 数组的值都为 到该点的时间。


(2)堆栈:采用栈(记录已经搜索过的但是未删除的点),每搜索到一个点,将它压入栈顶。如果这个点有 出度 就继续往下找,直到找到底。


(3)每次返回时都将子节点与该节点的Low值进行比较,谁小就取谁,保证最小的子树根。

当点u 有与点u’ 相连时,如果此时(时间为DFN[u]时)u’不在栈中,u的 Low 值为两点的 Low值 中较小的一个。

当点u 有与点u’ 相连时,如果此时(时间为DFN[u]时)u’在栈中,u的 Low 值为 u的 Low值 和u’的 DFN值 中较小的一个


(4)每当搜索到一个点经过以上操作后(也就是子树已经全部遍历)的 Low值等于DFN值( DFN[u] == Low[u]),则将它以及在它之上的元素弹出栈。这些出栈的元素组成一个强连通分量。

原因:u点在DFS树中,子节点(后代)不能找到更浅的点,那么 u点及其后代构成一个SCC(强连通分量)。且 u点 是这个强连通分量的根节点(因为这个Low[] 值是这个强连通分量里最小的。)
(5)继续搜索(或许会更换搜索的起点,因为整个有向图可能分为两个不连通的部分),直到所有点被遍历。

例题:Problem - 1269 

const int MAX=10010;
vector<int>G[MAX];
int dfn[MAX],low[MAX],sc[MAX],s[MAX];//dfn第一次出现的序号 low在dfs树中u或者u的子树可以追溯到最早的时间撮
int cnt,ans,top;
void dfs(int u){
    s[top++]=u;
    dfn[u]=low[u]=++cnt;
    for(int v:G[u]){
        if(!dfn[v]){//如果不在栈中uv2点low的最小值
            dfs(v);
            low[u]=min(low[u],low[v]);
        }
        else if(!sc[v]){//在栈中low dfn取最小
            low[u]=min(low[u],dfn[v]);
        }
    }
    if(low[u]==dfn[u]){
        ans++;
        while (1) {//将v之前的元素全部推栈
            int v=s[--top];
            sc[v]=ans;
            if(u==v) break;
        }
    }
}
void Tarjan(int n){
    ans=top=cnt=0;
    mms(dfn,0);
    mms(low,0);
    mms(sc,0);
    for(int i=1;i<=n;i++){//继续搜索
        if(!dfn[i]){
            dfs(i);
        }
    }
}

int main(){
    int n,m,u,v;
    while (cin>>n>>m&&(n||m)) {
        for(int i=1;i<=n;i++) G[i].clear();
        for(int i=1;i<=n;i++){
            cin>>u>>v;
            G[u].push_back(v);
        }
        Tarjan(n);
        ans==1?cout<<"YES"<<endl:cout<<"NO"<<endl;
    }
    
}

最小生成树 

一个图中可能存在多条相连的边,我们一定可以从一个图中挑出一些边生成一棵树。这仅仅是生成一棵树,还未满足最小,当图中每条边都存在权重时,这时候我们从图中生成一棵树(n - 1 条边)时,生成这棵树的总代价就是每条边的权重相加之和。

prim (普里姆算法)

思路:

Prim算法每次循环都将一个蓝点u变为白点,并且此蓝点u与白点相连的最小边权min[u]还是当前所有蓝点中最小的。这样相当于向生成树中添加了n-1次最小的边,最后得到的一定是最小生成树。 

例题:

代码实现:

const int MAX=1010;
int vis[MAX];
int dis[MAX];
int n,m;
ll sum;
int a[MAX][MAX];
ll  prim(int pos){
    dis[pos]=0;
    for(int i=1;i<=n;i++){
        int cur=-1;
        for(int j=1;j<=n;j++){
            if(!vis[j]&&(cur==-1||dis[j]<dis[cur])){
                cur=j;
            }
        }
        if(dis[cur]>=INF) return INF;
        sum+=dis[cur];
        vis[cur]=1;
        for(int k=1;k<=n;k++){
            if(!vis[k]){
                dis[k]=min(dis[k],dis[cur]+a[cur][k]);
            }
        }
    }
    return sum;
}
int main(){
    cin>>n>>m;
    mms(dis,INF);
    mms(a,INF);
    for(int i=0;i<m;i++){
        int u,v,w;
        cin>>u>>v>>w;
        a[u][v]=w;
        a[v][u]=w;
    }
    cout<<prim(1)<<endl;
}

 kruskal (克鲁斯卡尔算法)

Kruskal算法将一个连通块当做一个集合。Kruskal首先将所有的边按从小到大顺序排序(一般使用快排),并认为每一个点都是孤立的,分属于n个独立的集合。然后按顺序枚举每一条边。如果这条边连接着两个不同的集合,那么就把这条边加入最小生成树,这两个不同的集合就合并成了一个集合;如果这条边连接的两个点属于同一集合,就跳过。直到选取了n-1条边为止。

const int MAX=100010;
struct  node{
    int u,v,w;
}e[MAX];
bool cmp(node a,node b){
    return a.w<b.w;
}
int n,m;
int fa[MAX];
int find(int x){
    if(x==fa[x]) return x;
    return fa[x]=find(fa[x]);
}
ll sum;
int main(){
    cin>>n>>m;
    for(int i=0;i<=n;i++)
        fa[i]=i;
    for(int i=1;i<=m;i++){
        cin>>e[i].u>>e[i].v>>e[i].w;
    }
    sort(e+1,e+1+n,cmp);
    for(int i=1;i<=m;i++){
        int x=find(e[i].u);
        int y=find(e[i].v);
        if(x==y) continue;
        fa[y]=x;
        sum+=e[i].w;
    }
    int ans=0;
    for(int i=1;i<=n;i++){
        if(i==fa[i])
            ans++;
    }
    if(ans>1){
        cout<<"NO"<<endl;
    }
    else {
        cout<<sum<<endl;
    }
}

猜你喜欢

转载自blog.csdn.net/m0_57006708/article/details/120432631