搜索介绍

来源:互联网 发布:孙总定制机箱淘宝店 编辑:程序博客网 时间:2024/06/05 19:38

这周是搜索的题目,写一点对搜索算法的介绍,希望能帮助大家。

关键字【深搜】【IDS】【广搜】【双向广搜】【IDS】【A*】【IDA*】

背景题目: 

1:http://acm.hdu.edu.cn/showproblem.php?pid=1242(深搜或广搜)

2 : http://poj.org/problem?id=1011(剪枝)

3:http://poj.org/problem?id=1077 (IDS, A*, IDA*,  BFS)

4

http://uva.onlinejudge.org/index.php?option=com_onlinejudge&Itemid=8&page=show_problem&category=13&problem=1122&mosmsg=Submission+received+with+ID+9223032( IDA*)

一:深度优先搜索。

深度优先搜索对应使用的数据结构是栈,也就是说深度优先搜索的迭代行非常好,每次都是用最新的节点去扩展。栈中始终只保留着一条路径的状态信息,因此使用的内存空间比较少。但是裸的深搜算法时间复杂度太高,经常要我们根据题目的信息加一些优化剪枝,有时是需要我们判断重复的状态。

//使用栈写

Int dfs(Start_state, deep){

         Top= 0;

         Stack[top++]  = start_state;  //初始状态入栈

         While(top){

                   Temp= stack[--top] ;   //取出栈顶元素

                   If(Is_goal(temp) ) return temp;  //如果找到了则返回

                   For(i= 枚举由temp产生的状态){

                            If(i合法) strack[top ++] = I;

                   }

    }

}

使用递归(即使用系统的栈)

Int dfs(now_state, MAX_deep){

    If(now_state.deep> MAX_deep) return ;

         If(Is_goal(temp) ) return temp;  //如果找到了则返回

         For(i= 枚举由temp产生的状态){

                   If(i合法) dfs(i, deep)

         }

}

 

手写栈在内存消耗和时间上都要优于递归,如果写的递归返回栈溢出的信息或者超时,那就改成手写栈试试。

 

由于深搜是不见棺材不掉眼泪的,因此很容易走到很深而且没有解的地方,而又可能在很浅的地方又有解,为了避免这种问题,引入了IDS,迭代加深搜索,就是控制每次搜索的深度进行深搜:

For(deep = 初始深度; deep < 最大深度;deep ++)dfs(start_state, deep);

虽然deep每次增加使我们又将所有状态重新搜索了一遍,但是由于深搜的时间复杂度是深度的指数级,因此前面的状态用的时间对当前深度影响不会很大。

 

 

二:广度优先搜索

广搜在求解问题时时间效率很高,尤其是求解最优性问题。但是由于我们要将在未到达目标状态前的所有状态都存下来,因此空间消耗太大,而且大部分问题每一层的节点数都是以指数级方式增长,因此循环队列也解决不了内存问题。广搜是用队列来实现的,而且在广搜被用到求解最优问题时,队列一定要关于某一关键字递增

int bfs(){

         head= tail = 0;

         queue[tail++] = start_state;

         while(head!= tail){

                   now_state= queue[head ++];

    If( Is_goal(temp) ) return temp;  //如果找到了则返回

                   For(i= 枚举由temp产生的状态){

                            If(i合法) queue[tail ++] = I;

                   }

}

}

 

 

 

例子.

题目1的题意是说,在一个n×M的网格里要从a走到r。‘.’是路,‘#’是敌人,走一步需要花费1,打一个敌人需要花费1,求从a走到r的最少花费。这个题目用深搜很好写,遇到’.’则花费加1,继续搜。遇到‘#’则花费加2,继续搜。但是这是最优化的问题,明显用广搜更加合适,而且如果这个题的数据加强到1000,深搜很有可能超时。对于之前遇到’#’不能走的问题,我们直接用一个队列,把由当前状态扩展出来的结点放到队列尾部即可,但是这用方法对于本题是不行的。为什么会出现这种情况呢,因为花费有两种,如果只是按时间顺序的话,先扩展的点不一定就是最小的花费,有可能从另一个地方到达他又更小的花费。而之前的问题里,花费只有一种,因此花费和时间是等价的。

想想上面提到的一句话“队列一定要关于某一关键字递增”此题的关键字就是花费,当我们把一个被扩展出来的节点放入队列是,一定要保证队列关于花费单调递增,也就是说我们每次拿出来的去扩展节点的点,必须是队列中花费最小的点。这样才能保证最优解的正确性。这里通常用单调队列或优先队列实现。

 

 

 

题目2是一道经典的剪枝的题目。剪枝这种东西灵活性很强,具体题目都不一样,大概经常用的有1:对原始数据做排序、打乱等预处理。2:当前解和最优解比较。3:分析剩余数据的特点。4:估算步长下界. 5:记忆化,即记录搜索中一些会重复出现的状态,减少重复搜索。(the game就可用记忆化+步数+不拐弯方向优先剪枝AC)f[i][j][k]表式有方向k走到i,j的最小线段数。

 

 

         题目3是8数码问题,由于9位数字规模的大小,因此还能进行判重操作,所以能用的算法比较多。

1:判重。

由于棋盘转化成数字是9位,因此肯定要hash判重,否则复杂度太高。对于这种全排列问题。有一种很强的没有冲突的hash方式叫“康托展开”的东西。我们观察全排列,会发现对于每一个排列,都会有个独一无二的东西,那就是逆序数序列,例如:

数列  8 7 6 5 4 2 3 0 1

 逆序数 0 1 2 3 45 5 7 7

然后还有一个东西叫“变进制数”

我们经常使用的数的进制为“常数进制”,即始终逢p进1。例如,p进制数K可表示为

    K = a0*p^0 + a1*p^1 +a2*p^2 + ... + an*p^n (其中0 <= ai <= p-1),

若P = 10就是10进制.它可以表示任何一个自然数。

变进制数是每个位置i,逢pi进一。

有这样一中变进制的方法:第1位逢2进1,第2位逢3进1,……,第n位逢n+1进1

它的表示形式为  K = a1*1! + a2*2! +a3*3! + ... + an*n! (其中0 <= ai <= i),
也可以扩展为如下形式(因为按定义a0始终为0),以与p进制表示相对应

K= a0*0! + a1*1! + a2*2! + a3*3! + ... + an*n! (其中0 <= ai <= i)。

可以证明这种变进制在进位方面是正确的。而这个方法下,n个数的全排列的最大值只有

(N + 1)!-1

因此。我们将棋牌的逆序数列和这个变进制结合起来,就可以用O(1)的速度为棋牌判重。

关于变进制的详细内容:http://bbs.chinaunix.net/viewthread.php?tid=1283459

 

【IDS】(迭代加深)

上边我们讲到了如何高效判重,下一步就是搜索了。对于棋牌这种问题,由于搜索方向的不同,很容易错过解而陷入很深的地方,因此我们使用IDS算法,迭代加深搜索,这样才可以。

『用这个方法暴力搜索可将the game第3,4个搜过』

 

【双向广搜】

双向广搜是为了减少广搜的内存,增加速度。从末状态和初状态同时出发进行扩展节点,并判断是否有交集,发生了交集就表示找到了解。每次不是机械的前面走一步,后面走一步,而是尽量找节点数少的那一方去拓展。判断交集用hash就行。

广搜的效果是一个大三角形,而双向广搜的效果是大三角形中的一个菱形。

 

【A*】(优先队列广搜)

之前的算法都是没有去管当前状态有什么特点就去盲目的扩展,最多就是在转移的时候做一些优化的手脚。A*算法是启发式搜索,我们去分析当前状态,为状态打分。这样搜索就变的更加理性了。有三个参数:H (x):当前状态x的估价函数,表示状态x到目标状态距离的一个下界。假设x到目标的真实最短距离是dp。则要求H(x) <= dp; G(x)从起始状态到当前状态的距离。F(x) = G(x ) + H(x) 状态x的启发函数,相当于一个打分。

当我们要扩展节点时,启发函数值越小的,相对来说对我们越有利,因此在广搜中加入启发函数,每次我们都给每个状态计算出启发函数值,然后从队列中找函数值最小的节点进行扩展。为了效率,这里要使优先队列。而且某些点的F是可能变化的(虽然h值不变,但是G值有可能变)因此A*算法不仅要求储存所有状态,还要修改,所以这里对hash或者数据结构要求比较高。因此的空间复杂的很高。

我们只是要求估计函数小于真实值,因此估价函数的范围还是很广的,如果是0那就是裸的广搜了,因此估价函数的好坏直接影响搜索的效率。

在数码问题中使用的估价函数是曼哈顿距离,即棋盘上每个数离他目标位置的距离之和。

更强版本的是曼哈顿距离值之和×4/3. 给每个数加一个权值算出的估价函数更接近真实值。我还不会。。。。。。。谁知道希望能回帖。

 

 

 

题目3

【IDA*】(启发式迭代加深搜索)

由于A*算法对空间复杂度极高,因此很多问题用不了A*。题目三是15数码问题。16个数字的棋局,用变进制的方法也没办法快速判重,因此双向广搜和A*算法是个用不了的,只能试试IDS了。但是IDS的效率又很低,只是盲目的搜索,对与15数码16!种状态的搜索,肯定不行。于是一个叫IDA*的东西出现了,它中和A*和IDS的两个优点,利用A*估计出来的最小步数进行剪枝,也就是说如果当前状态的最小步数大于我们限制的最大步数,则不在从这个节点继续搜索。利用IDS不用判重的优点,每次只管搜下一个便可。因此IDA *算法实现其来都没有上面的算法麻烦,因为他什么数据结构之类的东西都不要,但是IDA *对启发函数的要求较高。

 

 

Int dfs(int deep){//深度限制

         Top= 0;

         Stack[top++]  = start_state;  //初始状态入栈

         While(top){

                   temp= stack[--top] ;   //取出栈顶元素

                   If(Is_goal(temp) ) return temp;  //如果找到了则返回

If(H(temp) + G(temp) > deep) continue;  //如果超出深度限制,则不去扩展。

                   For(i= 枚举由temp产生的状态){

                            If(i合法) strack[top ++] = i;

                   }

    }

}

 

 

 

 

由于对搜索不熟,因此漏洞百出,欢迎批评指正。

由于长期聊QQ,所以错字较多,欢迎批评指正。

 

原创粉丝点击