图的最短路径及最小生成树 模板

本文深入讲解图论中的三种核心算法:二分图判定、最短路径(Bellman-Ford算法)与最小生成树(Kruskal算法)。通过具体实例演示算法实现过程,帮助读者掌握这些算法的工作原理及应用场景。

本文来自《挑战程序设计竞赛》2.5 它们其实都是图

1.图的搜索

1.题目原文:

二分图判定。给定一个具有n个顶点的图,要给图上每个顶点染色,并且要使相邻的顶点颜色不同。问是否能最多用两种颜色进行染色。题目保证没有重边和自环。

1<=n<=1000

2.分析:

把相邻顶点染成不同颜色的问题叫作图的着色问题。对图进行染色所需要的最小颜色数成为最小着色数。最小着色数是2的图称作二分图。
如果只用两种颜色进行染色,那么确定一个顶点后,和它相邻的顶点也就确定了。因此可以从任意顶点出发,就可以判断是否可以被2种颜色染色了。用深度优先搜索很容易实现。

3.代码及样例

/*
3
0 1
0 2
1 2
-1 -1

NO

4
0 1
0 3
1 2
2 3
-1 -1
YES
*/
#include <iostream>
#include<vector>
#include<cstring>
using namespace std;
#define maxv 1000
vector<int> G[maxv];//图
int V;//顶点数
int color[maxv];//顶点i的颜色
//将顶点染成1或-1
bool dfs(int v,int c)
{
    color[v]=c;//把顶点v染成颜色c
    for(int i=0;i<G[v].size();i++){
        int u=G[v][i];
        //相邻顶点同色,返回false
        if(color[u]==c) return false;
        //相邻顶点还未染色,染成-c
        if(!color[u]&&!dfs(u,-c)) return false;
    }
    //如果所有的顶点都染过色了,则返回true
    return true;
}
void solve()
{
    for(int i=0;i<V;i++){
        if(color[i]==0){
            //顶点i未被染色,染成1
            if(!dfs(i,1)){
                cout<<"NO\n"<<endl;
                return;
            }
        }
    }
    cout<<"YES\n"<<endl;
}
int main()
{
    cin>>V;
    int u,v;
    cin>>u>>v;
    while(u>=0&&v>=0){
        G[u].push_back(v);
        G[v].push_back(u);
        cin>>u>>v;
    }
    memset(color,0,sizeof(color));
    solve();
    return 0;
}

2.最短路径问题

最短路是给定两个顶点,在以这两个顶点为起点和终点的路径中,边的权值之和最小的路径。智力游戏中的最小步数,也可以看作最短路径问题。

1.单源最短路径1(Bellman-Ford算法)

1.算法原理

记从起点s出发到顶点i的最短距离为d[i],则有d[i]=min{d[j]+(从j到i的边的权值)|e=(j,i)属于E}

如果给定的图是DAG,就可以按拓扑序给顶点编号,按照上述递推关系式计算出d。但是图中如果有圈,就不可以依赖这样的顺序计算。在这种情况下,设初值d[s]=0,d[i]=INF(足够大的常数),在不断利用这个递推关系式就可以解决。只要图中不存在负圈,操作就是有限的。


2.代码及样例:

/*
5 10
0 1 6
0 3 7
1 2 5
1 3 8
1 4 -4
2 1 -2
3 2 -3
3 4 9
4 0 2
4 2 7

0 2 4 7 -2
*/
#include <iostream>
#include<vector>
#include<cstring>
using namespace std;
#define maxn 1000
#define INF 0x7fffffff
int V,E;
struct edge
{
    int from;
    int to;
    int cost;
};
edge es[maxn];//边
int d[maxn];//最短距离
//求解从顶点s出发到所有顶点的最短距离
void shortest_path(int s)
{
    for(int i=0;i<V;i++){
        d[i]=INF;
    }
    d[s]=0;
    while(true){
        bool update=false;
        for(int i=0;i<E;i++){
            edge e=es[i];
            int u=e.from,v=e.to;
            if(d[u]!=INF&&d[v]>d[u]+e.cost){
                d[v]=d[u]+e.cost;
                update=true;
            }
        }
        if(!update) break;
    }
}
int main()
{
    cin>>V>>E;
    for(int i=0;i<E;i++){
        int u,v,c;
        cin>>u>>v>>c;
        es[i]=(edge){u,v,c};//这个感觉好6,以前没见过……
    }
    shortest_path(0);
    for(int i=0;i<V;i++){
        cout<<d[i]<<" ";
    }
    cout<<endl;
    return 0;
}

3.时间复杂度分析

如果图中不存在从s可达的负圈,那么最短路不会经过一个顶点两次(也就是说最多通过|V|-1条边,while循环最多执行|V|-1次。如果存在从s可达的负圈,那么第|V|次循环也会更新d的值,可以用这个性质,检查负圈。如果一开始把d[i]全部初始化成0,就可以检查所有的负圈。

4.负圈检查

//检查是否存在负圈
bool find_negative_loop()
{
    memset(d,0,sizeof(d));
    for(int i=0;i<V;i++){
        for(int j=0;j<E;j++){
            edge e=es[i];
            if(d[e.to]>d[e.from]+e.cost){
                d[e.to]=d[e.from]+e.cost;
                //如果第n次仍然更新了,则存在负圈
                if(i==V-1) return false;
            }
        }
    }
    return true;
}

3.最小生成树

2.Kruscal算法

1.算法原理

Kruscal算法按照边的权值的顺序从小到大查看一遍,如果不产生圈,就把这条边加入到生成树中。

现在介绍如何判断是否产生边。假设现在要把连接顶点u和v的边e加入到生成树中,如果之前u和v不在同一个连通分量中,那么加入e不会产生圈,否则会。可以用并查集高效地判断是否属于同一个连通分量。

Kruscal算法在边的排序上最花时间,算法的时间复杂度为O(ElogV)。

2.代码模板

/*
9 14
0 1 4
0 7 8
1 2 8
1 7 11
2 3 7
2 5 4
2 8 2
3 4 9
3 5 14
4 5 10
5 6 2
6 7 1
6 8 6
7 8 7

37
*/
#include<iostream>
#include<cstdio>
#include<set>
#include<algorithm>
using namespace std;
#define maxn 1005
int par[maxn];//父亲
int Rank[maxn];//树的高度
//初始化
void init(int n)
{
    for(int i=0;i<n;i++){
        par[i]=-1;
        Rank[i]=0;
    }
}
//查询父亲,路径压缩
int Find(int x)
{
    int s;
    for(s=x;par[s]!=-1;s=par[s]);
    while(s!=x){
        int temp=par[x];
        par[x]=s;
        x=temp;
    }
    return s;
}
//合并x,y属于的集合
void unite(int x,int y)
{
    x=Find(x);y=Find(y);
    if(x==y) return;
    if(Rank[x]<Rank[y]){
        par[x]=y;
    }
    else{
        par[y]=x;
        if(Rank[x]==Rank[y]) Rank[x]++;
    }
}
bool same(int x,int y)
{
    return Find(x)==Find(y);
}
struct edge
{
    int from;
    int to;
    int cost;
} ;
bool cmp(const edge& e1,const edge& e2)
{
    return e1.cost<e2.cost;
}
int V,E;
edge es[maxn];
int kruscal()
{
    sort(es,es+E,cmp);//按照edge.cost从小到大的顺序进行排序
    init(V);
    int res=0;
    for(int i=0;i<E;i++){
        edge e=es[i];
        if(!same(e.from,e.to)){
            unite(e.from,e.to);
            res+=e.cost;
        }
    }
    return res;
}
int main()
{
    cin>>V>>E;
    for(int i=0;i<E;i++){
        int u,v,c;
        cin>>u>>v>>c;
        es[i]=(edge){u,v,c};
    }
    printf("%d\n",kruscal());
    return 0;
}

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值