54
搜搜搜搜

搜索问题

Embed Size (px)

DESCRIPTION

搜索问题. 内容. 搜索 的目标和基本过程 深度优先搜索 广度优先搜索 带 深度控制的广度优先搜索 节点的编码和搜索效率. 内容. 搜索 的目标和基本过程 深度优先搜索 广度优先搜索 带 深度控制的广度优先搜索 节点的编码和搜索效率. 搜索. 不是通过对问题的分析找出问题的计算公式进而计算,而是根据问题的性质,找出问题的状态模型,确定包括目标状态在内的各种状态的生成方法,按一定的规律逐一生成各种可能的状态,从中寻找符合要求的目标状态 程序设计中一种常用策略 求解难以直接计算的 问题 适应 广泛. 搜索目标和基本过程. 搜索基本过程 - PowerPoint PPT Presentation

Citation preview

Page 1: 搜索问题

搜索问题

Page 2: 搜索问题

内容 搜索的目标和基本过程 深度优先搜索 广度优先搜索 带深度控制的广度优先搜索 节点的编码和搜索效率

Page 3: 搜索问题

内容 搜索的目标和基本过程 深度优先搜索 广度优先搜索 带深度控制的广度优先搜索 节点的编码和搜索效率

Page 4: 搜索问题

搜索 不是通过对问题的分析找出问题的计算公

式进而计算,而是根据问题的性质,找出问题的状态模型,确定包括目标状态在内的各种状态的生成方法,按一定的规律逐一生成各种可能的状态,从中寻找符合要求的目标状态 程序设计中一种常用策略 求解难以直接计算的问题 适应广泛

Page 5: 搜索问题

搜索目标和基本过程 搜索基本过程

一个问题所有可能的状态构成了该问题的状态空间 每一个具体的状态是该空间的一个节点 搜索过程可以看成对由表示状态的节点和表示状态转

移的弧所构成的有向图遍历 搜索目标

最终目标 例如:八皇后、数字填图(数独)

为了达到某种目标所要经过的中间状态或者通过的路径

八数码问题、全排列问题等

Page 6: 搜索问题

摘自 北大《数据结构与算法》讲义 张铭

Page 7: 搜索问题

2 8 3

1 6 4

7 5

2 8 3

1 6 4

7 5

2 8 3

1 4

7 6 5

2 8 3

1 4

7 6 5

2 8 3

1 6 4

7 5

2 8 3

1 4

7 6 5

1 2 3

8 4

7 6 5

2 3

1 8 4

7 6 5

2 8 3

1 6 4

7 5

2 8 3

1 4

7 6 5

2 8

1 4 3

7 6 5

2 8 3

1 4 5

7 6

八数码:初始状态和部分搜索过程

目标状态

Page 8: 搜索问题

盲目搜索 / 启发式搜索 根据搜索过程中是否用到有关被搜索空间的特殊

性质,搜索可分为 盲目搜索

仅根据节点的生成规则生成新的节点,并根据目标状态的要求对节点进行检测,而不用其他知识引导搜索

编程简单 对于搜索空间大的问题,效率低

启发式搜索 人工智能中的一个重要内容 利用与问题相关的知识来确定节点扩展和搜索的最优顺序,

设计对节点的评估函数,以确定节点的扩展和搜索方向,提高搜索效率

Page 9: 搜索问题

内容 搜索的目标和基本过程 深度优先搜索 广度优先搜索 带深度控制的广度优先搜索 节点的编码和搜索效率

Page 10: 搜索问题

深度优先搜索 (DFS) 的基本算法 沿着一条搜索路径向处于更深层次的节点

前进:优先扩展和搜索最新生成的节点1. 将初始节点压入栈缓冲区内2. 检查栈是否为空。当栈为空时停止搜索,否则弹出栈顶节点 w3. 检查 w 是否为目标节点。如果是,则输出结果,如果需要得到其他解则转第 2 步,否则终止搜索。4. 如果 w 不是目标节点,则扩展该节点,并将生成的新节点压入栈中5. 转到第 2 步

图 1. DFS 基本算法

Page 11: 搜索问题

分析 DFS 可能在初始阶段误入歧途,直到搜索到该路径的终点,

然手再对其他路径进行搜索 对于无限状态空间的问题,可能无法结束 状态空间有限,可能由于进入深度很大的无效路径而效率很

低 搜索路径或者结果不唯一时,不能不保证找到的结果是所有

可能结果中具有最短搜索路径的 适用情况

有限的状态空间 高度较为均衡的搜索树 需要对有限状态空间完全遍历的搜索 不要求寻找搜索步数最少的解的问题

Page 12: 搜索问题

举例:八皇后问题#include <iostream>using namespace std;

#define MAX_N 8#define MAX_NODES 36 // 栈的大小

#define pop( ) stack[--sp]#define push(node) stack[sp++] = node#define stack_not_empty (sp>0)

typedef struct state_t{short n, q[MAX_N]; //n 表示当前棋盘上的皇后个数; q

表示当前列上皇后的行号} state_t;

state_t stack[MAX_NODES];int sp = 0;int queens[MAX_N];

Page 13: 搜索问题

int conflict(int q, int p, short queens[]);int queen(int n);void print_queens(state_t *st);

int main( ){

state_t init_st;

init_st.n = 0;push(init_st);queen(8);return 0;

}

void print_queens(state_t *st) // 输出结果{

int i;for (i = 0; i < st->n; i++){

printf("%d ", st->q[i]);}printf("\n");

}

Page 14: 搜索问题

int queen(int n) // 解决 n 皇后问题{

int i, nCount=0;state_t st;

while(stack_not_empty){st = pop();if (st.n>=n){

print_queens(&st);nCount ++;continue;

}st.n++; // 当前棋盘上的皇后数量 +1for(i=0; i<n; i++){ //之前的皇后与当前皇后位置 i 是否冲突

if (conflict(st.n-1, i, st.q))continue;

st.q[st.n-1] = i; // 不冲突,放在 i 行push(st); // 新生成的节点入栈

}}printf("%d\n", nCount);return 0;

}

Page 15: 搜索问题

//判断是否冲突int conflict(int q, int p, short queens[]){

int i;for (i = 0; i < q; i++){

if (queens[i] == p) // 在同一行return 1;

if (queens[i] - p == q - i) //dx = -dy;反斜线return 1;

if (queens[i] - p == i - q) //dx = dy; 斜线return 1;

}return 0;

}

Page 16: 搜索问题

回溯搜索 回溯搜索是深度优先搜索的另一种搜索方

式,与前面一般的 DFS 搜索略有不同 对节点扩展时,每次只产生一个后继结点,并继续对这个节点进行回溯搜索

当一条路径搜索完成后,再返回到生成当前节点的那个节点,看是否可以从该节点再生成新的节点

重复上过程,直到找到搜索目标或者搜索完全部状态空间

Page 17: 搜索问题

摘自 北大《数据结构与算法》讲义 张铭

Page 18: 搜索问题

回溯搜索的编程 编程实现简单

对当前节点的所有后继节点的遍历通过搜索中回溯机制,即函数的递归调用和返回完成

不需要保存后继节点的栈结构

Page 19: 搜索问题

八皇后的回溯搜索 作业:实现八皇后的回溯搜索,比较八皇

后回溯搜索与标准深度优先搜索的输出结果。

Page 20: 搜索问题

内容 搜索的目标和基本过程 深度优先搜索 广度优先搜索 带深度控制的广度优先搜索 节点的编码和搜索效率

Page 21: 搜索问题

广度优先搜索( BFS ) 也是一种盲目搜索 节点的生成和扩展是按层次顺序进行的,

处于搜索树较浅层次的节点总是先被检查和扩展

节点搜索的先后顺序与它们生成顺序一致 使用队列作为存储缓冲区

Page 22: 搜索问题

广度优先搜索算法

与 DFS 算法非常相似,所不同的只是使用队列作为节点的存储结构 搜索按节点生成的层次进行 保证搜索到的结果具有最短的搜索路径

1. 选择初始节点并保存到队列缓冲区的尾部2. 判断队列是否为空,当队列为空时,停止搜索,否则从队列的头部中取出节点 h3. 检查 h 是否为目标节点。如果 h 是目标节点,则输出结果。如果要得到其他解则转到第 2 步,否则终止搜索4. 如果 h 不是目标节点,则扩展该节点,将所生成的新节点保存到队列的尾部5. 转到第 2 步

图 2 :广度优先搜索算法

Page 23: 搜索问题

八皇后问题的 BFS 解法 与八皇后的 DFS 算法非常类似,所不同的是节

点的存取放式,只影响到相应的节点访问函数和函数 queen( )

state_t queue[MAX_NODES]; int hd = 0, tl = 0;

#define put_node(st) queue[tl++] = st #define get_node() queue[hd++] #define queue_not_empty (hd < tl)

Page 24: 搜索问题

八皇后问题 BFS 算法分析 分析队列 queue[ ] 的大小

第 1 层 8 个节点,这 8 个节点进一步生成第 2 层的各个节点(首位 2 个可扩展出 6 个子节点,其余 6 个只能分别扩展出 5 个子节点),其余各层情况负责,平均至少比前一层节点都所能扩展的子节点的数量少 1

全部节点的上限 2 = 5868 实际中生成的节点远小于这个上限

可见与 DFS 相比, BFS使用的内存空间较大,因此对各层节点都同时展开

对于复杂、状态空间大的问题,使用 BFS 时有可能由于组合爆炸而产生大量的内存消耗,以致超出计算平台资源的限制,引起搜索失败

Page 25: 搜索问题

重复节点的判断 八皇后问题在搜索过程中不会构成环,即

搜索过程中不会出现重复的节点。这是问题的性质决定的

有些问题,其状态空间所构成的是带有圈的图,在搜索过程中可能生成出重复的节点。如果不处理,搜索会陷入圈中无法进展。 例如八数码问题

Page 26: 搜索问题
Page 27: 搜索问题

重复节点的判断( 2 ) 如何进行两个节点状态之间的比较

大部分问题,比较容易,逐一比较状态的属性即可

部分问题,状态描述比较复杂,需要选择适当的方法或采用适当编码技术

如何确定哪些节点需要比较 对于状态空间小的问题,比较效率影响不大 状态空间大,比较效率非常重要,尽可能避免

不必要的比较

Page 28: 搜索问题

分油问题 已知 A 、 B两个油罐的容积。 A 的容积小

于 B罐。求解使用这两个油罐量出规定油量并存放于 B罐的最小操作序列。油罐容量和所求的油量均为整数,从指定文件读入(注意:油罐上没有刻度)。

合法操作 从一个油罐倒油 清空一个油罐的油 将一个油罐的油注入另一油罐中

Page 29: 搜索问题

题目分析 题目的性质没有限制在搜索过程中重复节

点的生成。因此产生一个新节点时要对节点进行检查去重。

题目中涉及的对象有两类 所要操作的油罐 状态空间中的状态节点

记录两个油罐所乘的油量 要输出搜索过程的操作序列

记录节点之间的关联

Page 30: 搜索问题

主要数据结构( 1 )

//油罐typedef struct Jar {

int cap; //油罐的容量int val; //油罐当前的存油量int id; //油罐标识 0 或者 1 ,分别表示 A 和 B

} Jar;

// 状态typedef struct State {

int ca, ab; //油罐 A,B 当前的储油量int parent; // 本节点的父亲节点在数组 state[] 中的下

标char *act; // 生成本节点的操作名称

} State;

Page 31: 搜索问题

主要数据结构( 2 )Jar Ja, Jb; // 分别表示 A罐和 B罐State queue[MAXSTATE]; // 最大状态数是 (Ca+1)*(Cb+1)

int tail, target; //队列尾端, B罐目标油量char *fill[ ] = {“Fill A”, “Fill B”}; //操作名称数组char *empty[ ] = {“Empty A”, “Empty B”}; //同上char *pour[ ] = {“A->B”, “B->A”}; //同上

char * act_name; // 当前操作的名称

int fill_a( ), fill_b( ), empty_a( ), empty_b( ), a_to_b( ), b_to_a( ); // 定义操作函数

int (*action[])( ) = { fill_a, fill_b, empty_a, empty_b, a_to_b, b_to_a }; //指针数组,指向操作函数

Page 32: 搜索问题

核心代码int solve( ){

int i, j;

for (i = 0; i < tail; i++){if (succeed(i)){ // 找到目标状态

show(i); //递归打印出达到目标状态的操组序列return 1;

}

for (j = 0; j< NumberOf(action); j++){setsit(i); // 将油罐油量设置为节点 i 的状态if (action[j]() && !exist()) //执行 j操作并检查是否重

复append(i); //j操作后的状态加入队列中

}}return 0;

}

Page 33: 搜索问题

判断重复节点

int exist(){

int i;

for (i = 0; i < tail; i++){if (queue[i].ca == Ja.val && queue[i].cb == Jb.val)

return 1;}return 0;

}

Page 34: 搜索问题

内容 搜索的目标和基本过程 深度优先搜索 广度优先搜索 带深度控制的广度优先搜索 节点的编码和搜索效率

Page 35: 搜索问题

带深度控制的广度优先搜索 广度优先搜索中,节点按照层次展开。有些问题的

搜索过程中,需要对节点在搜索树中所处的层级进行明确的表示和控制。

确定对节点的操作方式:有些问题中对节点的扩展方式与层次密切相关。

控制搜索深度 对搜索深度有限制 状态空间巨大, BFS 不能保证在合理的深度找到解

深度控制的原理 记录所处的层次 节点扩展时,每轮只对属于同一层次的节点进行操作

Page 36: 搜索问题

带深度控制的 BFS框架 在 BFS 搜索框架的基础上

增加一个记录当前被搜索层次结尾的指针 搜索的循环由一层变为两层:一层控制搜索的

层数,一层负责遍历每层中所有的节点

level 是控制搜索深度的变量, tail 是队列尾部,end 是当前层次中最后一个节点的下标

for (level = head = 0; level < max_depth; level++) {for (end = tail; head < end; head++) {...}

}

Page 37: 搜索问题

内容 搜索的目标和基本过程 深度优先搜索 广度优先搜索 带深度控制的广度优先搜索 节点的编码和搜索效率

Page 38: 搜索问题

节点的编码和搜索效率 例:华容道——经典

智力问题通过移动各个棋子,

帮助曹操从初始位置移到棋盘最下方中部,从出口逃走。

Page 39: 搜索问题

具体描述 十个大小分别是 1*1 、 1*2 、 2*1 以及 2*2

个单位的矩形棋子放在一个 4*5 的矩形棋盘上,游戏的目标:将其中唯一一个 2*2的矩形棋子从初始位置移动到棋盘正下方宽度为 2 的出口处。在移动的过程中棋子不得互相重叠或者出界。给出华容道的初始布局,输出解决该问题的最小移动序列

Page 40: 搜索问题

题目分析 求解最小序列——适合 BFS 任一个给定的初始状态不一定有解,搜索空间

大,在搜索过程中需要控制搜索深度 重复状态的检查 状态空间大—— 12! 棋子形状不同 布局中有些棋子的形状是相同的

注意:形状相同的棋子互换——状态不变 状态节点数量大,查重的效率影响大

Page 41: 搜索问题

主要数据结构 (1)

基本操作对象:棋子 对右图布局的描述

typedef struct piece{short x, y; // 棋子的位置 (左下

角)short w, h; // 棋子的宽度和长度char name[8]; // 棋子的名称

} piece;piece pieces[] = {

1, 3, 2, 2, "曹操 ",1, 2, 2, 1, " 关羽 ",0, 3, 1, 2, " 张飞 ",0, 1, 1, 2, "赵云 ",3, 3, 1, 2, "马超 ",3, 1, 1, 2, "黄忠 ",0, 0, 1, 1, "兵 1",1, 1, 1, 1, "兵 2",2, 1, 1, 1, "兵 3",3, 0, 1, 1, "兵 4",1, 0, 1, 1, " 空 _1",2, 0, 1, 1, " 空 _2"

};

Page 42: 搜索问题

主要数据结构 (2)

搜索过程中棋子位置可能改变,但其名称、长宽不会变化,各个棋子在棋盘上的位置组合就描述了搜索过程中的状态

为棋子单独定义在搜索过程中使用的数据结构如下:

状态描述:使用有 10 个元素的 pos 数组,按照与数组 pieces 的对应顺序依次将各个棋子的位置保存在数组元素中

typedef struct pos {short x;short y;

} pos;

Page 43: 搜索问题

如何保存状态? 表示状态的数据结构

直接保存相应的 pos 数组——最简单方法 每个节点 40 个字节 状态空间大,占用空间大

实际上棋盘是 4*5 的矩形, x坐标 0~3 , y坐标 0~4 ,所以 x可以用 2 个二进制位, y 可以用 3 个二进制位,即一个棋子用 5个二进制位, 10 个棋子 +2 个空位用 60 个二进制位—— 2 个32位整数( 8 字节)

state queue[SSIZE]; // 状态空间存储队列 pos 数组压缩为 state 结构并加入队列中通过函数 putpos( )实现 getpos 是 putpos 的逆函数

typede struct state {unsigned long p1, p2;long parent;

} state;

Page 44: 搜索问题

// 对 pos 数组(表示棋子位置)进行编码void putpos(int n, pos *ps){

int i;unsigned long m1 = 0, m2 = 0;

for (i = 0; i < N_PIECES / 2; i++){m1 = (m1 << 5) | (ps[i].x << 3 | ps[i].y );m2 = (m2 << 5) |

(ps[i + N_PIECES / 2].x << 3 | ps[ i + N_PIECES / 2].y );}

queue[n].p1 = m1;queue[n].p2 = m2;

}

函数 putpos 的实现

Page 45: 搜索问题

putpos逆函数 getpos 的实现// 节点解码得到表示棋子位置的 pos 数组void getpos(int n , pos *ps){

int i;unsigned long m1, m2;

m1 = queue[n].p1;m2 = queue[n].p2;

for (i = N_PIECES / 2 - 1; i >= 0; i--){ps[i].y = m1 & 07;ps[i].x = (m1 >> 3) & 03;m1 >>= 5;

ps[i + N_PIECES / 2].y = m2 & 07;ps[i + N_PIECES / 2].x = (m2 >> 3) & 03;m2 >>= 5;

}}

Page 46: 搜索问题

搜索代码int search(int max_step){

int i, j, end, level;pos p[N_PIECES];

for (level = i = 0; i < tail && level <= max_step; level++){for (end = tail; i < end; i++){

getpos(i, p);if (reached(p)){

show_res(i);return 1;

}for (j = 0; j < 10; j++)

move_piece(i, j, p); // 节点扩展}

}}

Page 47: 搜索问题

节点扩展代码int (*actions[ ])(int, pos *) = {

go_left, go_right, go_up, go_down};

// 移动棋子即节点扩展void move_piece(int p_node, int p, pos ps[ ]){

int i;state tmpt;

buffer_check( ); // 检查 queue[ ] 是否有足够空间保存可能节点for (i = 0; i < NumberOf(actions); i++){

if (actions[i](p, ps) && !exist(tail)){queue[tail++].parent = p_node;

}}

}

Page 48: 搜索问题

int go_left(int i, pos ps[]){

int d;pos t[N_PIECES];

for (d = EMPTY_1; d <= EMPTY_2; d++){if (ps[i].y == ps[d].y && (ps[i].x - ps[d].x) == 1){

if (pieces[i].h == 1){copy(ps, t);t[i].x -= 1;t[d].x += pieces[i].w;putpos(tail, t);return 1;

}else if (ps[EMPTY_1].x == ps[EMPTY_2].x && ps[EMPTY_1 + EMPTY_2 - d].y - ps[d].y == 1){

copy(ps, t);t[i].x -= 1;t[EMPTY_1].x = t[EMPTY_2].x = ps[d].x + pieces[i].w;putpos(tail, t);return 1;

}}

}return 0;

}

Page 49: 搜索问题

检查重复节点——函数 exists( ) 1. 如何判断两个节点是否相同?

从棋盘局面看,一个表示棋盘状态的 pos 数组之前没有出现过,则认为是一个新节点

从搜索看,具有相同形状的棋子的位置互换是两个互相重复的相同状态

根据棋子的形状和位置对棋盘进行描述 状态与各个位置上的棋子形状有关

如何按照棋子的形状和位置对棋盘进行描述? 扫描棋盘 为棋子的形状编码,直接在棋盘上记录每个位置上棋子的形

状 2. 如果提高节点比较的效率?

Page 50: 搜索问题

共 4 种棋子,加上空格,每个位置上可以放置的棋子有 5 种不同的形状,编码使用 3位二进制即可 棋子的宽度左移 1位,再与高度“按位或” 2*2 —— 6 2*1 —— 5 1*2 —— 2 1*1 —— 3

Page 51: 搜索问题

记录盘面模式的方法 4*5 的二维数组

占用空间大,比较效率低 对盘面模式进行编码

节约空间,比较效率高 20 个格子,每个格子一个棋子,一个棋子 3位二进制,总共 60 个二进制。 2 个 32位整数即可 typedef struct pattern {

unsigned long p1, p2;} pattern;

Page 52: 搜索问题

根据棋盘局面生成盘面模式的函数void getpatt(int n, pattern *patt){

pos t[N_PIECES];unsigned long pat[2] = {0, 0};int i, id, p, off;

getpos(n, t);for (i = 0; i < N_PIECES - 2; i++){

p = pieces[i].w << 1 | pieces[i].h;id = t[i].x >> 1;off = t[i].y + 3 + ((t[i].x & 0x1) << 4);pat[id] |= p << off;

}patt->p1 = pat[0];patt->p2 = pat[1];

}

Page 53: 搜索问题

int exists(int n){

int i;pattern node_pat, cur_pat;

getpatt(n, &cur_pat);for (i = 0; i < tail; i++){

getpatt(i, &node_pat);if (cur_pat.p1 == node_pat.p1 && cur_pat.p2 == node_pat.p2)

return 1;}return 0;

}

检查重复节点

Page 54: 搜索问题

结果说明 图示布局称为“横刀立马”,民间的说法

是最优解需要 81 步,按照上述搜索方法得到的是 116 步 两者对步数的计算不一样