注意与最小生成树的区分:①边的权重全都相同,可以用prim和Kruskal算法。
②权值不相同,就要用以下的算法。
一、单源最短路
1、所有边都是正数
①朴素的Dijkstra算法(适用于稠密图,用邻接矩阵存图)O(n^2)
思想:每次将离起点最近的点加入起点所在的集合S,然后更新每个点到集合S的最小距离。
步骤:①初始化距离memset(d,INF,sizeof(d)),d[1]=0;②循环遍历不在集合S中的且离S最近的距离。
#include<iostream>
#include<cstring>
#include<cmath>
using namespace std;
const int N=510,INF=0x3f3f3f3f;
int g[N][N],d[N];
bool st[N];
int n,m;
int dijkstra()
{
memset(d,INF,sizeof(d));
d[1]=0;
for(int i=1;i<=n;i++)
{
int t=-1;
for(int j=1;j<=n;j++)//找出到1这个集合的最小距离
if(!st[j]&&(t==-1||d[t]>d[j]))
t=j;
st[t]=true;
for(int j=1;j<=n;j++)//更新每个点到集合的最小距离
d[j]=min(d[j],d[t]+g[t][j]);
}
if(d[n]==INF) return -1;
return d[n];
}
int main()
{
cin>>n>>m;
memset(g,INF,sizeof(g));
int a,b,c;
for(int i=0;i<m;i++)
{
cin>>a>>b>>c;
g[a][b]=min(g[a][b],c);//针对重边和自环
}
int t=dijkstra();
cout<<t<<endl;
return 0;
}
②堆优化的Dijkstra算法(对于稀疏图,用邻接表存图)O(mlogn)
思路:用优先队列存储每条边的权值,每次取肯定取出的是所有边,在更新每个点到起点的最小值的时候,是更新当前t的所有出边。
#include<iostream>
#include<cstring>
#include<queue>
using namespace std;
typedef pair<int,int> PII;
const int N=1.5e5+10,INF=0x3f3f3f3f;
int e[N],w[N],ne[N],h[N],idx;
int d[N];
bool st[N];
int n,m;
void add(int a,int b,int c)
{
e[idx]=b,w[idx]=c,ne[idx]=h[a],h[a]=idx++;
}
int dijkstra()
{
memset(d,INF,sizeof(d));
d[1]=0;
priority_queue<PII,vector<PII>,greater<PII>> heap;//小根堆
heap.push({0,1});
while(heap.size())
{
auto t=heap.top();
heap.pop();
int dis=t.first,ver=t.second;
if(st[ver]) continue;
st[ver]=true;
for(int i=h[ver];i!=-1;i=ne[i])//遍历从t出去的所有边
{
int j=e[i];
if(d[j]>dis+w[i])
{
d[j]=dis+w[i];
heap.push({d[j],j});
}
}
}
if(d[n]==INF) return -1;
return d[n];
}
int main()
{
cin>>n>>m;
memset(h,-1,sizeof(h));
int x,y,z;
for(int i=0;i<m;i++)
{
cin>>x>>y>>z;
add(x,y,z);
}
int t=dijkstra();
cout<<t<<endl;
return 0;
}
2、存在负边权
①Bellman-Ford(适用于有限制边数的情况)O(nm)
题意:求从点1到点n的最多经过k条边的最短路径,可能存在重边和自环,边权可能为负。
思想:通过不断更新每条边连接的两个点例如a→b,间接通过a来更新b到1的距离,所以如果不存在可以从1到n的路径,每个点也还是会被更新(只要有边连接)。
#include<iostream>
#include<cstring>
using namespace std;
const int N=1e4+10,INF=0x3f3f3f3f;
int d[510],backup[510];
int n,m,k;
struct Edge
{
int a,b,w;
}e[N];
int bellman_ford()
{
memset(d,INF,sizeof(d));
d[1]=0;//初始化勿忘
for(int i=0;i<k;i++)
{
memcpy(backup,d,sizeof(d));//先备份一份d[],这样就不会修改了一条边后导致后面的也跟着变化,因为这里是有k的限制的
for(int j=0;j<m;j++)
{
int a=e[j].a,b=e[j].b,w=e[j].w;
d[b]=min(d[b],backup[a]+w);
}
}
if(d[n]>INF/2) return INF;//为什么不能是d[n]==INF,因为有可能存在从1到达不了n,但是有其他的
//点可以到达n(边权为负),此时d[n]是被更新了的,不再是INF。
return d[n];
}
int main()
{
cin>>n>>m>>k;
for(int i=0;i<m;i++)
cin>>e[i].a>>e[i].b>>e[i].w;
int t=bellman_ford();
if(t==INF)
cout<<"impossible"<<endl;
else
cout<<t<<endl;
return 0;
}
② spfa算法(堆bellman_frod的优化)
思路:它只遍历当前被修改了的点的所有后继结点的权值并把他们入队,其他没有发生变化的就先不遍历。
#include<iostream>
#include<cstring>
#include<queue>
using namespace std;
const int N=1e5+10,INF=0x3f3f3f3f;
int e[N],w[N],ne[N],h[N],idx;
int d[N];
bool st[N];
int n,m;
void add(int a,int b,int c)
{
e[idx]=b,w[idx]=c,ne[idx]=h[a],h[a]=idx++;
}
int spfa()
{
memset(d,INF,sizeof(d));
d[1]=0;
queue<int> q;
q.push(1);
st[1]=true;
while(q.size())
{
int t=q.front();
q.pop();
st[t]=false;
for(int i=h[t];i!=-1;i=ne[i])
{
int j=e[i];
if(d[j]>d[t]+w[i])
{
d[j]=d[t]+w[i];
if(!st[j])
{
q.push(j);
st[j]=true;
}
}
}
}
return d[n];//这里可以直接返回d[n]来判断有没有路径的,因为进行更新的点都是从1开始拓展出去的
}
int main()
{
memset(h,-1,sizeof(h));
scanf("%d%d",&n,&m);
int x,y,z;
for(int i=0;i<m;i++)
{
scanf("%d%d%d",&x,&y,&z);
add(x,y,z);
}
int t=spfa();
if(t==INF) printf("impossible\n");
else printf("%d\n",t);
return 0;
}
AcWing 852. spfa判断负环
思路:增加一个数组cnt[i]来记录到达i点时经过的路径数量,当cnt[i]>=n时,说明此时这条路径经过的点的数量大于n+1,所以一定有点重复了,即存在负环。为什么是负环?这里d数组不用进行初始化?他会从第一次遇到负数的边开始找起,因为只要存在负环一定可以找到一个点(这个环里的点)使往后的所有边的权和为负数,然后无线循环下去直到cnt[i]>=n。
#include<iostream>
#include<cstring>
#include<queue>
using namespace std;
const int N=2010,M=1e4+10;
int h[M],e[M],w[M],ne[M],idx;
int d[N],cnt[N];
bool st[N];
int n,m;
void add(int a,int b,int c)
{
e[idx]=b,w[idx]=c,ne[idx]=h[a],h[a]=idx++;
}
bool spfa()
{
queue<int> q;
for(int i=1;i<=n;i++)
{
q.push(i);
st[i]=true;
}
while(q.size())
{
int t=q.front();
q.pop();
st[t]=false;
for(int i=h[t];i!=-1;i=ne[i])
{
int j=e[i];
if(d[j]>d[t]+w[i])
{
d[j]=d[t]+w[i];
cnt[j]=cnt[t]+1;
if(cnt[j]>=n) return true;//说明存在负环
if(!st[j])
{
q.push(j);
st[j]=true;
}
}
}
}
return false;
}
int main()
{
memset(h,-1,sizeof(h));
cin>>n>>m;
int x,y,z;
for(int i=0;i<m;i++)
{
cin>>x>>y>>z;
add(x,y,z);
}
if(spfa()) cout<<"Yes"<<endl;
else cout<<"No"<<endl;
return 0;
}
二、多源汇最短路
①Floyd算法 O(n^3)
思路:动态规划,d[i][j]表示从i到j的最短路径,k是i到j的过程中可能经过的其它点,所以d[i][j]=min(d[i][j],d[i][k]+d[k][j])。
#include<iostream>
using namespace std;
const int N=210,INF=0x3f3f3f3f;
int d[N][N];
int n,m,q;
void floyd()
{
for(int k=1;k<=n;k++)
for(int i=1;i<=n;i++)
for(int j=1;j<=n;j++)
d[i][j]=min(d[i][j],d[i][k]+d[k][j]);
}
int main()
{
scanf("%d%d%d",&n,&m,&q);
for(int i=1;i<=n;i++)
for(int j=1;j<=n;j++)
{
if(i==j) d[i][j]=0;//自环
else d[i][j]=INF;
}
int x,y,z;
for(int i=0;i<m;i++)
{
scanf("%d%d%d",&x,&y,&z);
d[x][y]=min(d[x][y],z);//重边
}
floyd();
while(q--)
{
scanf("%d%d",&x,&y);
if(d[x][y]>INF/2) //同样存在x到达不了y,但x,y也被更新了的情况
printf("impossible\n");
else printf("%d\n",d[x][y]);
}
return 0;
}