普力姆算法(Prim's Algorithm)是求图G=(V,E)最小生成树(MST)的代表性算法之一,基本思路如下:
- 从G中选取任一顶点r作为MST的根,将其添加至T
- 循环执行下述处理直至T=V
- 在连接T内顶点与V-T内定点的边中选取最小的边
,将其作为MST的边,并将u添加至T
- 在连接T内顶点与V-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
*/