启发式搜索学习笔记
启发式搜索
(一)定义:又叫做信息搜索(Informed Search),该算法利用问题的启发信息(所求解问题相关的辅助信息)引导搜索过程,来减少搜索范围,降低问题复杂度。
就是在状态空间中先对每一条搜索分支进行评估,得到最好的分支,再从这个分支继续搜索从而到达目标,这样可以有效省略大量无谓的搜索路径,大大提高了搜索效率。
(二)估价函数——启发式搜索算法定义了一个估价函数f(x), f(x)=g(x)+h(x)。
与问题相关的启发式信息都被计算为一定的f(x)值,引入到搜索过程中。
f(x)=g(x)+h(x) 解释:
f(x)——节点x的估价函数
g(x)——在状态空间中从初始节点到节点x的实际代价
h(x)——从节点x到目标节点的最佳路径的估计代价
g(x)是已知的,所以在这里主要是 h(x) 体现了搜索的启发信息,h(x)专业的叫法是启发函数。换句话说,g(x)代表了搜索的广度的优先趋势。但是当 h(x) >> g(x)时,可以忽略g(x),从而提高效率。
一、贪婪最佳优先搜索——Greedy Best-First Search(GBFS)
1、基本思想:
将节点表按照距目标的距离进行排序,再以节点的估计距离为标准选择待扩展节点。
2、简单理解:
“目光短浅”,每一步算法会先看左右临近节点,选择最低开销。不考虑总距离消耗。
3、评价函数:
f ( i ) = h ( i ) f(i) = h(i) f(i)=h(i)
4、代码实现:
在GBFS的算法中,其主要过程如下:
1️⃣建立一个已经排序好的初始节点表A(从小到大);
2️⃣若A为空集,退出,给出失败信号;
3️⃣a取为A的首节点(优先队列首节点原则),并在A中删除节点a,讲其放入已访问节点列表;
4️⃣若a为目标节点,退出,给出成功信号;否则,将a的后续节点加到A中,记为A’,对A‘中的节点按距目标的估计距离排序,并返回2.
import networkx as nx
import matplotlib.pyplot as plt
import Queue as Q
def getPriorityQueue(list):
q = Q.PriorityQueue()
for node in list:
q.put(Ordered_Node(heuristics[node],node))
return q,len(list)
def greedyBFSUtil(G, v, visited, final_path, dest, goal):
if goal == 1:
return goal
visited[v] = True
final_path.append(v)
if v == dest:
goal = 1
else:
pq_list = []
pq,size = getPriorityQueue(G[v])
for i in range(size):
pq_list.append(pq.get().description)
for i in pq_list:
if goal != 1:
#print "current city:", i
if visited[i] == False :
goal = greedyBFSUtil(G, i, visited, final_path, dest, goal)
return goal
def greedyBFS(G, source, dest, heuristics, pos):
visited = {}
for node in G.nodes():
visited[node] = False
final_path = []
goal = greedyBFSUtil(G, source, visited, final_path, dest, 0)
prev = -1
for var in final_path:
if prev != -1:
curr = var
nx.draw_networkx_edges(G, pos, edgelist = [(prev,curr)], width = 2.5, alpha = 0.8, edge_color = 'black')
prev = curr
else:
prev = var
return
class Ordered_Node(object):
def __init__(self, priority, description):
self.priority = priority
self.description = description
return
def __cmp__(self, other):
return cmp(self.priority, other.priority)
def getHeuristics(G):
heuristics = {}
f = open('heuristics.txt')
for i in G.nodes():
node_heuristic_val = f.readline().split()
heuristics[node_heuristic_val[0]] = node_heuristic_val[1]
return heuristics
#takes input from the file and creates a weighted graph
def CreateGraph():
G = nx.Graph()
f = open('input.txt')
n = int(f.readline())
for i in range(n):
graph_edge_list = f.readline().split()
G.add_edge(graph_edge_list[0], graph_edge_list[1], length = graph_edge_list[2])
source, dest= f.read().splitlines()
return G, source, dest
def DrawPath(G, source, dest):
pos = nx.spring_layout(G)
val_map = {}
val_map[source] = 'green'
val_map[dest] = 'red'
values = [val_map.get(node, 'blue') for node in G.nodes()]
nx.draw(G, pos, with_labels = True, node_color = values, edge_color = 'b' ,width = 1, alpha = 0.7) #with_labels=true is to show the node number in the output graph
edge_labels = dict([((u, v,), d['length']) for u, v, d in G.edges(data = True)])
nx.draw_networkx_edge_labels(G, pos, edge_labels = edge_labels, label_pos = 0.5, font_size = 11) #prints weight on all the edges
return pos
#main function
if __name__ == "__main__":
G, source,dest = CreateGraph()
heuristics = getHeuristics(G)
pos = DrawPath(G, source, dest)
greedyBFS(G, source, dest, heuristics, pos)
plt.show()
5、弊端:
1、不一定最优(不考虑总距离)
2、容易陷入死循环
3、有可能一会沿着一条道,但是这条道到不了终点(不完备性)
6、应用场景:
在计算机图形学中用于寻找两个三角形之间的最短路径,在自然语言处理中用于寻找最优的句子分割方案
二、A*算法
1、基本思想:
从起点开始,检查所有可能的扩展点(它的相邻点),对每个点计算g+h得到f,在所有可能的扩展点中,选择f最小的那个点进行扩展,即计算该点的所有可能扩展点的f值,并将这些新的扩展点添加到扩展点列表(open list)。
2、简单理解:
A* 算法就是在 Dijkstra 算法的基础上进行优化和改造。回顾一下 Dijkstra 算法的实现思路,其实有点儿类似广度优先搜索(BFS)算法,它每次找到跟起点最近的顶点,然后往外扩展。
3、评价函数:
f ( i ) = g ( i ) + h ( i ) f(i)=g(i)+h(i) f(i)=g(i)+h(i)
4、代码实现:
当我们遍历到某个顶点的时候,从起点走到这个顶点的路径长度是确定的,我们记作 g(i)(i 表示顶点编号)。但是,从这个顶点到终点的路径长度是未知的。虽然确切的值无法提前知道,但是我们可以用其他估计值来代替。这里我们可以通过这个顶点跟终点之间的直线距离,也就是欧几里得距离,来近似地估计这个顶点跟终点之间的路径长度(注意:路径长度跟直线距离是两个概念)。我们把这个距离记作 h(i)(i 表示这个顶点的编号),h(i) 专业的叫法是启发函数(heuristic function)。
由于欧几里得距离的计算公式,会涉及比较耗时的开根号计算,所以,我们一般通过另外一个更加简单的距离计算公式,那就是曼哈顿距离(Manhattan distance)。曼哈顿距离是两点之间横纵坐标的距离之和。计算的过程只涉及加减法、符号位反转,所以比欧几里得距离更加高效。
int hManhattan(Vertex v1, Vertex v2) { // Vertex表示顶点
return Math.abs(v1.x - v2.x) + Math.abs(v1.y - v2.y);
}
原来在 Dijkstra 算法中,只是单纯地通过顶点 i 与起点之间的路径长度 g(i),来判断谁先出队列,现在有了这个顶点到终点的路径长度估计值,我们可以通过两者之和 f(i)=g(i)+h(i),来判断哪个顶点该最先出队列。综合两部分,我们就能有效避免刚刚讲的“跑偏”。这里 f(i) 的专业叫法是估价函数(evaluation function)。
从刚刚的描述,我们可以发现,A 算法实际上就只是对 Dijkstra 算法的简单改造*。实际上,代码实现方面,我们也只需要稍微改动几行代码,就能把 Dijkstra 算法的代码实现,改成 A* 算法的代码实现。将整个地图抽象成一个有向有权图的代码实现如下:
public class Graph { // 有向有权图的邻接表存储方法
private LinkedList<Edge> adj[]; // 邻接表
private int v; // 顶点个数
public Graph(int v) {
this.v = v;
this.adj = new LinkedList[v];
for (int i = 0; i < v; ++i) {
this.adj[i] = new LinkedList<>();
}
}
public void addEdge(int s, int t, int w) { // 添加一条边
this.adj[s].add(new Edge(s, t, w));
}
private class Edge {
public int sid; // 边的起始顶点编号
public int tid; // 边的终止顶点编号
public int w; // 权重
public Edge(int sid, int tid, int w) {
this.sid = sid;
this.tid = tid;
this.w = w;
}
}
// 下面这个类是为了A* 算法实现中,顶点的定义
private class Vertex { //顶点的定义
public int id; // 顶点编号ID
public int dist; // 从起始顶点到这个顶点的距离,也就是g(i)
public int f; // 估价函数 f(i)=g(i)+h(i)
public int x, y; // 顶点在地图中的坐标(x, y)
public Vertex(int id, int x, int y) {
this.id = id;
this.x = x;
this.y = y;
this.f = Integer.MAX_VALUE;
this.dist = Integer.MAX_VALUE;
}
}
// Graph类的成员变量,在构造函数中初始化
Vertex[] vertexes = new Vertex[this.v];
// 新增一个方法,添加顶点的横纵坐标
public void addVetex(int id, int x, int y) {
vertexes[id] = new Vertex(id, x, y)
}
}
在 A* 算法的代码实现中,图 Graph 类的定义跟 Dijkstra 算法中的定义一样。而顶点 Vertex 类的定义,跟 Dijkstra 算法中的定义,稍微有点儿区别,多了 x,y 坐标,以及刚刚提到的 f(i) 值。而 A* 算法代码实现的主要逻辑是下面这段代码。它跟 Dijkstra 算法的代码实现,主要有 3 点区别:
优先级队列构建的方式不同。A* 算法是根据 f(i) 值(f(i)=g(i)+h(i))来构建优先级队列,而 Dijkstra算法是根据 dist 值(也就是刚刚讲到的 g(i))来构建优先级队列;
A* 算法在更新顶点 dist 值的时候,会同步更新 f(i) 值;
循环结束的条件也不一样。Dijkstra 算法是在终点出队列的时候才结束,A* 算法是一旦遍历到终点就结束。
private class PriorityQueue { // 根据vertex的f值构建小顶堆,而不是按照dist
private Vertex[] nodes;
private int count;
public PriorityQueue(int v) {
this.nodes = new Vertex[v+1];
this.count = v;
}
public Vertex poll()
public void add(Vertex vertex)
// 更新结点的值,并且从下往上堆化,重新符合堆的定义。时间复杂度O(logn)。
public void update(Vertex vertex)
public boolean isEmpty()
}
public void astar(int s, int t) { // 从顶点s到顶点t的路径
int[] predecessor = new int[this.v]; // predecessor 数组记录每个顶点的前驱顶点,用来还原最短路径
PriorityQueue queue = new PriorityQueue(this.v); // 根据vertex的f值构建小顶堆
boolean[] inqueue = new boolean[this.v]; // 标记是否进入过队列
vertexes[s].dist = 0; // 把起始顶点 s 的 dist 值初始化为 0
vertexes[s].f = 0; // 把起始顶点 s 的 f 值初始化为 0
queue.add(vertexes[s]); // 将起始顶点 s 添加到优先级队列中
inqueue[s] = true;
while (!queue.isEmpty()) {
Vertex minVertex = queue.poll(); // 取堆顶元素并删除
for (int i = 0; i < adj[minVertex.id].size(); ++i) {
Edge e = adj[minVertex.id].get(i); // 取出一条与minVetex相连的边 e
Vertex nextVertex = vertexes[e.tid]; // 边 e指向的顶点为nextVertex,即取出的有向边为:minVertex-->nextVertex
if (minVertex.dist + e.w < nextVertex.dist) { // 若存在更短的路径,更新nextVertex的dist,f
nextVertex.dist = minVertex.dist + e.w; // 更新顶点 dist值
nextVertex.f = nextVertex.dist + hManhattan(nextVertex, vertexes[t]); // 同步更新 f值
predecessor[nextVertex.id] = minVertex.id;
if (inqueue[nextVertex.id] == true) { // 如果这个顶点已经在优先级队列中了,就不要再将它重复添加进去,直接更新队列中该顶点的dist值即可
queue.update(nextVertex);
} else {
queue.add(nextVertex);
inqueue[nextVertex.id] = true; // 每当有新的顶点进入队列后,要在 inqueue数组中标记
}
}
if (nextVertex.id == t) { // 一旦遍历到终点 t就可以结束while了
queue.clear(); // 只有清空queue,才能推出while循环
break; // 这里的break 只能退出for循环
}
}
}
// 输出路径
System.out.print(s);
print(s, t, predecessor);
}
private void print(int s, int t, int[] predecessor) { // 通过递归的方式,将路径打印出来
if (s == t) return;
print(s, predecessor[t], predecessor);
System.out.print("->" + t);
}
5、弊端:
1、不一定最短(快速估计)
2、一旦搜索到终点就不再继续考虑其他顶点和路线了。
6、应用场景:
地图 App 中的出行路线规划问题、游戏中人物角色的自动寻路功能。
🌟辅助理解的tips:
1、状态空间搜索——就是将一个问题的求解过程表现为从初始状态到目标状态寻找一条路径的过程。
通俗点说,就是要在两点之间求一条线路,这两点是问题的开始和问题的结果,而这一线路不一定是直线,可以是曲折的。由于求解问题的过程中分支有很多(即求解方式有很多种),到达目标状态的方式也就有很多,这主要是由于求解过程中求解条件的不确定性和不完备性造成的。求解的路径很多,这就构成了一个图,我们说这个图就是状态空间。问题的求解实际上就是在这个图中找到一条路径可以从问题的开始到问题的结果。这个寻找的过程就是状态空间搜索。
2、常用的状态空间搜索——广度优先搜索&深度优先搜索
他们都有一个很大的缺陷:都是在一个给定的状态空间中穷举;都是根据搜索的顺序依次进行搜索,称为盲目搜索。在空间状态不大的时候可以用,但是如果状态空间非常大并且不可预测的情况下就不可取了,它们的效率会非常低。这时需要用到启发式搜索。
3、欧几里德距离&曼哈顿距离
1、欧几里德距离(欧氏距离)——两点之间的直线距离。
d
=
(
x
1
−
x
2
)
2
−
(
y
1
−
y
2
)
2
d=\sqrt{(x_1-x_2)^2-(y_1-y_2)^2}
d=(x1−x2)2−(y1−y2)2
2、曼哈顿距离——两点在南北方向上的距离加上在东西方向上的距离。
d
(
i
,
j
)
=
∣
x
i
−
x
j
∣
+
∣
y
i
−
y
j
∣
d(i,j)=|xi-xj|+|yi-yj|
d(i,j)=∣xi−xj∣+∣yi−yj∣