基本思想:找到最短距离已经确定的顶点(最初只有起点),从它出发更新相邻顶点的最短距离。把每个顶点当前的最短距离用堆维护,在每次更新时往堆里插入当前最短距离和顶点的值对,而每次从堆中取出的最小值就是下一次要使用的顶点。当取出的最小值不是最短距离的话,就丢弃这个值。当堆为空时,算法结束。
链式前向星法
int N,M,dix; //图的最大顶点数和边数
int h[N]; //顶点数组
struct node{
int e,v,next;
}edge[M];
//添加边
void add(int a,int b,int c) //边起点、终点、权值
{
edge[++dix].e=b;
edge[dix].v=c;
edge[dix].next=h[a];
h[a]=dix;
}
例题洛谷P4479(此题用SPFA会卡死)
c++代码
#include
using namespace std;
typedef pair<int,int> P; //first是最短距离,second是顶点的编号
const int N=100010,M=200010,inf=INT_MAX;
int n,m,s,dix;
int h[N],e[M],v[M],next[M],d[N]; //用数组代替结构体,思想是一样的
priority_queue<P,vector<P>,greater<P> > q; //堆按照first从小到大的顺序取出值
void add(int a,int b,int c){
e[dix]=b,v[dix]=c,next[dix]=h[a],h[a]=dix++;
}
void Dijkstra(){
while(!q.empty()){
P p=q.top();
q.pop();
int t=p.second;
if(d[t]<p.first) continue; //当前取出的最小值不是最短距离,丢弃这个值
for(int i=h[t];i!=-1;i=next[i]){
if(d[e[i]]>d[t]+v[i]){
d[e[i]]=d[t]+v[i];
q.push({d[e[i]],e[i]});
}
}
}
}
int main(){
cin>>n>>m>>s;
fill(d+1,d+n+1,inf);
fill(h+1,h+n+1,-1);
for(int i=0;i<m;i++){
int a,b,c;
cin>>a>>b>>c;
add(a,b,c);
}
d[s]=0;
q.push({d[s],s});
Dijkstra();
for(int i=1;i<=n;i++)
cout<<d[i]<<" ";
return 0;
}
基本思想:建立一个队列,初始时队列里只有起始点,再建立一个表格记录起始点到所有点的最短路径(该表格的初始值要赋为极大值,该点到他本身的路径赋为0)。然后执行松弛操作,用队列里有的点作为起始点去刷新到所有点的最短路,如果刷新成功且被刷新点不在队列中则把该点加入到队列最后。重复执行直到队列为空。
判断有无负环:如果某个点进入队列的次数超过N次则存在负环(SPFA无法处理带负环的图)
例题洛谷P3371(上题的弱化版)
c++代码
#include
using namespace std;
const int N=100010,M=200010,inf=INT_MAX;
int n,m,s,dix;
int h[N],e[M],v[M],next[M],d[N],vis_count[N];
bool vis[N];
queue<int> q;
void add(int a,int b,int c){
e[dix]=b,v[dix]=c,next[dix]=h[a],h[a]=dix++;
}
bool spfa(){
while(!q.empty()){
int t=q.front();
q.pop();
if(vis_count[t]++>n) return true; //返回正值代表存在从s可达的负圈
vis[t]=false;
for(int i=h[t];i!=-1;i=next[i]){
if(d[e[i]]>d[t]+v[i]){
d[e[i]]=d[t]+v[i];
if(!vis[e[i]]){
q.push(e[i]);
v[e[i]]=true;
}
}
}
}
return false;
}
int main(){
cin>>n>>m>>s;
fill(d+1,d+n+1,inf);
fill(h+1,h+n+1,-1);
for(int i=0;i<m;i++){
int a,b,c;
cin>>a>>b>>c;
add(a,b,c);
}
d[s]=0;
q.push(s);
spfa();
for(int i=1;i<=n;i++)
cout<<d[i]<<" ";
return 0;
}
Dijkstra算法的复杂度是O(|E|log|V|),SPFA的复杂度是O(Km)(k为常数,一般不大于2),最坏复杂度是O(nm),遇到异常数据会被卡死,像上面第一题,所以一般使用Dijkstra算法。但是,在图中存在负边的情况下,Dijkstra算法就无法正确求解问题,还是需要用SPFA。
基本思想:
从任意一条单边路径开始。所有两点之间的距离是边的权,如果两点之间没有边相连,则权为无穷大。
对于每一对顶点 i 和 j,看看是否存在一个顶点 k 使得从 i 到 k 再到 j 比己知的路径更短。如果是更新它。
注意点:
1. Floyd-Warshall算法时间复杂度:O(n^3)
2. Floyd-Warshall算法可以处理边是负数的情况。而判断图中是否有负圈,只需检查是否存在d[i][i]是负数的顶点i就可以了。
//Floyd-Warshall算法核心语句
for(k=1;k<=n;k++)
for(i=1;i<=n;i++)
for(j=1;j<=n;j++)
if(e[i][k]<inf && e[k][j]<inf && e[i][j]>e[i][k]+e[k][j])
e[i][j]=e[i][k]+e[k][j];
基本思想:按照边的权值的顺序从小到大查看一遍,如果不产生圈(重边等也算在内),就把当前这条边加入到生成树中。
判断是否产生圈:假设现在要把连接顶点u和顶点v的边e加入生成树中。如果加入之前u和v不在同一个连通分量里,那么加入e也不会产生圈。反之,如果u和v在同一个连通分量里,那么一定会产生圈。可以使用并查集高效地判断是否属于同一个连通分量。
例题洛谷P3366
c++代码
#include
using namespace std;
const int maxn=2e5;
int par[maxn],rank[maxn];
int n,m,res=0;
struct edge{
int u,v,cost;
}es[maxn];
bool cmp(edge e1,edge e2){
return e1.cost<e2.cost;
}
void init(int n){
for(int i=0;i<n;i++){
par[i]=i;
rank[i]=0;
}
}
int find(int x){
if(par[x]==x) return x;
return par[x]=find(par[x]);
}
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);
}
int main(){
cin>>n>>m;
for(int i=0;i<m;i++)
cin>>es[i].u>>es[i].v>>es[i].cost;
sort(es,es+m,cmp);
init(n);
for(int i=0;i<m;i++){
if(!same(es[i].u,es[i].v)){
unite(es[i].u,es[i].v);
res+=es[i].cost;
}
}
cout<<res;
return 0;
}
Kruskal算法的时间复杂度是O(|E|log|V|),适用于稀疏图。
等我想写了再补上(滑稽)
因为我感觉克鲁斯克尔算法已经够了qwq
2019.9.23 20:38 更新
上面那句话太打脸了,当遇到稠密图特别是完全图(图中每对顶点之间都有一条边)时,再用我最喜欢的Kruskal算法就要MLE了(难受)。
比如这道题 公路建路
没错就是这道题逼迫我去重新学了一遍Prim算法(完全图而且n=5000.。。。)
Prim算法基本思想:首先假设有一颗只包含一个顶点v的树T。然后贪心地选取T和其他顶点之间相连的最小权值的边,并把它加到T中。不断进行这个操作,就可以得到一颗生成树,可以证明通过这个方法得到的生成树就是最小生成树。
参考代码
#include
using namespace std;
const int inf=0x7fffffff;
int n;
bool v[5001]; //v[i]表示点i是否在当前生成树集合中
double d[5001],x[5001],y[5001],ans; //d[i]表示点i到当前集合的最短边的长度,ans表示最小生成树中所有边的总长度
double dis(double x,double y,double x1,double y1){ //计算两点之间的长度
return sqrt((x-x1)*(x-x1)+(y-y1)*(y-y1));
}
int main(){
cin>>n;
fill(d,d+n,inf);
for(int i=0;i<n;i++){
cin>>x[i]>>y[i];
}
d[0]=0;
for(int i=0;i<n;i++){
int t=-1,min_dis=inf;
for(int j=0;j<n;j++)
if(!v[j]&&d[j]<min_dis){
t=j;
min_dis=d[j];
}
v[t]=true;
ans+=d[t];
for(int j=0;j<n;j++){
double temp=dis(x[t],y[t],x[j],y[j]);
if(!v[j]) d[j]=min(d[j],temp);
}
}
printf("%.2f",ans);
return 0;
}
Prim算法时间复杂度:O(n^2),适用于稠密图。