对于具有n个顶点和m条边且边的权值非负的简单图(无重边和环),K短路,是指的起点s到终点t的最短路径中第k个最小的。K短路分为有限制的K短路和无限制的K短路,有限制的K短路是指求得的路径中不含有回路(路径上任何一个节点的出现次数不大于1次),无限制的K短路则对求得的路径中没有要求,这篇博客先讨论前者,后讨论后者。
本篇博客中使用的无向图实例如下图所示
如上图所示,求解节点A至节点C的K短路。本篇博客主要使用经典的A*算法。
1 A*算法简介。
通用图搜索路径常用的启发式搜索
其中f(x)称为评估函数,g(n)为从初始节点S到节点n的实际代价,h(n)是从节点n到目标节点T的最 优路径的评估代价,被称为启发式函数,它体现了问题的启发式信息。
由此引出A*算法:
定义评估函数:
在上式中,g*(n)是从起始节点S到节点n的最短路径的代价,h*(n)是从节点n到目标节点T的最短路径的代价。shi因为有可能最短路径还没找到,所以一般情况下有g(n)>=g*(n)
同时h(n)<=h*(n),就是说h(n)是h*(n)的下界,这个限制确保A*算法可以找到最优解。对于一个问题,启发式函数h(n)的设计通常有多种,一般有如下关系
(1)h(n)=0,A*算法退化为dijkstra算法,可以找到最优解
(2)h(n)<=h*(n),A*算法可以找到最优解,不过收敛速度较慢,h(n)越小,收敛速度越慢
(3)h(n)=h*(n),A*算法可以找到最优解,收敛速度很快,扩展的节点全是最短路径上的点
(4)h(n)>h*(n),A*算法不一定能找到最优解,但收敛速度很快。
2 求解过程
用A*算法求解K短路问题,可以概括为如下步骤。首先,定义评估函数
,其中g(n)是从起始节点S到达节点的n的实际代价,定义为从起始节点S到达节点n的路径上所经过边的权值之和,h(n)为从节点n到达目标节点的最短路径的代价,记为dis[],实现的方法是构建反图,以终点T为起点对全图进行一次dijkstra算法得到(在这里,h(n)=h*(n)),以上图为例,从节点C到各个节点的dis值如下:
dis[C]=0,dis[B]=2,dis[A]=3,dis[D]=4
定义如下结构:queue[]为优先级队列,count[]为节点出现的次数。这样f(n)代表从起点节点到目标节点的最短路径的费用。刚开始,queue[]只有起始节点,算法步骤如下
(1)从优先级队列选取队头元素,如果fx值相同则选取gx较小的
(2)用队头元素去更新其子女节点的f(n)值,并将子女节点入队
(3)当取出的队头元素是终点时,计算其出现次数,若是K次,算法结束,否则将其子女入队。
对上图,求解节点A到节点C的K短路,具体的求解过程如下:
(b)queue[]={A}
g(1)=0,h(1)=dis[A]=3;
f(1)=g(1)+h(1)=0+3=3;
(b)节点A出队,其子女节点B、C、D入队,
用f(1)更新其子女节点B、C、D的f值
g(2)=g(1)+w(A-B)=0+1=1(w(A-B)代表边A-B的权值)
h(2)=dis[B]=2;
f(2)=g(2)+h(2)=1+2=3;对应节点为B
g(3)=g(1)+w(A-C)=0+4=4;
h(3)=dis[C]=0;
f(3)=g(3)+h(3)=4+0=4;对应节点为C;
g(4)=g(1)+w(A-D)=0+1=1;
h(4)=dis[D]=4;
f(4)=g(4)+h(4)=1+4=5;对应节点为D
queue[]={2,3,4}
(c)节点2出队,B的子女节点A、C入队
g(5)=g(2)+w(B-A)=1+1=2;
h(5)=dis[A]=3;
f(5)=g(5)+h(5)=5; 对应节点为A
g(6)=g(2)+w(B-C)=1+2=3;
h(6)=dis[C]=0;
f(6)=g(6)+h(6)=3; 对应节点为C
queue[]={6,3,5,4},
(d)节点6出队,节点C的子女A、B、D 入队
节点6对应的节点是C,即终点C,也是C的第一次出队,即第一短路,count[C]++
g(7)=g(6)+w(C-A)=3+4=7;
h(7)=dis[A]=3;
f(7)=g(7)+h(7)=10;
g(8)=g(6) +w(C-B)=3+2=5;
h(8)=dis[B]=2;
f(8)=g(8)+h(8)=5+2=7,对应节点为B
g(9)=g(6)+w(C-D)=3+6=9;
h(9)=dis[D]=4;
f(9)=g(9)+h(9)=9+4=13;
queue[]={3,5,4,8,7,9}
(e)节点3出队,节点3(节点C)的子女A、B、D入队
节点3对应的图中节点即是节点C,是终点,这是终点的第2次出队,代表第2短路,count[C]++
以此类推,求出最短路
当节点较多时,优先级队列的元素较多,所以优先级队列的容量尽量大些.
3 实现
用C++编写,g++编译,实现无出错处理,仅供参考
#include
#include
#include
#include
using namespace std;
#define MAXNODE 64 //最大顶点数,顶点是字母,存储的是从大写字母A开始的64个字符
#define MAXEDGE 100 //最大边数
class EdgeNode{ //图用邻接表存储,这是邻接表中的边表节点,类中成员均为pulic,便于访问和修改
public:
EdgeNode(char to='Z',int weight=1):to(to),weight(weight){next=NULL;}
EdgeNode(const EdgeNode &en){
this->to=en.to;
this->weight=en.weight;
this->next=en.next;
}
EdgeNode & operator=(const EdgeNode &en)
{
this->to=en.to;
this->weight=en.weight;
this->next=en.next;
}
~EdgeNode(){}
char to;
int weight;
EdgeNode *next;
};
class VertexNode{ //邻接表中的顶点表节点,所有成员均为public
public:
VertexNode(char name='Z',EdgeNode *firstEdge=NULL):name(name),firstEdge(firstEdge) {}
char name;
EdgeNode *firstEdge;
~VertexNode(){}
};
VertexNode vn[MAXNODE],vnr[MAXNODE];//正图、反图,用正向和反向的邻接表表示
int dis[MAXNODE];//每个节点的dis值
int createGraph(int n,int m,int flag){ //构造图
char from,to;
int weight;
for(int i=0;i>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;//添加正图中的边表节点
}
if(vnr[to-65].name=='Z') vnr[to-65].name=to;
if(vnr[from-65].name=='Z') vnr[from-65].name=from; //这个点没有出边,这个点要保>存在数组里
EdgeNode *edge_new2=new EdgeNode(from,weight);
p=vnr[to-65].firstEdge;
if(vnr[to-65].firstEdge==NULL)
vnr[to-65].firstEdge=edge_new2;
else
{
while(p->next!=NULL)
p=p->next;
p->next=edge_new2;//添加反图中的边表节点
}
}
}
if(flag==0)
for(int i=0;i节点才会更新
for(int i=0;i>queue;//不是优先级队列,第一个代表距离,第二个代表节点
for(int i=0;i为max
}
}
vector>::iterator it=queue.begin();
while(!queue.empty())
{
sort(queue.begin(),queue.end());//对剩余节点进行排序
vector>::iterator tmp=queue.begin();
if(dis[queue.begin()->second-65]>queue.begin()->first)
dis[queue.begin()->second-65]=queue.begin()->first;//队首的距离值为最短的,而且要出队
EdgeNode *p=vnr[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->weightfirst)
{ //需要更新,相等的话则不再更新,相等代表距离一样,但经过的顶点更多
it->first=d+p->weight;
parent[it->second-65]=tmp->second; //更>新父节点
}
break;
}
}
p=p->next;
}
queue.erase(queue.begin());//删除起始节点
}
Edg
}
bool Astar(char from,char to,int k)//A star 求解K短路
{
if(vn[from-65].name=='Z'){
cout<<"node doesn't exist"<,int>,vector,int>>,greater,int>>>q; //优先级队列,第一个int存储的是fx值,第二个存储的是gx值,第三个存储的是扩展的节点序列,满足当fx值相等时,则选择fx较小的
gx[1]=0;hx[1]=dis[from-65];node[1]=from;//先将起点入队
fx[1]=gx[1]+hx[1];
parent[0]=-1; //便于沿着父节点数组一直找到起点
parent[1]=0;
q.push(make_pair(make_pair(fx[1],gx[1]),1));
while(!q.empty())
{
pair,int>tmp=q.top();
q.pop();
if(node[tmp.second]==to){//找到终点
vectorvec;//用vector存储此条路径
int now=tmp.second;
while(parent[now]!=-1)
{
vec.push_back(node[now]);//节点存入vector
now=parent[now];//找到父节点
}
EdgeNode *p=vn[node[tmp.second]-65].firstEdge;
while(p!=NULL) //将子女节点入队
{
++loc;
gx[loc]=gx[tmp.second]+p->weight;
node[loc]=p->to;
parent[loc]=tmp.second;
hx[loc]=dis[p->to-65];
fx[loc]=gx[loc]+hx[loc];
q.push(make_pair(make_pair(fx[loc],gx[loc]),loc));
p=p->next;
}
count++;
if(count==k){
vector::iterator it=vec.end()-1;
cout<<"k="<=vec.begin();it--)
cout<<*it<<" ";//输出节点序列
cout<weight;
node[loc]=p->to;
parent[loc]=tmp.second;
hx[loc]=dis[p->to-65];
fx[loc]=gx[loc]+hx[loc];
q.push(make_pair(make_pair(fx[loc],gx[loc]),loc));
p=p->next;
}
}
}
return 1;
}
int main(void)
{
memset(dis,10000,sizeof(dis));
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;//输入起点、终点和K
dijkstra(from,to);
Astar(from,to,k);
exit(0);
}
以上图为例,输入:
4 5 0 //边数、顶点数和是否有向
A B 1 //边
B C 2
A C 4
A D 1
D C 6
A C 3 //起点、终点和K值
输出如下:
k=3
A B A B C 5
从输出看出,第3条短路确实有环,从A-B-A,所以这个算法是解决的无限制的K短路,在实际应用中,有环的K短路一般情况下没有意义。导致出现环路的原因是某一个路径一个节点的出现次数大于1次,如果我们在当前节点入队之前,将当前节点实际对应的节点与所有的父节点相比较,如果父节点中已经出现过此节点,则表明当前节点对应节点在路径的出现次数大于1次,当前节点不再入队,则得到的路径便是没有环路。因此在每个结点入队前,判断此节点路径上的所有父节点是否与当前节点的对应节点相同,是,则不再入队。这样一来,优先级队列容量相对于前者要小。将上面代码q.push(make_pair(make_pair(fx[loc],gx[loc]),loc));的内容修改为:
int result=1; //先设定没有相同的
int now=tmp.second;
while(parent[now]!=-1)
{
if(node[now]==p->to) //父节点中找到当前节点相同的
{
result=0;
break;
}
now=parent[now];
}
if(result)
q.push(make_pair(make_pair(fx[loc],gx[loc]),loc));
因为在无环的K短路中,K值是有限制的,即一个图,无环短路的路径数量有个上线值,在Astar函数返回前添加如下代码:
if(count 参考资料: (1)https://zhuanlan.zhihu.com/p/34665151 (2)https://www.cnblogs.com/shuaihui520/p/9623597.html
代表大于k max值后的路径均找不到。