从城市A到城市B,有时候可以直达也可以途径其他城市到达,怎样选择最短的路径到达就是最短路问题。
分为单源最短路(所有点到某一特定点的最短路径)和多源最短路(任意两点间的最短路径)。根据边的正负也可以分为带负权边和不带负权边的最短路。
Dijkstra:用于解决不含负权边的单源最短路。基本思想:记S为已经找到到源点的最短路的点的集合,dis【i】表示顶点i到源点的最短距离。每次取不在S中的dis值最小的点u,将点u加入S并优化u周围点的dis值,重复直至所有点都在S中。
Dijkstra伪代码:
(1)初始化S,将源点加入S。
(2)初始化dis数组:for(T中每个与源点S有边相连的点u)dis【u】=min(dis【u】,w(u,s));
(2)while(S不包含所有的顶点)do{
u=min(dis【u】&& u不在S中);
u加入S;
for(每个不在S中的顶点v && dis【v】>dis【u】+w(u,v))dis【v】=dis【u】+w(u,v);
}
因为while循环和每次都要找dis最小的点,算法的时间复杂度为o(n^2),所以通常采用Dijkstra的队列优化版来解题(采用优先队列保存S中的点,这样就不用每次都去找dis最小的点)
队列优化模板:
/*zhizhaozhuo
Dijkstra优先队列模板 AND HUD-2544*/
#include<cstdio>
#include<cstring>
#include<queue>
#include<vector>
using namespace std;
const int maxn=110,INF=1e9;
int vis[maxn],dis[maxn];
struct node{
int to,len;
node(int to,int len):to(to),len(len){} //方便插入vector
bool operator<(const node& a)const{return len>a.len;}//用于优先队列排序,从小到大排
};
vector<node>Map[maxn];
void Dijkstra(int s,int n){
for(int i=1;i<=n;i++)dis[i]=INF;
memset(vis,0,sizeof(vis));
dis[1]=0;
priority_queue<node>Q;
Q.push(node(1,dis[1]));
while(!Q.empty()){
node u=Q.top();Q.pop();
if(vis[u.to])continue;
vis[u.to]=1;
for(int i=0;i<Map[u.to].size();i++){
node v=Map[u.to][i];
if(dis[v.to]>dis[u.to]+v.len){
dis[v.to]=dis[u.to]+v.len;
Q.push(node(v.to,dis[v.to]));
}
}
}
}
int main(){
int n,m;
while(~scanf("%d%d",&n,&m)&&n){
for(int i=0;i<=n;i++)Map[i].clear();
for(int i=0;i<m;i++){
int a,b,c;
scanf("%d%d%d",&a,&b,&c);
Map[a].push_back(node(b,c));
Map[b].push_back(node(a,c));
}
Dijkstra(1,n);
printf("%d\n",dis[n]);
}
return 0;
}
UVA:12661 Funny Car Racing 。
每一条路有一个开放时间,关闭时间,通过时间。分两种情况判断
#include<iostream>
#include<cstring>
#include<cstdio>
#include<vector>
#include<queue>
using namespace std;
const int N=310,INF=1<<30;
int d[N];
bool inq[N];
struct Edge{
int u,v,a,b,t;
};
struct node{
int p,t;
node(int p=0,int t=0):p(p),t(t){}
bool operator<(const node&a)const{return t>a.t;}
};
vector<Edge>edges;
vector<int>G[N];
int n,m,S,T;
void AddEdge(int u,int v,int a,int b,int t){
edges.push_back((Edge){u,v,a,b,t});
int m=edges.size();
G[u].push_back(m-1);
}
void Dijkstra(){
priority_queue<node>Q;
memset(inq,0,sizeof(inq));
for(int i=1;i<=N;i++)d[i]=INF;
d[S]=0;
inq[S]=true;
Q.push(node(S,0));
while(!Q.empty()){
node now=Q.top();Q.pop();
inq[now.p]=false;
for(int i=0;i<G[now.p].size();i++){
Edge e=edges[G[now.p][i]];
int l1=d[now.p]%(e.a+e.b),l2;
if(l1+e.t<=e.a)l2=e.t; //能直接通过
else l2=e.a+e.b-l1+e.t; //需要等待
if(d[now.p]+l2<d[e.v]){
d[e.v]=d[now.p]+l2;
if(!inq[e.v])Q.push(node(e.v,d[e.v]));
}
}
}
}
int main(){
int Case=0;
while(~scanf("%d%d%d%d",&n,&m,&S,&T)){
for(int i=1;i<=n;i++)G[i].clear();
edges.clear();
for(int i=0;i<m;i++){
int u,v,a,b,t;
cin>>u>>v>>a>>b>>t;
if(a>=t)AddEdge(u,v,a,b,t);
}
Dijkstra();
printf("Case %d: %d\n",++Case,d[T]);
}
return 0;
}
Floyd算法(多源最短路径算法):因为dis的值需要初始化为INF,这时候就存在一个潜在的问题。如果INF太小可能会使INF的边成为最短路的一部分,如果INF过大d[i][k]+d[k][j]可能会溢出。
主算法中只需要将 d[i][j]=min(d[i][j],d[i][k]+d[k][j]) 改为 d[i][j]=d[i][j] || (d[i][k]&&d[k][j]))结果就是有向图的传递闭包
void Floyd(int n){
for(int k=0;k<n;k++){
for(int i=0;i<n;i++){
for(int j=0;j<n;j++)if(i!=j&&i!=k&&j!=k){
if(d[i][j]<INF && d[k][j]<INF)
d[i][j]=min(d[i][j],d[i][k]+d[k][j]);
}
}
}
}
Floyd算法例题:FZU 2271 (在不改变所有点之间最短距离的情况下删除尽可能多的边)
重边与自环输入时就处理掉,重边保留最小的边。对题目的图进行两次Floyd,一次找到所有点之间的最短距离,第二次则模拟floyd的优化过程,如果当前的距离大于已得到最短距离或者存在一个中转点使得距离相等,那么这条边就可以删除。
/*zhizhaozhuo FZU 2271*/
#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
const int N=150,inf=1e8;
int map[N][N],b[N][N];
int n,m;
int main(){
int t,x,y,z,Case=0;
scanf("%d",&t);
while(t--){
int cnt=0;
memset(map,0,sizeof(map));
scanf("%d%d",&n,&m);
for(int i=1; i<=n; i++)
for(int j=1; j<=n; j++)
map[i][j]=(i==j)?0:inf;//图的预处理
for(int i=1; i<=m; i++){
scanf("%d%d%d",&x,&y,&z);
if(map[x][y]!=inf)cnt++;//处理重边与自环
if(map[x][y]>z)map[x][y]=map[y][x]=z;//保留最短的边
}
memcpy(b,map,sizeof(b));
for(int k=1;k<=n;k++)//第一次floyd
for(int i=1;i<=n;i++)
for(int j=1;j<=n;j++)if(map[i][k]+map[k][j]<map[i][j])map[i][j]=map[i][k]+map[k][j];
for(int i=1;i<=n;i++)//第二次floyd
for(int j=i+1; j<=n; j++)
for(int k=1; k<=n; k++){
if(map[i][j]<b[i][j] && b[i][j]!=inf && (i!=j&&k!=j&&i!=k)) {cnt++;break;} //距离大于最短距离
if(map[i][j]==map[i][k]+map[k][j] && b[i][j]!=inf &&(i!=j&&k!=j&&i!=k)) {cnt++;break;}//存在中转点
}
printf("Case %d: %d\n",++Case,cnt);
}
return 0;
}
UVA-10048 任意两点间可能有多条路径,每一条路有一个噪音值,使得经过两点的最大噪音值最小
#include<cstdio>
#include<iostream>
#include<cmath>
#include<cstring>
using namespace std;
const int N=100 +10;
int C;
int Map[N][N];
void floyd(){
for(int k=1;k<=C;k++){
for(int i=1;i<=C;i++){
for(int j=1;j<=C;j++){
if(Map[i][k]!=-1 && Map[k][j]!=-1){
int temp=max(Map[i][k],Map[k][j]);
if(Map[i][j]==-1 || Map[i][j]>temp)Map[i][j]=temp;
}
}
}
}
}
int main(){
int T=0,S,Q;
while(scanf("%d%d%d",&C,&S,&Q)==3 &&C){
for(int i=1;i<=C;i++)for(int j=1;j<=C;j++)Map[i][j]=-1;
int c1,c2,d;
while(S--){
scanf("%d%d%d",&c1,&c2,&d);
Map[c1][c2]=d;
Map[c2][c1]=d;
}
floyd();
if(T++)printf("\n");
printf("Case #%d\n",T);
while(Q--){
scanf("%d%d",&c1,&c2);
int ans=Map[c1][c2];
if(ans==-1)printf("no path\n");
else printf("%d\n",ans);
}
}
return 0;
}
Bellman-Ford算法:解决存在负权边的单源最短路径问题,时间复杂度为o(V*E)
(1)dis【i】为源点s与顶点i之间的路径长度,初始时dis【i】=INF,dis【s】=0;
(2)枚举每一条边(u,v)若dis【v】> dis【u】+w(u,v)那么dis【v】= dis【u】+w(u,v)
(3)如果步骤(2)没有更新dis数组,说明最短路查找完毕,否则重复执行(2),但至多执行n-1次
(4)检测图中是否存在负环路,枚举每一条边(u,v),如果存在 dis【v】> dis【u】+w(u,v)的边,则存在负环,反之找到了最短路。
SPFA算法:Bellman-Ford算法的队列实现,减少了不必要的冗余计算(比赛中一般使用SPFA算法)
在Bellman-Ford算法中,并不是每个顶点都会在松弛操作中改变,每次都枚举边所进行的冗余计算是不必要的时间花费。SPFA算法加入一个队列保存信息,初始时队列中只有源点,dis记录源点到所有点的最短路径。然后用队列里的点更新dis数组的值,如果某一点的dis值被更新,则将该点加入队尾,重复执行直至队列为空,如果某一点进入队列的次数超过n次,则存在负环。
下面给出白书的模板(紫书的模板是从起点出发的,如果没有找到负环可能只是起点到达不了负环,白书在开始时就将点都加入了队列,就能保证找到负环)
struct Edge{
int from,to,dist;
Edge(int f,int t,int d):from(f),to(t),dist(d){}
};
struct BellmanFord{
int n,m;
vector<Edge>edges;
vector<int>G[maxn];
bool inq[maxn];
int d[maxn],p[maxn],cnt[maxn];
void init(int n){
this->n=n;
for(int i=0;i<n;i++)G[i].clear();
edges.clear();
}
void AddEdge(int from,int to,int dist){
edges.push_back(Edge(from,to,dist));
m=edges.size();
G[from].push_back(m-1);
}
bool negativeCycle(int s){
queue<int>Q;
memset(inq,0,sizeof(inq));
memset(cnt,0,sizeof(cnt));
for(int i=0;i<n;i++){d[i]=0;inq[0]=true;Q.push(i);}
while(!Q.empty()){
int u=Q.front();Q.pop();
inq[u]=false;
for(int i=0;i<G[u].size();i++){
Edge& e=edges[G[u][i]];
if(d[e.to] > d[u]+e.dist){
d[e.to]=d[u]+e.dist;
p[e.to]=G[u][i];
if(!inq[e.to]){
Q.push(e.to);inq[e.to]=true;
if(++cnt[e.to]>n)return true;
}
}
}
}
return false;
}
};
BellmanFord solve;