浅谈K短路算法(KSP)之一(A*算法求解)

对于具有n个顶点和m条边且边的权值非负的简单图(无重边和环),K短路,是指的起点s到终点t的最短路径中第k个最小的。K短路分为有限制的K短路和无限制的K短路,有限制的K短路是指求得的路径中不含有回路(路径上任何一个节点的出现次数不大于1次),无限制的K短路则对求得的路径中没有要求,这篇博客先讨论前者,后讨论后者。

本篇博客中使用的无向图实例如下图所示

如上图所示,求解节点A至节点C的K短路。本篇博客主要使用经典的A*算法。

1 A*算法简介。

通用图搜索路径常用的启发式搜索

f(n)=g(n)+h(n)

其中f(x)称为评估函数,g(n)为从初始节点S到节点n的实际代价,h(n)是从节点n到目标节点T的最 优路径的评估代价,被称为启发式函数,它体现了问题的启发式信息。

由此引出A*算法:

定义评估函数:

f^{*}(n)=g^{*}(n)+h^{*}(n)

在上式中,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短路问题,可以概括为如下步骤。首先,定义评估函数

f(n)=g(n)+h(n),其中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 <iostream>
#include <cstdio>
#include <cstdlib>
#include <bits/stdc++.h>
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<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;//添加正图中的边表节点
        }

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<MAXNODE;i++) //无向图的复制
       vnr[i]=vn[i];

return 1;
}

bool dijkstra(char from,char to) //dijkstra 求解dis值
{
//V为顶点集,S为已求得最短距离的点的集合,T为余下点的集合,初始时,S={},T=V,T用带pair的vector实现,当T为空时,代表所有点均以到达集合S中,算法结束,当一个节点从集合T中迁移至S中时,这个顶点对应的距离值便是此节点到起点的最短距离值。

long max=1e8;
char parent[MAXNODE];//更新时记录每个节点的父节点,只有这个距离更新,此节点的父>节点才会更新
for(int i=0;i<MAXNODE;i++)
        parent[i]='Z';
vector<pair<int,char>>queue;//不是优先级队列,第一个代表距离,第二个代表节点
for(int i=0;i<MAXNODE;i++)
{
        if(vnr[i].name!='Z'){
                if(vnr[i].name==to)
                queue.push_back(make_pair(0,vnr[i].name));//起点的距离值为0
                else
                queue.push_back(make_pair(max,vnr[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(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->weight<it->first)
                                { //需要更新,相等的话则不再更新,相等代表距离一样,但经过的顶点更多
                                        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"<<endl;
		return 0;
	}
int count=0;//终点出现次数
int i=1,loc=1;    //步数
int fx[100],gx[100],hx[100];//A star中的函数值,即扩展的节点序列,暂定100
char node[100];             //存储节个序列中实际对应的每个节点
int parent[100];           //每个序列的父节点
for(int i=0;i<100;i++)
{
	fx[i]=0;
	gx[i]=0;
	hx[i]=0;
	node[i]='Z';
	parent[i]=0;
}
//添加父节点
priority_queue<pair<pair<int,int>,int>,vector<pair<pair<int,int>,int>>,greater<pair<pair<int,int>,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<pair<int,int>,int>tmp=q.top();
	q.pop();
	if(node[tmp.second]==to){//找到终点
	vector<char>vec;//用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<char>::iterator it=vec.end()-1;
	        	cout<<"k="<<k<<endl;
        		for(it;it>=vec.begin();it--)
                	cout<<*it<<"  ";//输出节点序列
        		cout<<fx[tmp.second]<<endl;//输出总的fx值
			break;//找到第K短路,退出
		}
		
	}
	else 
	{//将当前节点的子女节点入队
	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;
		
	}
	}

}
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<k)
        cout<<"K max:"<<count<<endl;


代表大于k max值后的路径均找不到。

 

 

 

参考资料:

(1)https://zhuanlan.zhihu.com/p/34665151

(2)https://www.cnblogs.com/shuaihui520/p/9623597.html

 

 

 

   

  • 3
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值