搜索算法介绍
(1)穷举搜索(Exhaustive Search)
(2)盲目搜索(Blind or Brute-Force Search)
- 深度优先(DFS)或回溯搜索(Backtracking);
- 广度优先搜索(BFS);
- 迭代加深搜索(Iterative Deepening);
- 分枝限界法(Branch & Bound);
- 博弈树搜索(α-β Search)
(3)启发式搜索(Heuristic Search)
- A*算法和最佳优先(Best-First Search)
- 迭代加深的A*算法
- B*,AO*,SSS*等算法
- Local Search, GA等算法
搜索空间的三种表示
- 表序表示: 搜索对象用线性表数据结构表示;
- 显式图表示: 搜索对象在搜索前就用图(树)的数据结构表示;
- 隐式图表示: 除了初始结点,其他结点在搜索过程中动态生成.缘于搜索空间大,难以全部存储.
提高搜索效率的思考:随机搜索
- 上世纪70年代中期开始, 国外一些学者致力于研究随机搜索求解困难的组合问题, 将随机过程引入搜索;
- 选择规则是随机地从可选结点中取一个,从而可以从统计角度分析搜索的平均性能;
- 随机搜索的一个成功例子:判定一个很大的数是不是素数, 获得了第一个多项式时间的算法;
回溯法:一个既带有系统性又带有跳跃性的搜索算法。
- 系统性:它在包含问题的所有解的解空间树中,按照深度优先的策略,从根结点出发搜索解空间树。
- 跳跃性:算法搜索至解空间树的任一结点时,判断该结点为根的子树是否包含问题的解,如果肯定不包含,则跳过以该结点为根的子树的搜索,逐层向其祖先结点回溯。否则,进入该子树,继续按深度优先的策略进行搜索。
这种以深度优先的方式系统地搜索问题的解的算法称为回溯法,它适用于解一些组合数较大的问题。
- 搜索从开始结点(根结点)出发,以DFS搜索整个解空间。
- 开始结点就成为一个活结点,同时也成为当前的扩展结点。在当前的扩展结点处向纵深方向移至一个新结点,并成为一个新的活结点,也成为当前扩展结点。
- 如果在当前的扩展结点处不能再向纵深方向扩展,则当前扩展结点就成为死结点。
- 此时,应往回移动(回溯)至最近的一个活结点处,并使这个活结点成为当前的扩展结点;直至找到一个解或全部解。
回溯算法的基本步骤
- (1)针对问题,定义问题的解空间(对解进行编码);
- (2)确定易于搜索的解空间组织结构(按树或图组织解);
- (3)以深度优先方式搜索解空间,搜索过程中裁减掉死结点的子树提高搜索效率。
注解:
(1)提高回溯法效率的两种方法
- ①用约束函数剪去不满足约束的子树;
- ②用限界函数剪去不能得到最优解的子树。
(2)两类常见的解空间树
- ①子集树:如0-1背包,叶结点数2n,总结点数2n+1,遍历时间为Ω(2n);
- ②排列树:如TSP问题,叶结点数n!,遍历时间为Ω(n!)。
回溯算法的一般框架:
1. 子集树回溯算法:
Backtrack(int t) {//搜索到树的第t层
//由第t层向第t+1层扩展,确定x[t]的值
if t>n thenoutput(x); //叶结点是可行解,输出解
else
while( all Xt)do { //Xt为所有x[t]的合法取值集
x[t]= Xt中第i个值;
if(Constraint(t) and Bound(t))
Backtrack(t+1);
}
}
执行时:Backtrack(1) //从1扩展并回溯
2. 排列树回溯算法:
Backtrack(int t) {//搜索到树的第t层
//由第t层向第t+1层扩展,确定x[t]的值
if t>n thenoutput(x); //叶结点是可行解,输出解
else
for i=t to n do {
swap(x[t], x[i]);
if(Constraint(t) andBound(t))
Backtrack(t+1);
swap(x[t], x[i]);
}
}
示例一:树和图的遍历
二叉树的遍历:
- 先序遍历
- 中序遍历
- 后序遍历
- 按层次遍历
图的遍历
- 深度优先遍历
- 广度优先遍历
- 双向广度优先遍历
二叉树的遍历
二叉链表的存储结构
typedefstruct BiTnode {
ElemType data;
struct BiTnode *lchild, *rchild;
}*BiTree;
Preorder(BiTreeT) {
//递归程序
if T!=nil then {
visit(T);
Preorder(T->lchild);
Preorder(T->rchild);
}
}
Preorder(BiTreeT){
//非递归程序
if T=nil then return;
inistack(S);push(S, T);
while(!empty(S))do{
BiTree p=pop(S);
while(p!=nil) do{
visit(p);
push(S, p->rchild);
p=p->lchild;
}
}
}
BFSorder(BiTreeT){
//按层次遍历
if T=nil then return;
iniqueue(Q);
enqueeu(Q,T);
while(!empty(Q))do{
BiTree p=Dequeue(Q);
visit(p);
if p->lchild!=nil then
enqueue(Q,p->lchild);
if(p->rchild!=NIL) then
enqueue(Q,p->rchild);
}
}
图的遍历:
深度优先搜索(DFS)
DFS(v0){
v0.visited=True;
while(所有与v0邻接的顶点v and !v.visited ) do
DFS(v);
return;
}
广度优先搜索(BFS)
BFS(v0){
iniqueue(Q);
enqueue(Q,v0);
while(!empty(Q)) do {
p=dequeue(Q);
p.visited=True;
while(所有与p邻接的顶点v && !v.visited ) do
enqueue(Q, v);
}
}
双向广度优先搜索
对于迷宫等问题(S为入口,T为出口),可以采用双向广度优先搜索。S正向搜索,T反向搜索,当两个方向搜索在某层上生成同一结点时,即找到一条路径。算法比单向搜索的结点数少得多。
实例二:N后问题
在4×4棋盘上放上4个皇后,使皇后彼此不受攻击。不受攻击的条件是彼此不在同行(列)、斜线上。求出全部的放法。
解编码:(x1,x2,x3,x4)4元组,xi表示皇后i放在i行上的列号,如(3,1,2,4)
解空间:{(x1,x2,x3,x4)| xi∈S, i=1~4} S={1,2,3,4}
解空间树(满四叉树):
bool placetest(int k){
int i = 1;
for (; i<k; i++)
if (X[i] == X[k] || abs(X[i] - X[k]) == abs(i - k))
return false;
return true;
}
void NQueen(int k){
int i = 1;
if (k>SIZE)
print(X);
else
for (; i <= SIZE; i++){
X[k] = i;
if (placetest(k))
NQueen(k + 1);
}
}
求解时执行NQueen(1);
示例三:排列生成问题
给定正整数n,生成1,2,…,n所有排列
解空间树(排列树)
当n=3时
回溯算法
Backtrack(int t){
if t>n then output(x);
else
for i=t to n do{
swap(x[t], x[i]);
Backtrack(t+1);
swap(x[t], x[i]);
}
}
实例四:TSP问题
有一个旅行商人要拜访n个城市,他必须选择所要走的路径,路径的限制是每个城市只能拜访一次,而且最后要回到原来出发的城市。路径的选择目标是要求得的路径路程为所有路径之中的最小值
基本思想:
利用排列生成问题的回溯算法Backtrack(2),对x[]={1,2,…,n}的x[2..n]进行全排列,则(x[1],x[2]),(x[2],x[3]),…, (x[n],x[1])构成一个回路。在全排列算法的基础上,进行路径计算保存以及进行限界剪枝
TSPBacktrack(int i){
//cc记录(x[1],x[2]), …,(x[i-1],x[i])的距离和
if i>n then //搜索到叶结点,输出可行解与当前最优解比较{
if ( cc+a[x[n]][1]<bestv or bestv=∞) then{
bestv=cc+a[x[n]][1];
for j=1 to n do
bestx[j]=x[j];
}
}else {
for j=i to n do
if ( cc+a[x[i-1]][x[j]]<bestv or bestv=∞) then{
//限界裁剪子树
swap(x[i], x[j]);
cc+=a[x[i-1]][x[i]];
TSPBacktrack(i+1);
cc-=a[x[i-1]][x[i]];
swap(x[i], x[j]);
}
}
}
示例五:0-1背包问题
解空间树:
无限界函数的算法
KnapBacktrack(int i){
//cw当前背包重量,cv当前背包价值,bestv当前最优价值
if i>n then {
//搜索到可行解
bestv=(bestv<cv)?cv:bestv;
output(x);
}else {
if cw+w[i]<=c then{
//走左子树
x[i]=1;
cw+=w[i];
cv+=v[i];
KnapBacktrack(i+1);
cw-=w[i];
cv-=v[i];
}
//以下走右子树
{ x[i]=0;
KnapBacktrack(i+1);
}
}//else
}//end