[C++]图论(二)

1 篇文章 0 订阅

Prim

最小生成树问题

n个点,以及n个点之间存在若干条权值不同的边,权值表示建这条边的代价。
现在求应该选哪些边,可以使得n个点连通,并且总花费最小。
定理: n个点,n-1条边的无向连通图,一定是树。
在一个图上生成出的树叫做这个图的一个生成树,边的权值之和最小的生成树叫做这个图的最小生成树。
Prim算法
首先把所有点分为两类:蓝点和白点
已经选 入最小生成树中的点: 蓝点
还没有选入最小生成树中的点:白点
初始化随机一点到最小生成树的距离为0,其它点到最小生成树的距离为inf
每次从白点的集合中挑选出距离已有的最小生成树距离最近的白点,加入最小生成树中
直到所有点都被加入最小生成树
Prim算法模板

 int MST_prim(){
    int ans=0;
    memset(dis,0x3f,sizeof(dis));
    memset(found,false,sizeof(found));
    dis[1]=0;
    while(1){
        int u=-1;
        for(int i=0;i<=n;i++){
            if(!found[i]&&(u==-1||dis[i]<dis[u])){
                u=i;
            }
        }
        if(u==-1) break;
        found[u]=true;
        ans+=dis[u];
        for(int i=0;i<=n;i++){
            if(!found[i]&&g[u][i]<dis[i]){
                dis[i]=g[u][i];
            }
        }
    }
    return ans;
}

拓扑排序

拓扑排序算法思想
从DAG图中选择一个没有前驱的顶点并输出
从图中删除该顶点和以该顶点为起点的有向边。
重复1、2,直到当前的DAG为空,或者当前图中不存在有前驱的顶点为止。如果最终图不为空,则必然存在环。
参考代码

int n,m;
vector<int> g[MAXN];
queue<int> q;
int f[MAXN],in[MAXN];

void Topsort(){
    for(int i=1;i<=n;i++){
        if(in[i]==0){
            q.push(i);
        }
    }
    while(!q.empty()){
        int t=q.front();
        cout<<t<<" ";
        q.pop();
        for(int i=0;i<g[t].size();i++){
            int idx=g[t][i];
            if(--in[idx]==0){
                q.push(idx);
            }
        }
    }
}

欧拉路径

  • 若图 G G G中存在这样一条路径,使得它恰通过 G G G中每条边一次,则称该路径为欧拉路径。若该路径是一个圈,则称为欧拉回路
  • 我们定义奇点为度为奇数的点,对于存在欧拉路径的图,有以下两个定理:
  • 存在欧拉路径的充要条件:图是连通的,有且只有 2 2 2个奇点
  • 存在欧拉回路的充要条件:图是连通的,有 0 0 0个奇点
  • 找欧拉路径(回路)的算法是深搜,时间复杂度为深搜的时间复杂度(邻接矩阵: O ( n 2 ) O(n^2) O(n2);邻接表: O ( n + m ) O(n+m) O(n+m)

参考代码

通过上面两个图,发现深搜时先序记录是不对的,只能后序遍历
代码如下:

#include <iostream>
#include <cstdio>
#include <vector>
#define MAXN 5010
using namespace std;
int n,m,start;//start是起点 
bool g[MAXN][MAXN];//此图用邻接矩阵存储 
int degree[MAXN];//每一个点的度 
int path[MAXN],cnt;//记录路径 
void find(int x){
    for(int i=1;i<=n;i++){
        if(g[x][i]){
            g[x][i]=g[i][x]=false;//该图是无向图 
            find(i);
        }
    }
    path[++cnt]=x;//记录当前路径,后序记录路径 
    //注意:只能后序记录,不能先序记录!先序记录是不对的! 
    //这是因为,如果遇到分叉口,先序遍历的话,一旦走错,就凉了! 
}
int main(){
    scanf("%d %d",&n,&m);//n为点的个数,m为边的数量 
    for(int i=1;i<=m;i++){//输入的时候处理度的问题 
        int x,y;
        scanf("%d %d",&x,&y);
        g[x][y]=g[y][x]=true;
        degree[x]++;
        degree[y]++;
    }
    for(int i=1;i<=n;i++){
        if(degree[i]%2==1){
            start=i;//存在奇点,以这个点为起点 
            break;
        }
    }
    if(start==0)//没有奇点 
        find(1);//从任何一个点开始均可 
    else//有两个奇点 
        find(start);//从一个奇点开始,到另一个奇点结束 
    for(int i=cnt;i>=1;i--){//如果题目要求你按字典序输出,需要逆序输出 
        printf("%d ",path[i]);
    }
    return 0;
}

哈密尔顿回路

  • 定义:不重复地走过所有的点,最后回到起点的路径
  • 哈密尔顿回路的求法就是搜索(填数问题),没有更好的方法(邻接矩阵: O ( n 2 ) O(n^2) O(n2);邻接表: O ( n + m ) O(n+m) O(n+m)

参考代码

#include <iostream>
using namespace std;
int n, m, g[105][105], ans[105];
bool vis[105];
bool dfs(int x, int t) {
    if(t>n) {
        if(g[x][1])    return true;
        return false;
    }
    for(int i=1; i<=n; i++) {
        if(g[x][i] && !vis[i]) {
            ans[t] = i;
            vis[i] = true;
            if(dfs(i, t+1))    return true;
            vis[i] = false;
        }
    }
    return false;
}
int main() {
    cin >> n >> m;
    int u, v;
    for(int i=1; i<=m; i++) {
        cin >> u >> v;
        g[u][v] = g[v][u] = 1;
    }
    ans[1] = 1;
    vis[1] = true;
    if(dfs(1, 2)) {
        for(int i=1; i<=n; i++)
            cout << ans[i] << ' ';
        cout << ans[1];
    }
    else    cout << "NO";
    return 0;
} 

Kruscal算法

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

参考代码

#include <iostream>
#include <cstdio>
#include <algorithm>
using namespace std;
int n,m;
struct edge{
    int u,v,cost;
    bool operator<(const edge &tmp)const{
        if(cost!=tmp.cost)
            return cost<tmp.cost;
        if(u!=tmp.u)
            return u<tmp.u;
        return v<tmp.v;
    }
}e[20010];
int fa[5010];
void init(){
    for(int i=1;i<=n;i++)
        fa[i]=i;
}
int find(int x){
    if(x==fa[x])
        return x;
    return fa[x]=find(fa[x]);
}
bool same(int x,int y){
    return find(x)==find(y);
}
void unite(int x,int y){
    x=find(x);
    y=find(y);
    if(x==y)
        return;
    fa[y]=x;
}
int Kruscal(){
    int MST=0;
    init();
    sort(e+1,e+1+m);//e为边集 
    for(int i=1;i<=m;i++){
        if(same(e[i].u,e[i].v)){
            unite(e[i].u,e[i].v);
            MST+=e[i].cost;
        }
    }
    return MST;
}
int main(){
    scanf("%d %d",&n,&m);
    for(int i=1;i<=n;i++)
        scanf("%d %d %d",&e[i].u,&e[i].v,&e[i].cost);
    printf("%d",Kruscal());
    return 0;
}

Bellman-Ford算法

  • B e l l m a n − F o r d Bellman-Ford BellmanFord算法解决的是一般情况下的单源最短路径问题,在这里,边的权值可以为负数
  • B e l l m a n − F o r d Bellman-Ford BellmanFord算法返回一个布尔值,以表明是否存在一个从原点可以到达的权值为负数的环。
  • 如果存在这样的环,算法将告诉我们不存在解决方案。如果没有这种环存在,算法将给出最短路径和它们的权值
  • 算法执行 n − 1 n-1 n1次,每次遍历边集,至少会找到一个点,使得新的最短路径比原来的更小

伪代码

for i from 1 to n-1: //算法需要重复执行n-1次
    for j from 1 to m: //遍历每一条边
        if dis[e[j].u]+e[j].w<dis[e[j].v]: //这个if操作叫做松弛
            dis[e[j].v]=dis[e[j].u]+e[j].w
for j from 1 to m: //再次执行一次算法
    if dis[e[j].u]+e[j].w<dis[e[j].v]: //此时所有的点的最短路径都算出来了
        return false //如果有边还能松弛,则有负环
return true //没有负环
  • 通过伪代码可以看出, B e l l m a n − F o r d Bellman-Ford BellmanFord的时间复杂度为 O ( n m ) O(nm) O(nm),完全劣于 D i j k s t r a Dijkstra Dijkstra算法的 O ( n 2 ) O(n^2) O(n2)时间复杂度(也劣于优化的 O ( m log ⁡ n ) O(m \log n) O(mlogn)时间复杂度)
    模版
#include <iostream>
#include <cstdio>
#include <cstring>
#include <vector>
using namespace std;
int n,m,st,dis[1010];
struct edge{
    int to,cost;
};
vector<edge> g[1010];
bool Bellman_Ford(int s){
    memset(dis,0x3f,sizeof(dis));
    dis[s]=0;
    for(int i=1;i<=n-1;i++){
        for(int j=1;j<=n;j++){
            for(int k=0;k<g[j].size();k++){
                //两层循环遍历整个邻接表 
                edge e=g[j][k];
                if(dis[j]+e.cost<dis[e.to])
                    dis[e.to]=dis[j]+e.cost;
            }
        }
    }
    for(int j=1;j<=n;j++){
        for(int k=0;k<g[j].size();k++){
            //两层循环遍历整个邻接表 
            edge e=g[j][k];
            if(dis[j]+e.cost<dis[e.to])
                return false;
        }
    }
    return true;
    //时间复杂度为O(nm),劣于Dijkstra 
}
int main(){
    cin>>n>>m>>st;
    for(int i=1;i<=n;i++){
        int u,v,w;
        cin>>u>>v>>w;
        g[u].push_back((edge){v,w});
    }
    if(Bellman_Ford(st)){
        for(int i=1;i<=n;i++)
            cout<<dis[i]<<" ";
    }
    else{
        cout<<"No";
    }
    return 0;
}

SPFA

  • S P F A SPFA SPFA B e l l m a n − F o r d Bellman-Ford BellmanFord算法的一种队列实现,减少了不必要的冗余计算
  • 主要思想初始时将起点加入队列。每次从队列中取出一个元素,并对所有与它相邻的点进行修改,若某个相邻的点修改成功,则将其入队。直到队列为空时算法结束
  • 这个算法,利用了每个店不会更新次数太多的特点发明的
  • S P F A SPFA SPFA在形式上和广度优先搜索非常类似,不同的是广度优先搜索中一个点除了队列就不可能重新进入队列,但是 S P F A SPFA SPFA中一个点可能在出队列之后再次被放入队列,也就是说一个点修稿过其它的点之后,过了一段时间可能会获得更短的路径,于是再次用来修改其它的点,这样反复进行下去
  • 时间复杂度: O ( k ⋅ m ) O(k \cdot m) O(km) m m m是边的数量, k k k是一个常数(不好分析),平均值为 2 2 2

核心代码

struct edge {
    int nxt, w;
};
queue<int> q;
int dis[1005];
bool exist[1005];//判断一个点是否已经加入了队列中 
void SPFA(int s) {
    memset(dis, 0x3f, sizeof(dis));
    memset(exist, false, sizeof(exist));
    dis[s] = 0;
    q.push(s);
    exist[s] = true;
    while(!q.empty()) {
        int h = q.front();
        q.pop();
        exist[h] = false;
        for(int i=0; i<g[h].size(); i++) {
            edge e = g[h][i];
            if(dis[e.nxt]>dis[h]+e.w) {
                dis[e.nxt] = dis[h]+e.w;
                if(!exist[e.nxt]) {
                    q.push(e.nxt);
                    exist[e.nxt] = true;
                }
            }
        }
    }
}
  • 6
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值