转载请标明出处:Best-First Search – TUNGFAIFONG.COM
1.介绍
最佳优先搜索(Best First Search),是一种启发式搜索算法(Heuristic Algorithm),我们也可以将它看做广度优先搜索算法的一种改进;最佳优先搜索算法在广度优先搜索的基础上,用启发估价函数对将要被遍历到的点进行估价,然后选择代价小的进行遍历,直到找到目标节点或者遍历完所有点,算法结束。
2.图解
Figure-1
Figure-2
假设我们现在有一棵树如Figure-1所示,A是根节点,L节点是我们的目标节点,假设我们已有启发估价函数,最佳优先搜索过程将会如Figure-2所示(右边数字为估价值),节点的遍历顺序会是A-5->B-4->C-4->H-3->G-4->L-3(这里的估价值是由我们假定的估价函数算出,实际情况估价值不一定为图示),找到L节点,算法结束。
3.实现
要实现最佳优先搜索我们必须使用一个优先队列(priority queue)来实现,通常采用一个open优先队列和一个closed集,open优先队列用来储存还没有遍历将要遍历的节点,而closed集用来储存已经被遍历过的节点。
最佳优先搜索的过程可以被描述为:
- 将根节点放入优先队列open中。
- 从优先队列中取出优先级最高的节点X。
- 根据节点X生成子节点Y:
3.1. X的子节点Y不在open队列或者closed中,由估价函数计算出估价值,放入open队列中。
3.2. X的子节点Y在open队列中,且估价值优于open队列中的子节点Y,将open队列中的子节点Y的估价值替换成新的估价值并按优先值排序。
3.3. X的子节点Y在closed集中,且估价值优于closed集中的子节点Y,将closed集中的子节点Y移除,并将子节点Y加入open优先队列。 - 将节点X放入closed集中。
- 重复过程2,3,4直到目标节点找到,或者open为空,程序结束。
Figure-2中的过程为:
- open = [A-5]; closed = [];
- open = [B-4, C-4, D-6]; closed = [A-5];
- open = [C-4, E-5, F-5, D-6]; closed = [A-5, B-4];
- open = [H-3, G-4, E-5, F-5, D-6]; closed = [A-5, B-4, C-4];
- open = [G-4, E-5, F-5, D-6]; closed = [A-5, B-4, C-4, H-3];
- open = [L-3, E-5, F-5, D-6]; closed = [A-5, B-4, C-4, H-3, G-4];
- 找到目标节点L,程序结束。
4.启发估价函数
在介绍中说到了,最佳优先搜索是一种启发式搜索算法。而什么是启发式搜索算法呢?
当我们在状态空间中搜索的时候,最简单的方法就是穷举,在之前文章提及到的广度优先搜索和深度优先搜索都属于穷举类型的搜索,这种搜索方法有一个很大的缺点,就是在状态空间十分大的时候效率非常的差,因为需要穷举的状态太多了。而启发式搜索就是对状态空间中的每个搜索的位置(如图中的节点)进行一个评估,然后选出最好的位置。而在启发估价中使用到的函数我们称之为启发估价函数。
启发估价函数:f(n) = g(n) + h(n),其中
- n为现在所在的节点,g(n)为从起始点到点n的实际代价,h(n)为从点n到目标点的估价。
我们要怎么去实现估价函数呢?
在本文章的第一个例子当中仅给出了估价值而没有给出估价方法只是为了更简单的描述出算法的过程,下面我们会举一些例子来说明怎么去实现估价函数。
假设我们有一串字符[CBAD],我们要将它转变成[ABCD],而每次只能相邻的字母做一次交换。
我们令估价函数中g(n)实际遍历的代价,h(n)为字母不在自己目标位置上的数目。估价将如Figure-3所示(图中每个节点左侧[****]表示状态,右侧数字表示为f(n) = g(n) + h(n)):
Figure-3
此时最佳优先搜索的遍历顺序为:[CBAD]->[CBDA]->[CABD]->[BCAD]->[ACBD]->[ABCD]。
当然你也可以使用别的估价函数,具体问题得具体分析,这里只是给出一种估价方法。
5.补充
-
A算法:最佳优先搜索算法我们也称为A算法(algorithm A)。
-
可采纳性(Admissibility):一个搜索算法如果能找到最短路径(也就是最优解),我们称这个算法为可采纳的。
-
A星算法:一个可采纳的A算法,我们称为A星算法(A* algorithm)。
-
单调性:如果一个启发估价函数满足以下条件,我们称这个估价函数是单调的:
1.对于任意的节点ni,nj,nj为ni的子节点:
h(ni) – h(nj) <= cost(ni,nj),
cost(ni,nj)为ni节点到nj节点的实际耗费。2.目标节点的估价为0,h(goal) = 0。
-
贪婪最佳优先搜索(Greedy Best-First Search):在估价函数当中,当h(n)比g(n)大很多,此时仅有h(n)对估价起作用是,我们称这种算法为贪婪最佳优先搜索。
6.C++实现代码(上面提及的[CBAD]转换成[ABCD]的问题的代码实现)
#include<iostream>
using namespace std;
#define N 4
//题目中所需要用到的节点
class Node {
public:
Node(char *data, int g = 0, int h = 0): data(data), g(g), h(h) {}
~Node() { delete[] data; }
int getF() const { return g + h; }
int getG() const { return g; }
int getH() const { return h; }
char* getData() const { return data; }
void setG(int g) { this->g = g; }
void setH(int h) { this->h = h; }
void setData(char *data) { this->data = data; }
bool operator==(const Node& node) {
for (int i = 0; i < N; ++i) {
if (this->data[i] != node.getData()[i]) {
return false;
}
}
return true;
}
bool operator!=(const Node& node) {
for (int i = 0; i < N; ++i) {
if (this->data[i] != node.getData()[i]) {
return true;
}
}
return false;
}
private:
int g;
int h;
char* data;
};
//h(n)
int heuristic(Node* current, Node* goal) {
int h = 0;
for (int i = 0; i < N; ++i) {
if (current->getData()[i] != goal->getData()[i]) {
h++;
}
}
return h;
}
//链表节点
struct ListNode {
ListNode* next;
Node* data;
};
void freeListNode(ListNode* node) {
delete node->data;
node->data = nullptr;
delete node;
node = nullptr;
}
//用链表实现优先队列
class List {
public:
~List() {
ListNode* p = head;
while (p != nullptr) {
p = head->next;
freeListNode(head);
head = p;
}
}
ListNode* getHead() { return head; }
void insert(ListNode* node) {
if (head == nullptr) {
head = node;
node->next = nullptr;
} else {
if (node->data->getF() < head->data->getF()) {
node->next = head;
head = node;
} else {
ListNode* p = head;
ListNode* q = p->next;
while (q != nullptr && node->data->getF() >= q->data->getF()) {
p = q;
q = q->next;
}
p->next = node;
node->next = q;
}
}
}
void remove(ListNode* node) {
if (head->data == node->data) {
head = head->next;
} else {
ListNode* p = head;
ListNode* q = head->next;
while (q != nullptr && q->data != node->data) {
p = q;
q = q->next;
}
if (q != nullptr && q->next != nullptr){
p->next = q->next;
q->next = nullptr;
freeListNode(q);
} else if (q->next == nullptr) {
p->next;
freeListNode(q);
}
}
}
ListNode* findNode(ListNode* node) {
ListNode* p = head;
while (p != nullptr) {
if (*node->data == *p->data) {
return p;
}
p = p->next;
}
return nullptr;
}
void pop() {
ListNode* p = head;
head = head->next;
}
bool empty() {
if (head == nullptr) {
return true;
}
return false;
}
private:
ListNode* head = nullptr;
};
void swapChar(char& a, char& b) {
char temp = a;
a = b;
b = temp;
}
int main() {
//初始化
List open;
List closed;
Node* goal = new Node(new char[4]{'A', 'B', 'C', 'D'});
ListNode* start_list_node = new ListNode();
start_list_node->data = new Node(new char[4]{'C', 'B', 'A', 'D'}, 0);
start_list_node->data->setH(heuristic(start_list_node->data, goal));
open.insert(start_list_node);
while (!open.empty()) {
ListNode* top = open.getHead();
open.pop();
//输出遍历的节点
cout << "[";
for (int i = 0; i < N; ++i) {
cout << top->data->getData()[i];
}
cout << "] " << top->data->getG() << "+" << top->data->getH() << endl;
//找到目标结束
if (*top->data == *goal) {
break;
}
//生成子状态
for (int i = N - 1; i > 0; --i) {
char* temp = new char[N];
for (int j = 0; j < N; ++j) {
temp[j] = top->data->getData()[j];
}
swap(temp[i], temp[i - 1]);
ListNode* child = new ListNode();
child->data = new Node(temp, top->data->getG()+1);
child->data->setH(heuristic(child->data, goal));
if (open.findNode(child) == nullptr && closed.findNode(child) == nullptr) {
open.insert(child);
} else if (open.findNode(child) != nullptr) {
ListNode* old = open.findNode(child);
if (child->data->getF() < old->data->getF()) {
open.remove(old);
open.insert(child);
}
} else if (closed.findNode(child) != nullptr) {
ListNode* old = closed.findNode(child);
if (child->data->getF() < old->data->getF()) {
closed.remove(old);
open.insert(child);
}
}
}
closed.insert(top);
}
delete goal;
}
推荐阅读
- Best-First search – WIKIPEDIA
- 《Artificial intelligence:Structures and Strategies for Complex Problem Sloving》George F. Luger.