540 likes | 647 Views
搜索问题. 内容. 搜索 的目标和基本过程 深度优先搜索 广度优先搜索 带 深度控制的广度优先搜索 节点的编码和搜索效率. 内容. 搜索 的目标和基本过程 深度优先搜索 广度优先搜索 带 深度控制的广度优先搜索 节点的编码和搜索效率. 搜索. 不是通过对问题的分析找出问题的计算公式进而计算,而是根据问题的性质,找出问题的状态模型,确定包括目标状态在内的各种状态的生成方法,按一定的规律逐一生成各种可能的状态,从中寻找符合要求的目标状态 程序设计中一种常用策略 求解难以直接计算的 问题 适应 广泛. 搜索目标和基本过程. 搜索基本过程
E N D
内容 • 搜索的目标和基本过程 • 深度优先搜索 • 广度优先搜索 • 带深度控制的广度优先搜索 • 节点的编码和搜索效率
内容 • 搜索的目标和基本过程 • 深度优先搜索 • 广度优先搜索 • 带深度控制的广度优先搜索 • 节点的编码和搜索效率
搜索 • 不是通过对问题的分析找出问题的计算公式进而计算,而是根据问题的性质,找出问题的状态模型,确定包括目标状态在内的各种状态的生成方法,按一定的规律逐一生成各种可能的状态,从中寻找符合要求的目标状态 • 程序设计中一种常用策略 • 求解难以直接计算的问题 • 适应广泛
搜索目标和基本过程 • 搜索基本过程 • 一个问题所有可能的状态构成了该问题的状态空间 • 每一个具体的状态是该空间的一个节点 • 搜索过程可以看成对由表示状态的节点和表示状态转移的弧所构成的有向图遍历 • 搜索目标 • 最终目标 • 例如:八皇后、数字填图(数独) • 为了达到某种目标所要经过的中间状态或者通过的路径 • 八数码问题、全排列问题等
目标状态 八数码:初始状态和部分搜索过程
盲目搜索/启发式搜索 • 根据搜索过程中是否用到有关被搜索空间的特殊性质,搜索可分为 • 盲目搜索 • 仅根据节点的生成规则生成新的节点,并根据目标状态的要求对节点进行检测,而不用其他知识引导搜索 • 编程简单 • 对于搜索空间大的问题,效率低 • 启发式搜索 • 人工智能中的一个重要内容 • 利用与问题相关的知识来确定节点扩展和搜索的最优顺序,设计对节点的评估函数,以确定节点的扩展和搜索方向,提高搜索效率
内容 • 搜索的目标和基本过程 • 深度优先搜索 • 广度优先搜索 • 带深度控制的广度优先搜索 • 节点的编码和搜索效率
深度优先搜索(DFS)的基本算法 • 沿着一条搜索路径向处于更深层次的节点前进:优先扩展和搜索最新生成的节点 • 1. 将初始节点压入栈缓冲区内 • 2. 检查栈是否为空。当栈为空时停止搜索,否则弹出栈顶节点w • 3. 检查w是否为目标节点。如果是,则输出结果,如果需要得到其他解则转第2步,否则终止搜索。 • 4. 如果w不是目标节点,则扩展该节点,并将生成的新节点压入栈中 • 5. 转到第2步 图1. DFS基本算法
分析DFS • 可能在初始阶段误入歧途,直到搜索到该路径的终点,然手再对其他路径进行搜索 • 对于无限状态空间的问题,可能无法结束 • 状态空间有限,可能由于进入深度很大的无效路径而效率很低 • 搜索路径或者结果不唯一时,不能不保证找到的结果是所有可能结果中具有最短搜索路径的 • 适用情况 • 有限的状态空间 • 高度较为均衡的搜索树 • 需要对有限状态空间完全遍历的搜索 • 不要求寻找搜索步数最少的解的问题
举例:八皇后问题 #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) typedefstructstate_t{ short n, q[MAX_N]; //n表示当前棋盘上的皇后个数;q表示当前列上皇后的行号 } state_t; state_t stack[MAX_NODES]; intsp = 0; int queens[MAX_N];
int conflict(int q, int p, short queens[]); int queen(int n); void print_queens(state_t *st); int main( ) { state_tinit_st; init_st.n = 0; push(init_st); queen(8); return 0; } void print_queens(state_t *st) //输出结果 { inti; for (i = 0; i < st->n; i++) { printf("%d ", st->q[i]); } printf("\n"); }
int queen(int n) //解决n皇后问题 { inti, nCount=0; state_tst; while(stack_not_empty){ st = pop(); if (st.n>=n) { print_queens(&st); nCount ++; continue; } st.n++; //当前棋盘上的皇后数量+1 for(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; }
//判断是否冲突 intconflict(int q, int p, short queens[]) { inti; 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; }
回溯搜索 • 回溯搜索是深度优先搜索的另一种搜索方式,与前面一般的DFS搜索略有不同 • 对节点扩展时,每次只产生一个后继结点,并继续对这个节点进行回溯搜索 • 当一条路径搜索完成后,再返回到生成当前节点的那个节点,看是否可以从该节点再生成新的节点 • 重复上过程,直到找到搜索目标或者搜索完全部状态空间
回溯搜索的编程 • 编程实现简单 • 对当前节点的所有后继节点的遍历通过搜索中回溯机制,即函数的递归调用和返回完成 • 不需要保存后继节点的栈结构
八皇后的回溯搜索 • 作业:实现八皇后的回溯搜索,比较八皇后回溯搜索与标准深度优先搜索的输出结果。
内容 • 搜索的目标和基本过程 • 深度优先搜索 • 广度优先搜索 • 带深度控制的广度优先搜索 • 节点的编码和搜索效率
广度优先搜索(BFS) • 也是一种盲目搜索 • 节点的生成和扩展是按层次顺序进行的,处于搜索树较浅层次的节点总是先被检查和扩展 • 节点搜索的先后顺序与它们生成顺序一致 • 使用队列作为存储缓冲区
广度优先搜索算法 1. 选择初始节点并保存到队列缓冲区的尾部 2. 判断队列是否为空,当队列为空时,停止搜索,否则从队列的头部中取出节点h 3. 检查h是否为目标节点。如果h是目标节点,则输出结果。如果要得到其他解则转到第2步,否则终止搜索 4. 如果h不是目标节点,则扩展该节点,将所生成的新节点保存到队列的尾部 5. 转到第2步 • 与DFS算法非常相似,所不同的只是使用队列作为节点的存储结构 • 搜索按节点生成的层次进行 • 保证搜索到的结果具有最短的搜索路径 图2:广度优先搜索算法
八皇后问题的BFS解法 • 与八皇后的DFS算法非常类似,所不同的是节点的存取放式,只影响到相应的节点访问函数和函数queen( ) • state_t queue[MAX_NODES]; • inthd = 0, tl = 0; • #define put_node(st) queue[tl++] = st • #define get_node() queue[hd++] • #define queue_not_empty (hd < tl)
八皇后问题BFS算法分析 • 分析队列queue[ ]的大小 • 第1层8个节点,这8个节点进一步生成第2层的各个节点(首位2个可扩展出6个子节点,其余6个只能分别扩展出5个子节点),其余各层情况负责,平均至少比前一层节点都所能扩展的子节点的数量少1 • 全部节点的上限2= 5868 • 实际中生成的节点远小于这个上限 • 可见与DFS相比,BFS使用的内存空间较大,因此对各层节点都同时展开 • 对于复杂、状态空间大的问题,使用BFS时有可能由于组合爆炸而产生大量的内存消耗,以致超出计算平台资源的限制,引起搜索失败
重复节点的判断 • 八皇后问题在搜索过程中不会构成环,即搜索过程中不会出现重复的节点。这是问题的性质决定的 • 有些问题,其状态空间所构成的是带有圈的图,在搜索过程中可能生成出重复的节点。如果不处理,搜索会陷入圈中无法进展。 • 例如八数码问题
重复节点的判断(2) • 如何进行两个节点状态之间的比较 • 大部分问题,比较容易,逐一比较状态的属性即可 • 部分问题,状态描述比较复杂,需要选择适当的方法或采用适当编码技术 • 如何确定哪些节点需要比较 • 对于状态空间小的问题,比较效率影响不大 • 状态空间大,比较效率非常重要,尽可能避免不必要的比较
分油问题 • 已知A、B两个油罐的容积。A的容积小于B罐。求解使用这两个油罐量出规定油量并存放于B罐的最小操作序列。油罐容量和所求的油量均为整数,从指定文件读入(注意:油罐上没有刻度)。 • 合法操作 • 从一个油罐倒油 • 清空一个油罐的油 • 将一个油罐的油注入另一油罐中
题目分析 • 题目的性质没有限制在搜索过程中重复节点的生成。因此产生一个新节点时要对节点进行检查去重。 • 题目中涉及的对象有两类 • 所要操作的油罐 • 状态空间中的状态节点 • 记录两个油罐所乘的油量 • 要输出搜索过程的操作序列 • 记录节点之间的关联
主要数据结构(1) //油罐 typedefstruct Jar { int cap; //油罐的容量 intval; //油罐当前的存油量 int id; //油罐标识0或者1,分别表示A和B } Jar; //状态 typedefstruct State { intca, ab; //油罐A,B当前的储油量 int parent; //本节点的父亲节点在数组state[]中的下标 char *act; //生成本节点的操作名称 } State;
主要数据结构(2) Jar Ja, Jb; //分别表示A罐和B罐 State queue[MAXSTATE]; //最大状态数是(Ca+1)*(Cb+1) inttail, target; //队列尾端,B罐目标油量 char *fill[ ] = {“Fill A”, “Fill B”}; //操作名称数组 char *empty[ ] = {“Empty A”, “Empty B”}; //同上 char *pour[ ] = {“A->B”, “B->A”}; //同上 char * act_name; //当前操作的名称 intfill_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 }; //指针数组,指向操作函数
核心代码 int solve( ) { inti, 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; }
判断重复节点 int exist() { inti; for (i = 0; i < tail; i++){ if (queue[i].ca == Ja.val && queue[i].cb == Jb.val) return 1; } return 0; }
内容 • 搜索的目标和基本过程 • 深度优先搜索 • 广度优先搜索 • 带深度控制的广度优先搜索 • 节点的编码和搜索效率
带深度控制的广度优先搜索 • 广度优先搜索中,节点按照层次展开。有些问题的搜索过程中,需要对节点在搜索树中所处的层级进行明确的表示和控制。 • 确定对节点的操作方式:有些问题中对节点的扩展方式与层次密切相关。 • 控制搜索深度 • 对搜索深度有限制 • 状态空间巨大,BFS不能保证在合理的深度找到解 • 深度控制的原理 • 记录所处的层次 • 节点扩展时,每轮只对属于同一层次的节点进行操作
带深度控制的BFS框架 • 在BFS搜索框架的基础上 • 增加一个记录当前被搜索层次结尾的指针 • 搜索的循环由一层变为两层:一层控制搜索的层数,一层负责遍历每层中所有的节点 • level是控制搜索深度的变量,tail是队列尾部,end是当前层次中最后一个节点的下标 for (level = head = 0; level < max_depth; level++) { for (end = tail; head < end; head++) { ... } }
内容 • 搜索的目标和基本过程 • 深度优先搜索 • 广度优先搜索 • 带深度控制的广度优先搜索 • 节点的编码和搜索效率
节点的编码和搜索效率 • 例:华容道——经典智力问题 通过移动各个棋子,帮助曹操从初始位置移到棋盘最下方中部,从出口逃走。
具体描述 • 十个大小分别是1*1、1*2、2*1以及2*2个单位的矩形棋子放在一个4*5的矩形棋盘上,游戏的目标:将其中唯一一个2*2的矩形棋子从初始位置移动到棋盘正下方宽度为2的出口处。在移动的过程中棋子不得互相重叠或者出界。给出华容道的初始布局,输出解决该问题的最小移动序列
题目分析 • 求解最小序列——适合BFS • 任一个给定的初始状态不一定有解,搜索空间大,在搜索过程中需要控制搜索深度 • 重复状态的检查 • 状态空间大——12! • 棋子形状不同 • 布局中有些棋子的形状是相同的 • 注意:形状相同的棋子互换——状态不变 • 状态节点数量大,查重的效率影响大
主要数据结构(1) typedefstruct 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" };
主要数据结构(2) • 搜索过程中棋子位置可能改变,但其名称、长宽不会变化,各个棋子在棋盘上的位置组合就描述了搜索过程中的状态 • 为棋子单独定义在搜索过程中使用的数据结构如下: • 状态描述:使用有10个元素的pos数组,按照与数组pieces的对应顺序依次将各个棋子的位置保存在数组元素中 typedefstructpos { short x; short y; } pos;
如何保存状态? • 表示状态的数据结构 • 直接保存相应的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的逆函数 typedestruct state { unsigned long p1, p2; long parent; } state;
函数putpos的实现 //对pos数组(表示棋子位置)进行编码 void putpos(int n, pos *ps) { inti; 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逆函数getpos的实现 //节点解码得到表示棋子位置的pos数组 void getpos(int n , pos *ps) { inti; 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; } }
搜索代码 int search(intmax_step) { inti, 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); //节点扩展 } } }
节点扩展代码 int (*actions[ ])(int, pos *) = { go_left, go_right, go_up, go_down }; //移动棋子即节点扩展 void move_piece(intp_node, int p, posps[ ]) { inti; state tmpt; buffer_check( ); //检查queue[ ]是否有足够空间保存可能节点 for (i = 0; i < NumberOf(actions); i++){ if (actions[i](p, ps) && !exist(tail)){ queue[tail++].parent = p_node; } } }
intgo_left(inti, posps[]) { intd; 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; }
检查重复节点——函数exists( ) • 1. 如何判断两个节点是否相同? • 从棋盘局面看,一个表示棋盘状态的pos数组之前没有出现过,则认为是一个新节点 • 从搜索看,具有相同形状的棋子的位置互换是两个互相重复的相同状态 • 根据棋子的形状和位置对棋盘进行描述 • 状态与各个位置上的棋子形状有关 • 如何按照棋子的形状和位置对棋盘进行描述? • 扫描棋盘 • 为棋子的形状编码,直接在棋盘上记录每个位置上棋子的形状 • 2. 如果提高节点比较的效率?
共4种棋子,加上空格,每个位置上可以放置的棋子有5种不同的形状,编码使用3位二进制即可共4种棋子,加上空格,每个位置上可以放置的棋子有5种不同的形状,编码使用3位二进制即可 • 棋子的宽度左移1位,再与高度“按位或” • 2*2 —— 6 • 2*1 —— 5 • 1*2 —— 2 • 1*1 —— 3