图中的算法多的要命,大多数都是搜索算法吧,因为其应用实在太广了。就比如说机器人眼中的世界其实就是一张graph,graph的大小的有限的(这跟分配给它的memory有关),机器人就根据现有的graph信息边计算边决策,然后边走边获取新graph信息,再重新计算再决策再走……这是一个迭代过程,直到到达目的地为止,汽车导航仪也是类似的。我觉得这过程有点像你用一个英雄去打敌方老窝,在地图还不是完全可见的情况下,你得亲自控制着他,看好路,根据情况选择对应的战斗策略。……哈哈扯远了,这个其实算Informed Search了,也就是带有决策的启发式搜索了(我们将在下一篇中介绍)。我们先来介绍简单的Uninformed Search。
什么叫Search?Search就是在所有Search space(状态空间——往往能用树或图来表示出)中找到你想要的那个状态。
Uninformed Search,顾名思义就是消息不灵通的search,被蒙蔽的search,也可以叫做Blind Search(盲目搜索)或者Brute-force Search(也就是传说中的暴力解法)。
我们先来看两个搜索的例子。
如图,我们从A处进入这个空间,我们想要到达J处。当然我们人眼是肯定一眼就看穿路线了,但让机器应该怎么做?
计算机势必得先把这张地图信息存起来,格式只能是计算机能实现的数据结构。
在这棵树(也可以叫图)上,我们把每一个节点视作一个搜索状态,每一条边视作一次搜索动作,只有能从初始节点到达目标节点就算搜索成功了。经典的算法莫过于深度优先搜索和广度优先搜索了(我们将待会儿介绍)。
我们再来看一个例子。
有一种两人游戏叫Nim Game,规则是:在一根柱子上放N个大小相同的环,两个人轮流从上面取环下来,规定每人每次最多只能取连续的m个环,不能不取,谁取到最后一个环就算输。
假定现在N=6, m=3,两人游戏过程如图。想想看先取者和后取者谁的胜率较高?进一步再想想看先取者和后取者有没有最有把握胜的策略。
我们将这个问题中所有的状态空间用一棵树来表示出。
所有的节点中的数字表示剩下的环的个数,剩下1则表示接下来取的那个人肯定输了,图中的边则表示取下的环的个数。
可见,总共有13种不同的游戏过程,先取者赢的概率为7/13,后取者赢的概率为6/13。更进一步,如果要制定最有把握胜的策略的话,这件事只要交给计算机去做好了。先把所有状态空间都存起来,每当对手取完后,就找到取完那个状态的节点,然后遍历那个节点的子树以计算出胜率最大的下一步操作。
这里已经略微涉及到人机博弈的概念了(这个我还没有研究过),我想基础的东西肯定是大同小异的,只是复杂度提高了。大名鼎鼎的“深蓝”挑战国际象棋冠军,它里面存有了200万张不同的棋局,配有200多个独立的计算器(应该也是ALU吧),不管对手怎么动,它都能在它的库存中找到对应的最佳解法。
下面我们开始介绍真正的Uninformed Search方法吧。
1. Depth-First Search(DFS)深度优先
经典算法啊,想必大家都懂的,这里我只想提一下它的非递归实现方法。
还是要使用一个Stack
(1)把根节点入栈
(2)Pop一下,访问该节点,然后把它的右子女和左子女(不为NULL)依次Push进去
(3)重复(2),直到找到目标节点或栈为空(没有找到)。
为了避免环的出现而使算法陷入死循环,可以引入一个visited数组。
显然,DFS算法虽然一定能找到出现在图中的节点,但效率很低。若要找F,DFS就相当于把整个图都遍历了一遍。
2. Depth-Limited Search(DLS)
DLS其实是DFS的变体,它限制住了访问的深度,在DFS算法的第(2)步中,若该节点当前深度大于限制深度,那就不再把它的子女Push进去了。
为了实现此算法,又多开辟了一个深度Stack,它与节点Stack同步进行操作,Push节点的时候也把当前的深度Push进去,Pop同理。
显然,DLS未必能找到目标节点。
3. Iterative Deepening Search(IDS)
IDS其实又是DLS的变体了,它通过depth的增量(初始值和增量大小可视情况而定),迭代进行DLS算法,直到找到目标节点。
显然当目标节点很深的情况下,IDS算法的效率肯定不如直接DFS的。但当深度很大,而且标节点处于中间位置或较浅位置时,或者深度未知的情况下,就能体现出IDS算法的价值了。
4. Breadth-First Search(BFS)广度优先
也是经典啊,通过队列实现,不多说了。只说下,BFS也可以像DFS一样引入一个visited数组,不是为了避免死循环,而是可以避免有环的情况下重复访问相同的节点,以加速搜索。
5. Uniform-Cost Search(UCS)
这个算法适用与带权图中,为了找出最短路径。
如图,找出从A到E的最短路径。
我们需要一个优先队列(也就是堆)来实现,里面存放节点和当前到达该节点总共的Cost,按Cost来把堆调整为最小堆。
(1)初始节点插入空堆
(2)出堆,访问该节点,把它所有的子女(不为NULL)依次入堆(Cost=当前节点的Cost + 与子女边的权)
(3)重复(2),直到出堆的节点即为目标节点,或堆为空了(没有找到)。
注意,因为是堆,所以每次入堆出堆操作堆内都会进行调整。
这里我们再来讨论一种backtracking技术,因为问题是通过此算法,我们只能找到最短路径的长度是多少,而不知道这条路径经过了哪些节点。其实最简单的方法就是只要让每个节点记住它的parent就可以了。
我们需要的数据结构如下
struct Node
{
int node_id;
int current_total_cost; // current_total_cost = 父节点current_total_cost + 边上的权
Node* parent;
}
每次取当前节点的子女时,将parent指针指向当前节点就OK了。只要将赋值好的Node插入堆中就行了。
当算法结束时,取出的最后一个节点为目标节点,然后通过它的parent指针以及parent的parent指针(迭代)一路可以找到初始节点(初始节点的parent当然为NULL),然后按正向输入节点id就OK了。
6. Bidirectional Search 双向搜索
顾名思义,就是从两端同时搜索。但它有个前提条件,必须知道目标节点在哪(有指针指向它)。
它的基本思想是两端都采用BFS搜索,直到两条路径碰头。但现实中,若都采用BFS搜索,往往是不会碰头的,即使碰头的话,效率也非常低。
现实中的图往往复杂到这样
所以一般两端都采用一种Informed Search叫A* Search(我们将在下篇中介绍)
Summary
Algorithm |
Time Complexity |
Space Complexity |
Derivative |
DFS |
O(bm) |
O(bm) |
|
DLS |
O(bl) |
O(bl) |
DFS |
IDS |
O(bd) |
O(bd) |
DLS |
BFS |
O(bd) |
O(bd) |
|
UCS |
O(bd) |
O(bd) |
BFS |
BIDI(Bidirectional) |
O(bd/2) |
O(bd/2) |
BFS(两端采用BFS) |
b, branching factor
d, tree depth of the solution
m, tree depth
l, search depth limit
综上这些算法,除了UCS(通过优先队列来选择当前Cost最小的节点优先访问)带有一点策略性,(BIDI当两端采用A* Search时也具有策略性),其他算法都是不具备决策能力的盲目式搜索,当数据量很大时,显然是低效的。