前言
图搜索算法:图是由节点和边组成,分为有向图、无向图、权重图等,图搜索的基本流程是:维护一个容器,从容器中访问一个节点,扩展该节点的邻居节点,将扩展的新节点加入容器,结束循环的条件是容器为空,若图有回环,则新建一个容器,加入已经访问过的节点,之后不再访问该容器中的节点。
一、图遍历算法:BFS和DFS
假定每条边的权重一样
- BFS广度优先搜索:使用的队列,先进先出
- DFS深度优先搜索:使用的栈,后进先出
1.DFS
弹出树状结构中层级最深的节点
先找到初始节点加入堆栈,然后将初始节点弹出,查找周围相邻节点,将相邻节点按照一定的规则放入堆栈,弹出栈顶元素依次查找,直到找到目标节点或者容器为空。
以有向图为例进行分析:S是初始点,G位目标点,从S->G利用DFS找到一条路径。
首先初始化栈容器,S入栈,弹出S,访问S的子节点有d,e,p,按照p、e、d的顺序依次入栈,之后弹出层级最高的节点d,此时容器中的元素为e、p;扩展d的子节点b、c、e,按照e、c、b的顺序入栈,此时容器中元素为b c e p,弹出b,扩展b的子节点a,a入栈,弹出a,a没有子节点,弹出c,扩展c的子节点a,a入栈,弹出a,没有子节点,此时容器中剩下e p,弹出e,扩展e的子节点h r,按照r h 依次入栈,弹出h,扩展h的子节点p q,按照q,p依次入栈,此时容器内元素 q r p,弹出q,q没有子节点,弹出q,此时容器内r p,弹出r,扩展r的子节点f,入栈,栈内元素f p,弹出f,扩展f的子节点c G,此时栈内元素c G p,弹出c,扩展c,找到G。整个路径就是:S-d-e-r-f-G。在二维地图中,只走直线,不一定是最佳的路径,上图中可以看到S-e-r-f-G为最佳路径,而DFS寻路的结果是S-d-e-r-f-G。
实际应用中需要确定一个方向只能走一次,按照哪种探索方式进行探索,同时要将已经走过的路径进行标记,不能再走了。
准备工作:要有一张地图【1(不能走)、0(可以走)】,定义探索方向枚举类型(上、右、下、左 or 上 左 下 右 )、同时还需要辅助地图、回退的点结构。
//定义地图大小
#define COLS 8
#define ROWS 8
//探索方向枚举类型
enum direct
{
p_up,p_right,p_down,p_left
};
//辅助地图
struct pathNode
{
int val; //地图中每个格子的值 0 ,1
direct dir; //试探方向
bool isFind; //是否走过
}
//回退点结构
struct MyPoint
{
int row;
int col;
}
int main()
{
//构建地图
int map[ROWS][COLS]={
{1,1,1,1,1,1,1,1},
{1,0,1,0,0,0,0,1},
{1,0,1,0,1,0,1,1},
{1,0,1,0,1,0,1,1},
{1,0,1,0,1,0,0,1},
{1,0,1,0,1,0,1,1},
{1,0,0,0,1,0,0,1},
{1,1,1,1,1,1,1,1}
};
//辅助地图
pathNode pathMap[ROWS][COLS] = { 0};
for(int i=0;i<ROWS;i++)
{
for(int j=0;j<COLS;j++)
{
pathMap[i][j].val = map[i][j];
}
}
//定义起点、终点
MyPoint beginPos;
MyPoint endPos;
cout << "请输入起点(x,y):";
cin >> beginPos.col;
cin >> beginPos.row;
cout << "请输入终点(x,y):";
cin >> endPos.col;
cin >> endPos.row;
//准备一个栈
stack<MyPoint> st;
//标记起点走过
pathMap[beginPos.row][beginPos.col].isFind = true;
//起点入栈
st.push(beginPos);
//开始寻路:准备变量存储当前位置,准备试探点
MyPoint currentPos = beginPos;
//试探点
MyPoint searchPos;
//是否找到终点
bool isFindEnd = false;
while(1){
searchPos = currentPos; //试探点从当前点开始探索
switch(pathMap[currentPos.row][currentPos.col].dir)
{
//上
case p_up:
searchPos.row--; //试探点更改
//因为试探点能不能走都需要改变试探方向,所以就先改变试探方向
pathMap[currentPos.row][currentPos.col].dir = p_right;
//试探点开始试探,能不能走:能走(没有走过且没有障碍物)
if(pathMap[searchPos.row][searchPos.col].isFind !=true && pathMap[searchPos.row][searchPos.col].val !=1)
{
//能走,更新当前点
currentPos = searchPos;
//已经走了,标记当前点已经走过
pathMap[currentPos.row][currentPos.col].isFind = true;
//当前点入栈
st.push(currentPos);
}
else{
//不能走,改变当前试探方向,前面已经改变啦
}
break;
//同理,右下类似
case p_right: ... break;
case p_down: ... break;
case p_left:
searchPos.col--;
//已经全部试探完毕。不需要改试探方向
if (pathMap[searchPos.row][searchPos.col].isFind != true &&pathMap[searchPos.row][searchPos.col].val != 1)
{
//能走
currentPos = searchPos;
//因为已经走了,标记当前点已经走过
pathMap[currentPos.row][currentPos.col].isFind = true;
//当前点入栈
st.push(currentPos);
}
else{
//不能走 --- 回退
//删除栈顶元素
st.pop();
//将栈顶元素作为当前点
currentPos = st.top();
}
break;
}
if(currentPos.row == endPos.row && currentPos.col == endPos.col)
{
isFindEnd = true;
break; //找到终点,跳出循环
}
if(st.empty())
{
break; //栈为空,跳出循环
}
}
//打印路径
if(isFindEnd)
{
while(!st.empty())
{
cout<<st.top().row <<","<<st.top().col<<endl;
st.pop();
}
}
return 0;
}
2.BFS
弹出树状结构中层级最浅的节点
先找到初始节点加入队列,然后将初始节点弹出,查找周围相邻节点,将相邻节点按照一定的规则放入队列,队头元素出队列,直到找到目标节点或者容器为空。
S是初始点,G位目标点,从S->G利用DFS找到一条路径。S进入队列,S出队列,S的子节点d e p, 依次进入队列,此时队列中元素p e d,弹出p,p的子节点q,进入队列(e d q),弹出e,e的子节点进入队列(d q r h),弹出d,d的子节点进入队列(q r h c b)弹出q,q没有子节点,弹出r, r的子节点进入队列(h c b f)弹出h,h的子节点走过,弹出c,子节点进入队列(b f a),弹出b ,b的子节点已在队列中,弹出f,子节点进入队列(a c G),弹出a ,子节点为空,弹出c,c的子节点a走过,弹出G找到终点。
二、启发式搜索
贪心策略,启发式搜索是一个猜测,启发式函数应该朝正确的扩展方向,且在搜索中是容易计算的。
1.Dijkstra
使用优先级队列存储元素,弹出代价g最小的节点。
初始化,加入起始节点Xs,代价为g(Xs) = 0,其它节点初始g(n) 为无穷;
若容器为空,返回false,搜索失败
从优先级队列open list中弹出g值最小的节点n,标记为已经扩展过,放入另外一个容器close list,判断n是否是目标节点(是—返回true,跳出循环),n不是目标节点,扩展n周围的没有扩展的节点m,若g(m)为无穷大,表示之前没有发现该节点,更新g(m) = g(n)+cost(n->m)当前n节点的代价加上n到m节点的代价,同时将m节点加入优先级队列;若g(m)有值不是无穷,g(m) >g(n)+cost(n->m),则更新g(m) = g(n)+cost(n->m)表示m节点在n节点前已经被其他节点发现,m节点已经在优先级队列中,但此时m的g值是其他节点到m的值,同时比n节点到m节点的g值大,就保留最小的g(m);循环下去,直到找到终点。
初始化优先级队列,S进入队列,g(S) =0;
S的节点d e p进入容器,S到相邻结节点的代价
g(d) = g(S)+ cost(S->d)=3,
g(e) = g(S)+ cost(S->e)=9,
g(p,)= g(S)+ cost(S->p)=1 ,
优先级队列按照g值进行排序(p d e),弹出容器中g值最小的元素p,扩展p,p的节点q,
g(q) = g(p,)+cost(p->q) = 16,
容器中元素(d e q)弹出d,扩展d,d的节点b c e,
g(b) = g(d)+cost(d->b) = 4,
g(c,) = g(d)+cost(d->c)=11,
g(e)=g(d)+cost(d->e)=5
更新g(e)此时容器中元素(b e c q),弹出b扩展b的节点a,
g(a)=g(b)+cost(b->a)=6,
容器元素更新(e a c q),弹出e扩展e的节点h r,
g(h)=g(e)+cost(e->h)=13,
g(r,)=g(e)+cost(e->r)=6,
容器元素更新(a r c h q)弹出a,扩展a没有子节点,弹出r,扩展r,r的子节点f,
g(f)=g(r,)+cost(r->f)=7,
容器元素(f c h q),弹出f扩展f节点c G,
g(c,) =g(f)+cost(f->c)=11,
g(G)=g(f)+cost(g->G) = 9
容器中元素(G c h q),找到终点。
2.A*
和Dijkstra算法类似,除了维护代价g以外,还维护了一个h值,按照f=g+h值进行优先级队列排序,h取0则会退化Dijkstra算法。
优先级队列初始化,S进入容器,
f(S)=g(S)+h(S)=6,
弹出S,S的子节点a 进入容器,
f(a)=g(a)+h(a)=g(S)+cost(S->a)+h(a)=6,
队列中元素a,弹出a,扩展a的子节点 b d e
f(b)=g(b)+h(b)=g(a)+cost(a->b)+h(b)=1+1+6=8;
f(d)=g(d)+h(d)=6;
f(e)=7,
按照f值较小的入队,容器中元素(d e b),弹出d,扩展d的节点G,f(G)=6,进入容器,元素更新(G e b),弹出G找到终点。
但是A*的结果不是最优的,要想保持最优性:估计的代价h要小于等于最终达到目标的真实代价h
权重A*:f= g+ah;a是权重。
a=0—退化为Dikjkstra; a=1—A*;a=无穷—忽略g,贪心
-
对角线启发式函数:
dx = abs(node.x-goal.x)
dy = abs(node.y-goal.y)
h = (dx+dy)+(sqrt(2)-2 )*min(dx,dy) -
打破对称性:
h = h*(1+p),p<一步最小的cost/期望最长路径的cost
对相同的f值的节点打破对称性 ,有一个倾向性(原本对f排序,f相同,对h排序,谁的h小,谁放在前面);
加上倾向性(沿着起点到终点直线的路径代价),计算任何一个节点,额外计算一个代价cross
dx1 = abs(node.x-goal.x)
dy1 = abs(node.y-goal.y)
dx2 = abs(start.x-goal.x)
dy2 = abs(start.y-goal.y)
cross = abs(dx1dy2- dx2dy1)
h=h+cross*0.001
总结
本文介绍了图搜索中深度优先搜索和广度优先搜索的流程,也介绍了Dijkstra和A*算法的过程,存在许多的不足,欢迎指正。