深度优先搜索

深度优先搜索

        搜索算法是全国信息学竞赛必考的算法之一,也是最常见的算法之一。很多复杂的算法都在搜索算法的基础上发展起来的。在竞赛中,有一些问题一般难以找到公式或规律,特别是信息学竞赛试题难度较大,根本就没有公式可循,这时一般采用搜索法利用计算机高速运行的特点来编程求解,如何分析此题是否需要用搜索算法来解,这就显得关键。我们知道,所谓搜索,就是在数据集合中寻找满足某种条件的数据对象。
         搜索算法是利用计算机的高性能来有目的的穷举一个问题的部分或所有的可能情况,从而求出问题的解的一种方法。
         搜索算法被称为“通用解题法”,在算法和人工智能中占有重要地位。很多算法都是搜索算法的扩展,如图论中的某些算法、动态规划、贪心等,只是他们找到了某些规律来进行变相剪枝而已。 我们常用的搜索算法有深度优先搜索depth first search(dfs)、广度优先搜索breadth first search(bfs)和A*算法。其中A*算法属于启发式搜索。


深度优先搜索的概念

        按照深度优先的顺序遍历状态空间,通常用递归或者栈来实现。说白了就是往深里搜,当某个节点的所有子节点都搜索过了就返回父节点继续进行。就像走迷宫,你没有办法用分身术来站在每个走过的位置。不撞南山不回头

    假设按照以下的顺序来搜索:
         (1) v0 -> v1 ->v4,此时已经到尽头,仍然到不了v6,于是原路返回到v1去搜索其它路径;
         (2) 返回到v1后继续搜索v2,于是搜索路径是v0 -> v1 -> v2 -> v6,找到目标节点,返回有解。
         这样,搜索2步就到达了。这就是dfs的过程。搜索的过程,实际上就是建立一棵树的过程。如果图1全部搜索完,构建的树如下:
    
    图1的dfs伪代码如下:

/*dfs核心伪代码 */

void dfs(k,…){    //k代表目前dfs的深度
	if 找到解 or 走不下去了  { …. return; }
	for i=1 to n do     //解空间
	……
	枚举下一种情况,dfs(k+1,…)
}



【程序1】特殊的质数肋骨

         农民约翰母牛总是产生最好的肋骨。
         你能通过农民约翰和美国农业部标记在每根肋骨上的数字认出它们。
         农民约翰确定他卖给买方的是真正的质数肋骨,是因为从右边开始切下肋骨,每次还剩下的肋骨上的数字都组成一个质数,举例来说:
         7 3 3 1
         全部肋骨上的数字 7331是质数;三根肋骨 733是质数;二根肋骨 73 是质数;当然,最后一根肋骨 7 也是质数。
         7331 被叫做长度 4 的特殊质数。
         写一个程序对给定的肋骨的数目 N (1<=N<=8),求出所有的特殊质数。
         数字1不被看作一个质数。
输入格式
         单独的一行包含N。
输出格式
         按顺序输出长度为 N 的特殊质数,每行一个。
样例输入
         4
样例输出
         2333
         2339
         2393
         2399
         2939
         3119
         3137
         3733
         3739
         3793
         3797
         5939
         7193
         7331
         7333
         7393

问题分析:
         样例中n=4,即四位数中具有这种性质的数。如果直接搜索枚举,n稍稍大一点,就会超时。那么我们可以直接构造,第一位数字只能是2,3,5,7中的1个,因为其它的都不是素数。我们进行适当的剪枝,缩小解空间,除第一位外,其它位的数字不可能是偶数,也不可能是5,只能是1,3,7,9。构造的搜索树如下(n=3为例):

         深度优先搜索的过程就是建立一棵搜索树的过程,上图中的根节点0是不存在的,添加以方便表示。
子程序dfs(k)框架如下:

void dfs(x, k){  //搜索第k位上可能的数字,构造前k位数x,此处k为目前dfs的深度
	if  x不是素数 then 返回;
	if  x=n then 输出;
	dfs(x*10+1,k+1);
	dfs(x*10+3,k+1);
	dfs(x*10+7,k+1);
	dfs(x*10+9,k+1);
}
参考代码:
#include
using namespace std;
int n;
bool prime(int m) {
	for (int i=2;i*i<=m;++i) 
		if (m%i==0) return false;
	return true;
}  
void dfs(int x,int k) {
	if (!prime(x))  return;
	if (k==n) cout << x << endl;
	dfs(x*10+1,k+1);
	dfs(x*10+3,k+1);
	dfs(x*10+7,k+1);
	dfs(x*10+9,k+1);
}
int main() {
	cin >> n;
	dfs(2,1);
	dfs(3,1);
	dfs(5,1);
	dfs(7,1);
}



【程序2】全排列 

         输入n(1<=n<=9),由1~n组成的所有不重复的数字序列,每行一个序列,相邻数字无需空格隔开。
样例输入
         3
样例输出
         123
         132
         213
         231
         312
         321
        

问题分析:
         对于全排列,搜索的深度就是它的位数。以n=3为例,我们很容易想到,它的每一位数字都是由1-3组成,依次为111,112,113,121,122,123,……,搜索树如下:

         搜索的过程中会出现相等的情况,如111,这时我们要判重,可以使用一个boolean型数组vis来实现,初识时置vis为true,如果数字i用过,置vis[i]=false即可。解空间为1到n,dfs程序框架如下:

void dfs(k){  //搜索第k位上可能的数字,构造前k位数x,此处k为目前dfs的深度
	if k>n then 输出;
	for i=1 to n do {  //第k位数字的所有范围1-n
	    if vis[i]=true then {   //如果数字i没有用过
		a[k]=i;     //第k位数字为i
		vis[i]=false;     //标记i已经使用过
		dfs(k+1);      //搜索第k+1位可用的数字
		vis[i]=true;      //把第k位数字为i,占用的i释放掉
	    }
	}
}
参考代码:
#include
using namespace std;
int n,a[11];
bool vis[11];
void out(){
	for(int i=1;i<=n;i++) cout << a[i];
	cout << endl;
}  
void dfs(int k) {
	if (k>n) {out(); return;} 
	for(int i=1;i<=n;i++){
		if(vis[i]==true) {
			a[k]=i;
			vis[i]=false;
			dfs(k+1);
			vis[i]=true;
		}
	}
}
int main() {
	cin >> n;
	memset(vis,true,sizeof(vis));
	dfs(1);
}



【程序3】数的计数

         要求找出具有下列性质的数的个数(包含输入的自然数n):
         先输入一个自然数n,然后对此自然数按照如下方法进行处理:
           ① 不作任何处理;
           ② 在它的左边加上一个自然数,但该自然数不能超过原数的一半;
           ③ 加上数后,继续按此规则进行处理,直到不能再加自然数为止。
样例输入
         6
样例输出
         6
注释
         样例中满足条件的数为:6 16 26 126 36 136

参考代码:
#include
using namespace std;
int n,ans=0;
void dfs(int k) {
	ans++;
	for(int i=1;i<=k/2;i++) dfs(i);
}
int main() {
	cin >> n;
	dfs(n);
	cout << ans << endl;
}



【程序4】借书问题(laoj1352)

        学校放暑假时,信息学辅导教师有n本书要分给参加培训的n个学生。如:A,B,C,D,E共5本书要分给参加培训的张、刘、王、李、孙5位学生,每人只能选1本。教师事先让每个人将自己喜爱的书填写在如下的表中,然后根据他们填写的表来分配书本,希望设计一个程序帮助教师求出可能的分配方案,使每个学生都满意。

A B C D E
    Y Y  
Y Y      
  Y Y    
      Y  
Y       Y


输入格式
    第一行一个数n(学生的个数,书的数量)
    以下共n行,每行n个0或1(由空格隔开),第I行数据表示第i个同学对所有书的喜爱情况。0表示不喜欢该书,1表示喜欢该书。
输出格式
    依次输出每个学生分到的书号。
    如果有多种分配方案,按顺序全部输出,一行一个方案。
输入
5
0 0 1 1 0
1 1 0 0 0
0 1 1 0 0
0 0 0 1 0
0 1 0 0 1
输出
3 1 2 4 5
【分析】 这个问题中喜爱的书是随机的,没有什么规律,所以用穷举法比较合适。
    为编程方便,用1、2、3、4、5分别表示这五本书。这五本书的一种全排列就是五本书的一种分法。例如54321表示第5本书(即E)分给张,第4本书(即D分给王,)……第1本书(即A分给钱)。 “喜爱书表”可以用二维数组a[i][j]来表示,1表示第i个人喜爱第j本书,0表示第i个不喜爱第j本书。

算法框架如下:
procedure dfs(k);   //寻找第k个人喜爱的书
  if x>n then 输出
    else 
       for i=1 -> n   //枚举所有的书
         if (a[k,i]=1 && vis[i]){    //如果第k个人喜爱第i本书,且第i本书没被别人拿走
	    vis[i]=false;   //标记第i本书已本拿走
	    h[k]=i;    //存储第k人喜爱的书i
	    dfs(k+1);   //寻找k+1个人喜爱的书
	    vis[i]=true;   //回溯,第i本书放回去
         }
【C++】
#include
using namespace std;
int n,a[35][35],h[35];
bool vis[35];
void out1() {
	for (int i=1;in) {out1;return;}
	for (int i=1;i<=n;++i) {
		if (a[k][i]==1 && vis[i]==false) {
		    h[k]=i;
		    vis[i]=true;
		    dfs(k+1);  
		    vis[i]=false;  
		}
	}
}
int main() {
	memset(vis,false,sizeof(vis));
	cin >> n;
	for (int i=1;i<=n;++i) 
	    for (int j=1;j<=n;++j) cin >> a[i][j]; 
	dfs(1);   
	return 0;     
}


【程序5】八皇后问题

        在8×8格的国际象棋上摆放8个皇后,使其不能互相攻击,即任意两个皇后都不能处于同一行、同一列或同一斜线上,,请问有多少种摆法,并将每种摆法打印出来。如下图即是摆法的一种。


【分析】
    对于棋盘坐标我们一般会想到用二维数组来表示,如定义h[1][2]表示第1行第2列摆放一个皇后。
    但是实际上我们可以用一个一维数组就可以解决这个问题。由于每行只能摆1个皇后,h[1]=7表示第1行第7列摆放一个皇后,这种方法同时又无须再判断两个皇后是否在同一的行的问题。那么上图所示的摆法为:
    15863724
    我们定义1个dfs(k)函数来寻找第k个皇后(第k行放置的那个皇后)的摆放位置。

算法如下:
void dfs(k);{  //寻找第k个皇后摆放的位置
  for i=1 -> 8  //对于可能摆放的位置进行枚举(共8列)
    if 第i列可以摆放 
       h[k]=i;
       对(k,i)坐标所在的列,两条对角线做标记,表示不能在这些位置摆放;
       if (k==8)  一种摆法结束,打印输出这种摆法;
             else dfs(k+1);  //寻找第k+1个皇后摆放的位置
       释放对(k,i)坐标所在的列,两条对角线所做的标记;
   }

    显然问题的关键在于如何判定某个皇后所在的行、列、斜线上是否有别的皇后;可以从矩阵的特点上找到规律,如果在同一行,则行号相同;如果在同一列上,则列号相同;如果同在/ 斜线上的行列值之和相同;如果同在\ 斜线上的行列值之差相同;如下图所示:

    当前皇后的位置为[i,j]=[4,3],那么\对角线方向不能放置,相应位置为[5,4]、[6,5]、[7,6]和[8,7],即j-i都等于-1。
    考虑每行有且仅有一个皇后,设一维数组h[1..8]表示皇后的放置:第i行皇后放在第j列,用h[i]=j来表示,即下标是行数,内容是列数。例如:h[3]=5就表示第3个皇后在第3行第5列上。
    判断皇后是否安全,即检查同一列、同一对角线是否已有皇后,建立标志数组b[1..8]控制同一列只能有一个皇后,若两皇后在同一对角线上,则其行列坐标之和或行列坐标之差相等,故亦可建立标志数组b[0..16]、c[-7..7] (由于C++语言的最小下标为0,可以把所有的下标+8) 控制同一对角线上只能有一个皇后。

C++ code:
#include
using namespace std;
bool a[20],b[20],c[20];
int h[20];
int ans;
void out1(){
	ans++;
	for(int i=1;i<=8;i++) cout << h[i];
	cout << endl;
}
void dfs(int k) {
	for (int i=1;i<=8;++i)
	    if (a[i]==false && b[k+i]==false && c[k-i+8]==false) { 
			a[i]=true, b[k+i]=true, c[k-i+8]=true; 
			h[k]=i;
			if (k==8) out1();  else  dfs(k+1);
			a[i]=false, b[k+i]=false, c[k-i+8]=false;		    
	    }   
}
int main() {
	dfs(1);
	cout << ans << endl;
}


【例题6】简单跳马问题(laoj1353)

        题目查看及提交链接:http://www.layz.net/laoj/Problem_Show.asp?id=1353
数据结构:
用数组a存放x方向能成行到达的点坐标;用数组b存放y方向能成行到达的点坐标;
马跳到下一步的增量方向分别用常量数组dx和dy表示。

算法描述:
① 以(x0, y0)为起点,按顺序用四个方向试探,找到下一个可行的点(x1, y1);
② 判断找到的点是否合理 (不出界),若合理,就存入a数组中;如果到达目的就打印,否则重复第①步骤;
③ 如果不合理,则换一个方向试探,如果四个方向都已试过,就退回一步(回溯),用未试过的方向继续试探。重复步骤①;
④ 如果已退回到原点,则程序结束。

子程序设计:
procedure dfs(k);
    for i=1 -> 4 {
	x1 = a[k-1].x + dx[i];
        y1 = a[k-1].y + dy[i];
	if not((x1>x) or (y1<0) or (y1>y)) {
                a[k].x = x1;
                a[k].y = y1;
	        if (y1==y) 并且 (x1==x) 输出;
                dfs(k+1);
	}
}
C++ code:
#include
#include 
using namespace std;
struct rec{
	int x,y;
}a[25];
int x,y,sum,step=1;
const int dx[4]={1,2,2,1},dy[4]={2,1,-1,-2};
void out() {
    ++sum;
    cout  << "<" << sum << ">" << "(" << a[1].x << "," << a[1].y << ")";
    for (int i=2;i<=step;++i) {
        cout  << "-" << "(" << a[i].x << "," << a[i].y << ")";
    }
    cout << endl;
}
void dfs(int x1,int y1) {
    int ux,uy;
	if (x1==x && y1==y) {
		out();
		return;
	}
    for(int i=0;i<4;i++){
		ux=x1+dx[i];
		uy=y1+dy[i];
		if(ux>=0 && uy>=0 && ux<=x && uy<=y){
			step++;
			a[step].x=ux;
			a[step].y=uy;
			dfs(ux,uy);
			step--;
		}
    }
}
int main() {
    cin >> x >> y;
    dfs(0,0);
    cout << sum << endl;
}


【例题7】自然数的拆分

输入自然数n(n<100),输出所有和的形式。不能重复。
如:4=1+1+2;4=1+2+1;4=2+1+1 属于一种分解形式。

样例输入:
7
输出:
7=1+6
7=1+1+5
7=1+1+1+4
7=1+1+1+1+3
7=1+1+1+1+1+2
7=1+1+1+1+1+1+1
7=1+1+1+2+2
7=1+1+2+3
7=1+2+4
7=1+2+2+2
7=1+3+3
7=2+5
7=2+2+3
7=3+4
子程序设计:
procedure dfs(x,k){ //拆分x,当前拆分到第k项
    t:=x div 2;
    for i=1 -> t do
      if (a[k-1]<=i) {
          a[k]=i;
          输出n,'=';
          for j=1 -> k { 输出a[j],'+';}
          输出x-i;
          dfs(x-i,k+1);
      }
}
也可以只用一个参数。
C++ code:
#include
using namespace std;
int n,now=1,a[1001];
void dfs(int k){
	int i;
	if(k!=n){
		cout << n << '=';
		for(i=1;i> n;
	a[0]=1;
	dfs(n);
}


【例题8】 反素数

        如果正整数n的约数个数超过比n小的任何数的约数个数,则n称为反素数。输入一个n(n<=2*10^5),求不超过n的最大反素数数m。
输入格式:
         一个整数n
输出格式:
         不超过n的最大反素数数m
样例输入:
         5
样例输出:
         4

【分析】
         问不超过N的最大的x,使得任何比x小的数的约数个数都比x的约数个数少。
         其实说到底就是求[1, n]中约数个数最多的数,如果有多个这样的数,取最小的(因为题目要求任何比x小的数的约数个数都必须小于x的约数个数,不能取等)。
         另外做本题之前还要掌握约数个数公式:
         若把x表示成多个质因数的乘积,x=p1a1 * p2a2 * ... * pnan p1,p2...pn是质数 那么x的约数个数=(a1+1)*(a2+1)*...(an+1)。
         例如100分解质因数为100=2*2*5*5=22*52,所以100的约数个数=(2+1)*(2+1)=9
         我们可以直接DFS暴搜找答案,枚举每一个质因数的幂,然后根据已经乘积得出的数的约数个数,不断更新答案。另外此题有个小技巧需要注意:枚举的质因数只要是从小到大前10个质数就可以了,因为从小到大前十个以上的质数的乘积已经超出了n的最大范围。
         而且枚举第i个质因数的幂时,第i个质因数的幂大小不会超过第i-1个质因数的幂。(因为差不多大小的数字,大的质因数的指数比小的质因数的指数大的数字绝对没有小的质因数的指数比大的质因数指数大的数字的约数个数多,根据贪心思想,大的质因数会占用更大的积,不如小的质因数来得划算)

C++ code:
#include
#include
using namespace std;
const int p[13]={0,2,3,5,7,11,13,17,19,23,29,31,37};
int n,ans=0,num=0;
void dfs(int now,int cj,int cs,int last,int res) {
//当前数,当前乘积,当前数出现次数,上一个数出现次数,约数个数
	if(ans==res*(cs+1)&&(cjans){
		ans=res*(cs+1);
		num=cj;
	}
	if(cs+1<=last && cj*p[now]<=n) dfs(now,cj*p[now],cs+1,last,res);
	for(int i=now+1;i<=12;i++) 
		if(cj*p[i]<=n) dfs(i,cj*p[i],1,cs,res*(cs+1));
}
int main() {
	cin >> n;
	dfs(1,1,0,100,1);
	cout << num << endl;
} 

你可能感兴趣的:(搜索算法)