算法和数据结构——有权图(最小生成树、最短路径计算)

普力姆算法(Prim's Algorithm)是求图G=(V,E)最小生成树(MST)的代表性算法之一,基本思路如下:

  • 从G中选取任一顶点r作为MST的根,将其添加至T
  • 循环执行下述处理直至T=V
    • 在连接T内顶点与V-T内定点的边中选取最小的边(p_{u},u),将其作为MST的边,并将u添加至T

实现这一算法的关键,在于选择边时“如何保存权值最小的边”。用邻接矩阵来实现prim算法需要准备一下变量:

color[n]用于记录顶点访问状态WHITE、GRAY、BLACK

M[n][n]

邻接矩阵,M[u][v]为u到v的权值

d[n]记录权值最小的值
p[n]p[u]记录u的父结点
int prim(int s){
    int u,minv;
    int color[MAX],d[MAX],p[MAX];
    for(int i=0;i<n;i++){
        d[i]=INFTY;
        p[i]=-1;
        color[i]=WHITE;
    }
    //起点初始值
    d[s]=0;
    while(1){
        u=-1;
        minv=INFTY;
        for(int i=0;i<n;i++){
            if(color[i]!=BLACK&&minv>d[i]){
                minv=d[i];
                u=i;
            }
        }
        //u=-1时代表寻找完毕
        if(u==-1)
            break;

        color[u]=BLACK;
        //从u出发更新到v的权值        
        for(int v=0;v<n;v++){    
            if(color[v]!=BLACK&&M[u][v]!=INFTY){
                if(d[v]>M[u][v]){
                    d[v]=M[u][v];
                    color[v]=GRAY;
                    p[v]=u;
                }
            }
        }
    }
    int sum=0;
    for(int i=0;i<n;i++){
        if(p[i]!=-1)
            sum+=M[p[i]][i];
    }
    return sum;
}

克鲁斯卡尔(Kruskal)算法:使用并查集来将边一条一条的插入。判断放入的点是否已经存在了边(有相同的祖先)。

 

struct Msg{
    int x,y;
    int l;
}sdge[6000];
long findfa(long x){
    return fa[x]==x?x:(fa[x]=findfa(fa[x]));
}
void merge(long x,long y){
    fa[findfa(x)]=findfa(y);
}
long kruskal(){
    long cnt=0;
    long long sum=0;
    for(long i=0;i<m;i++){
        long fx=findfa(edge[i].x);
        long fy=findfa(edge[i].y);
        if(fx!=fy){
            merge(fx,fy);
            cnt++;
            sum+=edge[i].l;
            if(cnt>=n-1)
                break;
        }
    }
    return sum;
}

单源最短路径:求从一个点出发到各点的最短距离。

狄克斯特拉(Dijkstra)算法:

void dijkstra(int s){
    int minv;
    int d[max],color[max];
    for(int i=0;i<n;i++){
        d[i]=INFTY;
        color[i]=WHITE;
    }
    d[s]=0;
    color[s]=GRAY;
    while(1){
        minv=INFTY;
        int u=-1;
        for(int i=0;i<n;i++){
            if(minv>d[i]&&color[i]!=BLACK){
                u=i;
                minv=d[i];
            }
        }
        if(u==-1)
            break;
        color[u]=BLACK;
        for(int v=0;v<n;v++){
            if(color[v]!=BLACK&&M[u][v]!=INFTY)
                if(d[v]>d[u]+M[u][v]){
                    d[v]=d[u]+M[u][v];
                    color[v]=GRAY;
                }
        }
    }
    for(int i=0;i<n;i++){
        cout<<i<<" "<<(d[i]==INFTY?-1:d[i])<<endl;   
    }
}

此方法的Dijkstra算法寻找最小边需要花费O(|V|)。接下来用优先队列来替换循环搜索。

#include<iostream>
#include<algorithm>
#include<vector>
#include<queue>
using namespace std;
static const int MAX = 10000;
static const int INFTY = (1<<20);
static const int WHITE = 0;
static const int GRAY = 1;
static const int BLACK = 2;
int n;
vector<pair<int,int> >adj[MAX];
/*
	优先队列+狄克斯特拉算法
	需要从队列中取出|V|次顶点
	队列中插入|E|次操作
	算法复杂度为O((|V|+|E|)log|V|) 
*/
void dijkstra_PQ(int s){
	priority_queue<pair<int,int> > PQ;
	int color[MAX];
	int d[MAX];
	//初始化 
	for(int i=0;i<n;i++){
		d[i]=INFTY;
		color[i]=WHITE;
	}
	//起点赋值 
	d[s]=0;
	PQ.push(make_pair(0,s));
	color[s]=GRAY;
	
	while(!PQ.empty()){
		//取出队首(权值最小)的结点 
		pair<int,int> f=PQ.top();
		PQ.pop();
		//取出结点编号 
		int u=f.second;
		
		color[u]=BLACK;
		//取出的权值大于已有的值 
		if(d[u]<f.first*(-1))
			continue;
		//否则更新以u点出发能抵达的v结点权值 
		for(int j=0;j<adj[u].size();j++){
			//取出u邻接的结点v 
			int v=adj[u][j].first;
			//判断v是否已使用 
			if(color[v]==BLACK)
				continue;	
			if(d[v]>d[u]+adj[u][j].second){
				d[v]=d[u]+adj[u][j].second;
				//priority_queue 默认优先最大值 因此要×(-1)
				//将更新后的结点放入优先队列中 
				PQ.push(make_pair(d[v]*(-1),v));
				color[v]=GRAY; 
			}
		} 
	}
	for(int i=0;i<n;i++){
		cout<<i<<" "<<(d[i]==INFTY?-1:d[i])<< endl;
	}
}
int main(){
	int k,u,v,c;
	cin>>n;
	for(int i=0;i<n;i++){
		cin>>u>>k;
		for(int j=0;j<k;j++){
			cin>>v>>c;
			adj[u].push_back(make_pair(v,c));
		}
	}
	dijkstra_PQ(0);
	return 0;
}

注:该算法不能出现负权边,如果求各个顶点之间的最短距离,我们可以将各个顶点作为起点执行|V|次狄克斯特拉算法来求解这类问题。这样做的算法复杂度为O(|V|^3) 。用优先级队列实现的话可以简化至O(|V|*(|E|*log|V|)。


Ford算法:从源点逐次绕过其他顶点,以缩短达到终点的最短路径长度方法,在解决APSP(All Pairs Shortest Path),复杂度为O(|V|^3)的弗洛伊德算法广为人知。而且弗洛伊德算法不需要G的所有边均非负,只要G不包含负环即可正常执行。负环指所有边的权值之和负的环。这种可以让两点之间的成本无限缩小,因此无法定义最短路径。

弗洛伊德算法的另一个功能就是判断G中是否存在负环。算法执行结束时,如果G的某顶点v到v顶点(其自身)距离为负,就是证明G中存在负环。利用动态规划的思想实现噢,找一个中间节点来跟不断地更新距离。

#include<iostream>
#include<algorithm>
#include<vector>
#include<climits>
using namespace std;
static const int MAX = 100;
static const long long INFTY = (1LL<<32);
int n;
long long d[MAX][MAX];
void floyd(){
	for(int k=0;k<n;k++){
		for(int i=0;i<n;i++){
			if(d[i][k]==INFTY)
				continue;
			for(int j=0;j<n;j++){
				if(d[k][j]==INFTY)
					continue;
				d[i][j]=min(d[i][j],d[i][k]+d[k][j]);
			}
		}
	}
}  
int main(){
	int e,u,v,c;
	cin>>n>>e;
	for(int i=0;i<n;i++){
		for(int j=0;j<n;j++){
			d[i][j]=((i==j)?0:INFTY);
		}
	}
	for(int i=0;i<e;i++){
		cin>>u>>v>>c;
		d[u][v]=c;
	}
	floyd();
	bool negative = false;
	//判断是否有负环产生
	for(int i=0;i<n;i++)
		if(d[i][i]<0)
			negative=true;
	if(negative){
		cout<<"NEGATIVE CYCLE"<<endl;
	}else{
		for(int i=0;i<n;i++){
			for(int j=0;j<n;j++){
				if(j)
					cout<<" ";
				if(d[i][j]==INFTY) 
					cout<<"INF";
				else
					cout<<d[i][j]; 
			} 
			cout<<endl;
		}
	}
	return 0;
} 
/*
4 6
0 1 1
0 2 5
1 2 2
1 3 4
2 3 1
3 2 7
0 1 3 4
INF 0 2 3
INF INF 0 1
INF INF 7 0

4 6
0 1 1
0 2 -5
1 2 2
1 3 4
2 3 1
3 2 7
0 1 -5 -4
INF 0 2 3
INF INF 0 1
INF INF 7 0

4 6
0 1 1
0 2 5
1 2 2
1 3 4
2 3 1
3 2 -7
NEGATIVE CYCLE
*/

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值