<前导>
这里回顾四种计算最短路径的算法,分别为:
- Floyd算法;
- Dijkstra算法;
- Bellman-Ford算法;
- SPFA算法;
<Floyd算法>
Floyd算法的实现非常简单,直接看代码:
int d[maxn][maxn];//d[i][j]表示顶点i、j之间的最短距离
int inf=99999999;
for(int i=0;i<maxn;i++){
d[i][i]=0; //初始化
}
void Floyd(){
for(int k=0;k<n;k++){ //注意Floyd算法先循环中转点k
for(int i=0;i<n;i++){
for(int j=0;j<n;j++){
if(d[i][k]<inf&&d[k][j]<inf&&d[i][k]+d[k][j]<d[i][j])
d[i][j]=d[i][k]+d[k][j];
}
}
}
}
<dijkstra算法>
//以邻接矩阵为例
bool book[maxn]={false};
int d[maxn];
int G[maxn][maxn];//注意G也需要初始化,除了输入的边之外,其余不连通的边全部设为inf
int n,inf=99999999;
void Dijkstra(int s){
for(int i=0;i<maxn;i++){
d[i]=inf;//初始化
}
d[s]=0;
for(int i=0;i<n;i++){
int u,mind=inf;
for(int j=0;j<n;j++){ //该循环可以用堆优化,使复杂度降低至logn;
if(book[j]==false&&d[j]<mind){
u=j;
mind=d[j];
}
}
book[u]=true;
for(int v=0;v<n;v++){
if(book[v]==false&&G[u][v]<inf&&d[u]+G[u][v]<d[v])
d[v]=d[u]+G[u][v];
}
}
}
这种写法的复杂度为O(n*(n+n))=O(n^2),当然用堆优化之后会降至O(n*(logn+n)),如果再用上邻接表,那么复杂度会进一步降至O(n*(logn+m));
当然这是针对于顶点远小于边数的稀疏图来讲,在规定必须使用Dijkstra算法的情况下,对于储存图的方式,如果是顶点远大于边数的稀疏图,用邻接表比较适合,如果是边数大于顶点数的稠密图,用邻接矩阵计较适合;
<Dijkstra的拓展>
1.输出最短路径
核心依旧不变,只需要多开一个pre[maxn]数组记录每次更新d[v]时v的前一个结点即可;
int n,G[maxn][maxn];
int d[maxn],pre[maxn];
bool book[maxn]={false};
void Dijkstra(int s){
for(int i=0;i<maxn;i++){
d[i]=inf;
pre[i]=i;//初始化
}
d[s]=0;
for(int i=0;i<n;i++){
int u,mind=inf;
for(int j=0;j<n;j++){
if(book[j]==false&&d[j]<mind){
u=j;
mind=d[j];
}
}
book[u]=true;
for(int v=0;v<n;v++){
if(book[v]==false&&G[u][v]<inf&&d[u]+G[u][v]<d[v]){
d[v]=d[u]+G[u][v];
pre[v]=u;//记录前驱
}
}
}
}
//如果要输出,则可以用递归实现
void dfs(int s,int v){ //从终点开始递归
if(v==s){
cout<<s<<" ";
return;
}
dfs(s,pre[v]);
cout<<v<<" ";
}
2.除距离之外新增第二标尺,如物资、花费、最短路径不止一条等;
一个例子:
int n,G[maxn][maxn],weight[maxn];//新增点权
int d[maxn],w[maxn],num[maxn];//最短路径不止一条
bool book[maxn]={false};
void Dijkstra(int s){
for(int i=0;i<maxn;i++){
d[i]=inf;
w[i]=0;
num[i]=0;
}
d[s]=0;
w[s]=weight[s];
num[s]=1;
for(int i=0;i<n;i++){
int u,mind=inf;
for(int j=0;j<n;j++){
if(book[j]==false&&d[j]<mind){
u=j;
mind=d[j];
}
}
book[u]=true;
for(int v=0;v<n;v++){
if(book[v]==false&&G[u][v]<inf){
if(d[u]+G[u][v]<d[v]){
d[v]=d[u]+G[u][v];
w[v]=w[u]+weight[v];
num[v]=num[u];
}
else if(d[u]+G[u][v]==d[v]){
if(w[u]+weight[v]>w[v])
w[v]=w[u]+weight[v];
}
num[v]+=num[u];
}
}
}
}
3.Dijkstra与dfs结合处理复杂问题
vector<int> pre[maxn];
void Dijkstra(int s){
for(int i=0;i<maxn;i++){
d[i]=inf;
}
d[s]=0;
for(int i=0;i<n;i++){
int u,mind=inf;
for(int j=0;j<n;j++){
if(book[j]=false&&d[j]<mind){
u=j;
mind=d[j];
}
}
book[u]=true;
for(int v=0;v<n;v++){
if(book[v]==false&&G[u][v]<inf){
if(d[u]+G[u][v]<d[v]){
d[v]=d[u]+G[u][v];
pre[v].clear();
pre[v].push_back(u);
}
else if(d[u]+G[u][v]==d[v])
pre[v].push_back(u);
}
}
}
}
//dfs的书写
vector<int> temppath,path;
void dfs(int v){
if(v==s){
temppath.push_back(v);
int value;
for(int i=temppath.size()-1;i>0;i--){
//计算value
}
if(value<mincost){ //此处mincost只是借指,即在这里进行第二、甚至第三标尺的比较
mincost=value;
path=temppath;//更新path
}
temppath.popback();
return;
}
temppath.push_back(v);
for(int i=0;i<pre[v].size();i++){
dfs(pre[v][i]);
}
temppath.pop_back();
}
<Bellman-Ford算法>
Bellman-Ford算法的核心在于对所有的边进行松弛操纵,一步一步确立最短路径;
需要进行多少次松弛?如果顶点数是n,那么最多需要进行n-1轮,注意是最多,因为bf算法是一步步确定最短路径,因此也有可能不需要n-1次就已经得到了最短路径;
那么为什么是n-1次,因为最多只有n-1条边;
最短路径可能包含回路么?答案是不可能。回路要么正权回路,要么负权回路,正权回路显然不可能为最短路径,负权回路的话,每一次循环该回路都会使最短路径长度更小,那就没有最短路径了;
代码:
struct node{
int v,dis;
}
int n,d[maxn];
vector<node> Adj[maxn];
bool bf(int s){
for(int i=0;i<maxn;i++){
d[i]=inf;
}
d[s]=0;
for(int i=0;i<n-1;i++){
for(int u=0;u<n;u++){
for(int j=0;j<Adj[maxn].size();j++){
int v=Adj[u][j].v;
int dis=Adj[u][j].dis;
if(d[u]+dis<d[v])
d[v]=d[u]+dis;
}
}
}
for(int u=0;u<n;u++){ //判断是否有负权回路
for(int j=0;j<Adj[u].size();j++){
int v=Adj[u][j].v;
int dis=Adj[u][j].dis;
if(d[u]+dis<d[v]) //仍然可以被松弛
return false;
}
}
return true;
}
该算法的时间复杂度为O(NE),其中E为边的个数,多数情况下该算法的效率并不能让人满意,于是就有了优化后的Bellman-Ford算法;
<SPFA算法>
int n,d[maxn],num[maxn];//num数组判断是否有负环
vector<bode> Adj[maxn];
bool book[maxn]={false};
bool SPFA(int s){
for(int i=0;i<maxn;i++){
d[i]=inf;
num[i]=0;
}
queue<int> q;
q.push(s);
d[s]=0;
num[s]=1;
book[s]=true;
while(!q.empty()){
int u=q.front();
q.pop();
book[u]=false;
for(int j=0;j<Adj[u].size();j++){
int v=Adj[u][j].v;
int dis=Adj[u][j].dis;
if(d[u]+dis<d[v]){
d[v]=d[u]+dis;
if(!book[v]){
q.push(v);
book[v]=true;
num[v]++;
if(num[v]>=n)
return false;
}
}
}
}
return true;
}
SPFA算法的时间复杂度为O(kE),其中k为常数,且多数情况下不超过2,但如果有从起点可达的负环,那么这个复杂度就会退化为O(NE);
<小结>
Dijkstra与Floyd算法不能解决有负权的情况,且与点关系密切,适用于稀疏图,其中Floyd解决全源最短路径问题,Dijkstra解决单源最短路径问题;
Bellman-Ford与SPFA算法可以解决有负权;