对于具有n个顶点和m条边且边的权值非负的简单图(无重边和环),K短路,是指的起点s到终点t的最短路径中第k个最小的。K短路分为有限制的K短路和无限制的K短路,有限制的K短路是指求得的路径中不含有回路(路径上任何一个节点的出现次数不大于1次),无限制的K短路则对求得的路径中没有要求,这篇博客讨论后者。
1、基础理论
1.1 偏离路径
算法主要使用了偏离路径的思想,主要概念如下:
偏离点:两条路径中按顺序出现最后一个相同的点,即出现的第一个不相同的点的前一个点,也可以理解为从偏离点开始,两条路径开始不同
偏离边:两条路径中出现的第一条不相同的边
偏离路径:从偏离点到终点N的路径
设在一个图中有如下两条最短路径p1、p2,起点为节点0,终点为节点5,见上图所示,每条路径经过的顶点如下:
p1:0-2-3-5
p2:0-2-4-5
则p2相对于p1的偏离点是节点2,p2相对于p1的偏离边是2-4,p2相对于p1的偏离路径为2-4-5。
1.2 相关定义
在Yen的算法中,首先定义如下:
,是从起点到终点N的第K短路,其中
、
、...
代表第K短路经上第2个、第3个、第k个节点
:第k条最短路径
:第k-1条最短路径
是个集合,代表前k-1个最短路径的集合
是通过
求得的,求得的方式如下:
设=1,2,3,..
代表
路径上按照顺序的每个节点,在
路径上,从节点1到节点
的路径设为
,在节点
上偏离至一个新节点,设此新节点为
,要求此
节点不与集合
在节点
上的下一个节点相同,同时计算通过新节点
到达终点N的最短偏离路径,记为
,
路径中的节点不能
中的节点相同,则
,代表的是在偏离节点
上,相对于
的偏离路径。由上述步骤可知,
是无环的。
2 求解步骤
下面给出具体的求解的步骤。
首先维护两个链表ListA,ListB,ListA是已求得的集合,ListB是
的候选集合
(1)求。
的求得方式很多,dijkstra,spfa等。在Yen的论文中,求
是用Yen自己的方法,使用的图是不带负环的图。
(2)对于代表
路径上按照顺序的每个节点,已经求得
,如果
节点在集合
中出现过,设
=∞,其中q是
集合中的
节点。(
节点至少在
路径上出现过),结束本次循环后,
恢复原值。
(3)对图求得节点到终点N的最短偏离路径,即
,且这条路径上(
)的节点与前半部分路径(
上,节点1到节点
的路径,即
)的节点不能相同,如果有多个最短偏离路径,则任选其一
(4),将求得的
加入候选集合ListB。将ListB中的取最小值加入LIstA。如果有多个最小值,取节点数少的那个
(5)如果ListA中已经有K个,则算法结束,最后一个加入的即为第K短路,否则进入k+1次循环。
步骤说明:
(1)设=∞的目的是在偏离点
上强制偏离至一个新的节点,新的节点与
的
节点均不相同。这样形成的新路径总值不小于
,即
(2)对路径节点进行偏离,实质上是对除了终点之外的所有点进行偏离,即
,终点N不进行偏离
3求解过程
使用的无向图如下图所示,求
如上图所示,图中有6个顶点,9条边;即n=6,m=9。
先求得,
=A-D-E-F,值12,这样ListA、ListB的状态如下:
ListA | ListB |
![]() | |
(1)对=A-D-E-F进行扩展,求得
偏离点 | 在![]() ![]() | ![]() | 除![]() ![]() ![]() | 偏离路径 | 生成的候选路径 | 操作 |
A | A-D=∞ | NULL | NULL | A-B-C-F 16 | A-B-C-F 16 | 加入ListB |
D | D-E=∞ | A-D | NULL | D-B-C-F 14 | A-D-B-C-F 16 | 加入ListB |
E | E-F=∞ | A-D-E | NULL | E-C-F 9 | A-D-E-C-F 12 | 加入ListB |
第三条候选路径最短,将第三条路径A-D-E-C-F加入ListA,距离12,则此时=A-D-E-C-F,ListA、ListB的状态如下:
ListA | ListB |
![]() | A-B-C-F 16 |
![]() | A-D-B-C-F 16 |
(2)对=A-D-E-C-F进行扩展,求得
偏离点 | 在![]() ![]() | ![]() | 除![]() ![]() ![]() | 偏离路径 | 生成的候选路径 | 操作 |
A | A-D=∞ | NULL | NULL | A-B-C-F 16 | ListB中已有此路径,不再加入 | |
D | D-E=∞ | A-D | NULL | D-B-C-F 14 | ListB中已有此路径,不再加入 | |
E | E-C=∞ | A-D-E | E-F=∞ | E-B-C--F 12 | A-D-E-B-C-F 15 | 加入ListB |
C | C-F=∞ | A-D-E-C | NULL | 不存在 | 不存在 |
第一条、第二条候选路径已存在,将第三条候选路径加入ListB,并ListB中最短的路径A-D-E-B-C-F 加入ListA,此时
ListA、ListB的状态如下:
ListA | ListB |
![]() | A-B-C-F 16 |
![]() | A-D-B-C-F 16 |
![]() | |
(3)对=A-D-E-B-C-F进行扩展,求得
偏离点 | 在![]() ![]() | ![]() | 除![]() ![]() ![]() | 偏离路径 | 生成的候选路径 | 操作 |
A | A-D=∞ | NULL | NULL | A-B-C-F 16 | ListB中已有此路径,不再加入 | |
D | D-E=∞ | A-D | NULL | D-B-C-F 14 | ListB中已有此路径,不再加入 | |
E | E-B=∞ | A-D-E | E-C=∞,E-F=∞ | 不存在 | 不存在 | |
B | B-C=∞ | A-D-E-B | NULL | 不存在 | 不存在 | |
C | C-F=∞ | A-D-E-B-C | NULL | 不存在 | 不存在 |
此次循环,没有新的序列加入,将原ListB中距离最短的A-B-C-F的加入ListA中,此时ListA、ListB的状态如下:
ListA | ListB |
![]() | A-D-B-C-F 16 |
![]() | |
![]() | |
![]() |
(4)对=A-B-C-F进行扩展,求得
偏离点 | 在![]() ![]() | ![]() | 除![]() ![]() ![]() | 偏离路径 | 生成的候选路径 | 操作 |
A | A-B=∞ | NULL | A-D=∞ | 不存在 | 不存在 | |
B | B-C=∞ | A-B | NULL | B-E-F 13 | A-B-E-F 21 | 加入ListB |
C | C-F=∞ | A-B-C | NULL | C-E-F 12 | A-B-C-E-F 22 | 加入ListB |
将ListB中最短的路径:A-D-B-C-F 加入ListA,此时
此时ListA、ListB的状态如下:
ListA | ListB |
![]() | A-B-E-F 21 |
![]() | A-B-C-E-F 22 |
![]() | |
![]() | |
![]() |
已求得,算法结束。
4算法实现
用C++编写,编译器g++,代码无出错处理流程,仅供参考
#include <iostream>
#include <cstdio>
#include <cstdlib>
#include <bits/stdc++.h>
#include <algorithm>
#include <typeinfo>
using namespace std;
#define MAXNODE 64
#define MAXEDGE 100
#define INFINITE 1e8
class EdgeNode{ //图用邻接表来存储,这是边表节点
public:
EdgeNode(char to='Z',int weight=1):to(to),weight(weight),weight2(weight){next=NULL;}
EdgeNode(const EdgeNode &en){
this->to=en.to;
this->weight=en.weight;
this->weight2=en.weight;
this->next=en.next;
}
EdgeNode & operator=(const EdgeNode &en)
{
this->to=en.to;
this->weight=en.weight;
this->weight2=en.weight;
this->next=en.next;
}
~EdgeNode(){}
char to;
int weight; //原值,不改变
int weight2; //用于修改数值(即设置无穷大)并重置
EdgeNode *next;
};
class VertexNode{ //顶点表节点
public:
VertexNode(char name='Z',EdgeNode *firstEdge=NULL):name(name),firstEdge(firstEdge) {}
char name;
EdgeNode *firstEdge;
~VertexNode(){}
};
VertexNode vn[MAXNODE];//全局数据结构,
int createGraph(int n,int m,int flag){ //构造图
char from,to;
int weight;
for(int i=0;i<m;i++)
{
cin>>from>>to>>weight;
if(flag==0){
if(vn[from-65].name=='Z') vn[from-65].name=from;
if(vn[to-65].name=='Z') vn[to-65].name=to;
EdgeNode *edge_new=new EdgeNode(to,weight);
EdgeNode *p=vn[from-65].firstEdge;
if(vn[from-65].firstEdge==NULL){
vn[from-65].firstEdge=edge_new;
}
else
{
while(p->next!=NULL)
p=p->next;
p->next=edge_new;
}
EdgeNode *edge_new2=new EdgeNode(from,weight);
p=vn[to-65].firstEdge;
if(vn[to-65].firstEdge==NULL)
vn[to-65].firstEdge=edge_new2;
else
{
while(p->next!=NULL)
p=p->next;
p->next=edge_new2;
}
}
else {
if(vn[from-65].name=='Z') vn[from-65].name=from;
if(vn[to-65].name=='Z') vn[to-65].name=to;//这个点没有出边,这个点要保存在数组里
EdgeNode *edge_new=new EdgeNode(to,weight);
EdgeNode *p=vn[from-65].firstEdge;
if(vn[from-65].firstEdge==NULL)
vn[from-65].firstEdge=edge_new;
else
{
while(p->next!=NULL)
p=p->next;
p->next=edge_new;
}
}
}
return 1;
}
pair<int,vector<char>> dijkstra(char from,char to,int *t) //求最短路径
{
long max=1e8; //无穷大的值
int distance=0;
char parent[MAXNODE];//更新时记录每个节点的父节点,便于找到最短路径,只有这个距离更新,此节点的父节点才会更新
for(int i=0;i<MAXNODE;i++)
parent[i]='Z';//初始化所有节点父节点为'Z'
vector<char>vec;//存储的是T中的每个节点,即最短路径,
vector<pair<int,char>>queue;//代表的是T,即未扩展边的集合,用vector 实现
for(int i=0;i<MAXNODE;i++)
{
if(vn[i].name!='Z'){
if(vn[i].name==from&&t[vn[i].name-65]==0)//排除点不再入队
queue.push_back(make_pair(0,vn[i].name));//起点的距离值为0
else if(t[vn[i].name-65]==0)
queue.push_back(make_pair(max,vn[i].name));//初始化其他点的距离为max
}
}
vector<pair<int,char>>::iterator it=queue.begin();
while(!queue.empty())
{
sort(queue.begin(),queue.end());//按距离值从小到大排序
vector<pair<int,char>>::iterator tmp=queue.begin();
if(queue.begin()->second==to) {
distance=queue.begin()->first;//当弹出节点是终点时,便是终点的最短距离
if(distance >=1e8)//终点的路径值大于等于无穷大,代表此路径不存在
{
distance =-1; //显示找不到路径,设置距离值为负值
break;
}
}
EdgeNode *p=vn[tmp->second-65].firstEdge;
int d=tmp->first;//存储此节点出队的距离值
while(p!=NULL)
{
for(it=queue.begin()+1;it!=queue.end();it++)
{
if(p->to==it->second)//在T中找到要更新的点
{
if(d+p->weight2<it->first)
{ //需要更新,相等的话则不再更新
it->first=d+p->weight2;
parent[it->second-65]=tmp->second; //更新父节点
}
break;
}
}
p=p->next;
}
queue.erase(queue.begin());//删除首节点
}
char c=to;
int i=1;
while(c!=from&&c!='Z')
{
if(i>MAXNODE)//大于最大节点数,退出循环
break;
vec.push_back(c);
c=parent[c-65];
i++;
}
if(c==from)
vec.push_back(c);//vector 是按照逆序存储的每个节点
return make_pair(distance,vec);
}
void reset()
{//修改后,将图复位
for(int i=0;i<MAXNODE;i++){
EdgeNode *p=vn[i].firstEdge;
while(p!=NULL)
{
p->weight2=p->weight;
p=p->next;
}
}
}
inline bool setinfinite(char start,char end) //设置无穷大
{
int result=0;
EdgeNode *p=vn[start-65].firstEdge;//在vn上修改
while(p!=NULL)
{
if(p->to==end)
{
p->weight2=INFINITE;//操作的是weight2
result=1;
}
p=p->next;
}
p=vn[end-65].firstEdge;//无向图,也适用于有向图
while(p!=NULL)
{
if(p->to==start)
{
p->weight2=INFINITE;
result=1;
}
p=p->next;
}
return result;
}
bool search(char node,vector<pair<int,vector<char>>>ListA) //在ListA中将节点node开始的所有边设为infinite
{
for(vector<pair<int,vector<char>>>::iterator it=ListA.begin();it!=ListA.end();it++)
{
for(vector<char>::iterator it2=it->second.end()-1;it2>it->second.begin();it2--) //按照顺序逐个设置无穷大
{
if(*it2==node)
{
vector<char>::iterator it3=it2-1;
if(!setinfinite(*it2,*it3))
return 0;
}
}
}
return 1;
}
bool exist(vector<pair<int,vector<char>>>List,vector<char>vec)//新序列是否在ListB中出现过
{
for(vector<pair<int,vector<char>>>::iterator it=List.begin();it!=List.end();it++)
{
if(it->second==vec)
return 1;
}
return 0;
}
void print(vector<pair<int,vector<char>>>List) //打印ListA、ListB的状态
{
if(!List.empty())
for(vector<pair<int,vector<char>>>::iterator it=List.begin();it!=List.end();it++)
{
for(vector<char>::iterator it2=it->second.end()-1;it2>=it->second.begin();it2--)
cout<<*it2<<" ";
cout<<it->first<<endl;
}
}
int main(void)
{
vector<pair<int,vector<char>>>ListA,ListB;//第一个代表此路径值,第二个代表路径节点序列
int excluded[MAXNODE]; //进行dijkstra时,排除的R_{i}^{k}的节点
memset(excluded,0,MAXNODE*sizeof(int));
int n,m;
int flag=0; //0代表无向图,1代表有向图
cin>>n>>m>>flag;
createGraph(n,m,flag);
char from,to;
int k;
cin>>from>>to>>k;
pair<int,vector<char>>A1;
A1=dijkstra(from,to,excluded);
ListA.push_back(A1);//将A1加入ListA
for(int i=1;i<=k;i++)
{
reset();//将图复位
vector<pair<int,vector<char>>>::iterator end=ListA.end()-1;//最底元素是需要进行扩展的路径
vector<char>temp=(ListA.end()-1)->second;
memset(excluded,0,MAXNODE*sizeof(int));
vector<char>result;
int total=0;//路径前半部分的值
for(vector<char>::iterator iter=(end->second.end())-1;iter!=end->second.begin();iter--) //对偏离节点进行扩展
{//不包括vector路径的起始节点,即不包括终点
search(*iter,ListA);//对偏离点进行扩展
if(iter<(end->second.end()-1)) //不是第一个偏离点
{
vector<char>::iterator iter2=iter+1;
int weight=0;
EdgeNode *p=vn[*iter2-65].firstEdge;//使用原值数组vn
while(p!=NULL)
{
if(p->to==*iter){
weight=p->weight;
break;
}
p=p->next;
}
total=total+weight;//路径前半部分的值,随着偏离点的前进。路径值也会逐渐增大
vector<char>::iterator iter3=iter+1;
if(iter3<end->second.end())
{
excluded[*iter3-65]=1; //进入dijkstra,加入排除节点
result.push_back(*iter3); //排除节点即路径前半部分的节点
}
}
pair<int,vector<char>>tmp;
tmp=dijkstra(*iter,to,excluded);
if(tmp.first>0){ //求得的路径是有效值
tmp.first=total+tmp.first;//前半部分距离+后半部分距离=总距离
if(!result.empty()){//将路径前半部分加入,形成一个完整的路径
for(vector<char>::iterator it=result.end()-1;it>=result.begin();it--){
tmp.second.push_back(*it);
}
}
if(!exist(ListB,tmp.second)) //如果ListB不存在,才加入ListB
{
ListB.push_back(tmp);
}
}
sort(ListB.begin(),ListB.end());//对ListB排序
} //偏离点的循环
if(!ListB.empty())
{ //将ListB中的首元素移入ListA
vector<pair<int,vector<char>>>::iterator begin=ListB.begin();
ListA.push_back(*begin);
ListB.erase(begin);
}
else
{
if(i!=1) //队列B为空,且不是循环刚开始的时候,代表可能的K短路已全部找到,余下已无法再找。退出循环
cout<<"K max :"<<i<<endl;
break;
}
} // i的for循环
print(ListA);//打印结果
exit(0);
} //main 主函数
输入上述示例图:
6 9 0 //顶点数、边数和是否有向图
A B 8
B D 6
A D 2
D E 1
B E 4
B C 2
C E 3
C F 6
E F 9
A F 4 //起点、终点和K值
输出结果如下:
A D E F 12
A D E C F 12
A D E B C F 15
A B C F 16
5 算法时间、空间复杂度分析
在第K个循环中,设偏离点的个数为
(1)求最短路径使用Yen自己的算法,则的情况下,需要
次加法和
次比较,其中q为偏离点的个数,如果如果允许
,则需要
次加法和
次比较,其中N为图中顶点属
(2)需要的空间复杂度大约为
参考资料:
(1)Finding the K Shortest Loopless Paths in a Network Jin.Y.Yen. https://www.docin.com/p-1819184782.html
(2)Yen的K条最短路径算法(KSP).https://www.jianshu.com/p/ea0e6894259b