搜索与回溯(深搜和广搜概念、定义、算法框架、及经典例题详讲加代码)

搜索与回溯

  • 搜索和回溯
    • 一、 概念
      • (一)、深度搜索
        • 1、基本思想
        • 2、深度优先搜索的算法框架
        • 3、典型例题
          • (1)、八皇后原始版本
          • (2)、八皇后
          • (3)、字符序列
          • (4)、自然数的拆分
          • (5)、 自然数的拆分2
      • (二)、回溯
        • 1、概念
        • 2、回溯的算法框架
        • 3、深度优先搜索与回溯法的关系
        • 4、典型例题
          • (1)、数的拆分
          • (2)、选排列的生成
          • (3)、N 皇后问题
      • (二)、广度搜索
        • 1、基本思想
        • 2、广度优先搜索的算法框架
        • 3、典型例题
          • (1)、瓷砖
          • (2)、最大黑区域
          • (3)、飞越原野
          • (4)、关系网络
    • 二、 超级重点!!

搜索和回溯

一、 概念

搜索是计算机程序设计中一种最基本、最常用的算法。搜索算法是直接基于计算机高速运算的
特点而使用的计算机求解方法。
它是从问题的初始状态出发,根据其中的约束条件,按照一定的策略,有序推进,不断深入,
对于达到的所有目标状态(解空间),一一验证,找到符合条件的解(可行解),或者找出所
有可行解中的最优解。

(一)、深度搜索

1、基本思想

深度优先搜索是将当前状态按照一定的规则顺序,先拓展一步得到一个新状态,
再对这个新状态递归拓展下去。如果无法拓展,则退回一步到上一个状态,再按
照原先设定的规则顺序重新寻找一个状态拓展。如此搜索,直至找到目标状态,
或者遍历完所有状态。所以,深度优先搜索也是一种“盲目”搜索。
对于图9.10-1所示的一个“无向图”,从顶点V0开始进行深度优先搜索,
得到的一个序列为:V0,V1,V2,V6,V5,V3,V4
搜索与回溯(深搜和广搜概念、定义、算法框架、及经典例题详讲加代码)_第1张图片

2、深度优先搜索的算法框架

深度优先搜索(Depth First Search,DFS),简称深搜,
其状态“退回一步”的顺序符合“后进先出”的特点,
所以采用“栈”存储状态。深搜适用于要求所有解方案的题目。
深搜可以采用直接递归的方法实现,其算法框架如下:

void dfs(int dep, 参数表 );{
	自定义参数 ;
	if( 当前是目标状态 ){
		输出解或者作计数、评价处理 ;
	}else
		for(i = 1; i <= 状态的拓展可能数 ; i++)
			if( 第 i 种状态拓展可行 ){
				维护自定义参数 ;
				dfs(dep+1, 参数表 );
			}
}

3、典型例题

(1)、八皇后原始版本

题目描述

在国际象棋棋盘上(8*8)放置八个皇后,使得任意两个皇后之间不能在同一行,同一列,也不能位于同于对角线上。问共有多少种不同的方法,并且按字典序从小到大指出各种不同的放法。

输入格式

输出格式

1 5 8 6 3 7 2 4
1 6 8 3 7 4 2 5

总方法数

样例

样例输入

样例输出
1 5 8 6 3 7 2 4
1 6 8 3 7 4 2 5

92

解题思想
用深搜模拟当前棋盘上皇后的位置,判断当前位置可不可以放下皇后
若可以,储存下皇后的纵坐标,当8个皇后放完之后开始下一轮模拟,
直到找到最后一种解

代码实现

#include
#include
int n,i,a[101],sum,w[101][10],j;
bool judge(int N)
{
	for(int j=1;jn)
	{
		save();
		return ;
	}
	for(int j=1;j<=n;j++)
	{
		a[i]=j;
		if(judge(i))
		{
			solve(i+1);
		}
	}
}
int main()
{
	n=8;
	solve(1);
	for(i=1;i<=sum;i++)
	{
		for(j=1;j<=8;j++)
		{
			printf("%d ",w[i][j]);
		}
		printf("\n");
	}
	printf("%d",sum);
}
(2)、八皇后

题目描述

会下国际象棋的人都很清楚:皇后可以在横、竖、斜线上不限步数地吃掉其他棋子。如何将8个皇后放在棋盘上(有 8 * 8 个方格),使它们谁也不能被吃掉!这就是著名的八皇后问题。对于某个满足要求的 8 皇后的摆放方法,定义一个皇后串 a 与之对应,即 a=b1b2…b8,其中 bi 为相应摆法中第 i 行皇后所处的列数。已经知道 8 皇后问题一共有 92 组解(即92个不同的皇后串)。 给出一个数 b,要求输出第 b 个串。串的比较是这样的:皇后串 x 置于皇后串 y 之前,当且仅当将 x 视为整数时比 y 小。

输入格式

第 1 行是测试数据的组数 n,后面跟着 n 行输入。每组测试数据占 1 行,包括一个正整数 b(1 <= b <= 92)。

输出格式

输出有 n 行,每行输出对应一个输入。输出应是一个正整数,是对应于 b 的皇后串。

样例

输入样例
2
1
92
输出样例
15863724
84136275

解题思想
利用上一题将所有皇后模拟出来的数组
直接打印对应组号就可以A掉

代码实现

#include
#include
int n,i,a[101],sum,w[101][10],j,num,t;
bool judge(int N)
{
	for(int j=1;jn)
	{
		save();
		return ;
	}
	for(int j=1;j<=n;j++)
	{
		a[i]=j;
		if(judge(i))
		{
			solve(i+1);
		}
	}
}
int main()
{
	n=8;
	solve(1);
	scanf("%d",&num);
	while(num--)
	{
		scanf("%d",&t);
		for(i=1;i<=8;i++)
			printf("%d",w[t][i]);
		printf("\n");
	}
}
(3)、字符序列

题目描述

从三个元素的集合[A,B,C]中选取元素生成一个 N 个字符组成的序列,使得没有两个相邻的子序列(子序列长度=2)相同。例:N = 5 时 ABCBA 是合格的,而序列 ABCBC 与 ABABC 是不合格的,因为其中子序列 BC,AB 是相同的。
对于由键盘输入的 N(1<=N<=12),求出满足条件的 N 个字符的所有序列和其总数。

输入格式

一个N(1<=N<=12)

输出格式

求出满足条件的 N 个字符的所有序列和其总数。

样例

输入样例
4
输出样例
72

解题思想
依旧靠模拟现在数组中的元素并且判断相邻两个
元素构成的子序列是否是满足条件的解,是就进行
计数并搜索下一层

代码实现

#include
#define ll long long
ll a[1000005],n,sum;
bool check(int p)
{
	return p>3&&a[p-3]*10+a[p-2]==a[p-1]*10+a[p];
}
void dfs(int p)
{
	ll i;//要每次都重新定义i的变量,不然在调用的时候会改变外层搜索的i变量 
	for(i=1;i<=3;i++)
	{
		a[p]=i;
		if(check(p))//只用判断相邻的字符串,之前的是因为判断了所有的情况 
			continue;
		else
		{
			if(p==n)
				sum++;
			else
				dfs(p+1);
		}
	}
}
int main()
{
	scanf("%lld",&n);
	dfs(1);
	printf("%lld",sum);
}
(4)、自然数的拆分

题目描述

任何一个大于1的自然数n,总可以拆分成若干个小于n的自然数之和。
当n=7共14种拆分方法:
7=1+1+1+1+1+1+1
7=1+1+1+1+1+2
7=1+1+1+1+3
7=1+1+1+2+2
7=1+1+1+4
7=1+1+2+3
7=1+1+5
7=1+2+2+2
7=1+2+4
7=1+3+3
7=1+6
7=2+2+3
7=2+5
7=3+4

输入格式

输入n。

输出格式

按字典序输出具体的方案。

样例

输入样例
7
输出样例
1+1+1+1+1+1+1
1+1+1+1+1+2
1+1+1+1+3
1+1+1+2+2
1+1+1+4
1+1+2+3
1+1+5
1+2+2+2
1+2+4
1+3+3
1+6
2+2+3
2+5
3+4

解题思想
在DFS函数中传入当前递归层数,记录当前分解的值
并搜索下一层

代码实现

#include
int a[1005]={1},n;
int print(int t)
{
	int i;
	for(i=1;i
(5)、 自然数的拆分2

题目描述

输入自然数N,然后将其拆分成由若干数相加的形式,参与加法运算的数可以重复。

输入格式

第1行:1个整数n(n≤30)

输出格式

所有拆分方案,顺序见样例。

样例

输入样例
4
输出样例
1+3
1+1+2
1+1+1+1
2+2

解题思想
在DFS函数中传入当前递归层数,记录当前分解的值
并搜索下一层,不过因为a[i]<=a[i+1],所以循环只调用
n的一半

代码实现

#include
int a[100005]={1},n;
void print(int d)
{
	if(d>=2)
	{
		int i;
		for(i=1;i

(二)、回溯

1、概念

“回溯法”也称“试探法”。它是从问题的某一状态出发,不断“试探”着往前走一步,当一条路走到“尽头”,不能再前进(拓展出新状态)的时候,再倒回一步或者若干步,从另一种可能的状态出发,继续搜索,直到所有的“路径(状态)”都一一试探过。这种不断前进、不断回溯,寻找解的方法,称为“回溯法”。
深度优先搜索求解的时候,当找到目标结点之后,还要回头寻找初始结点到目标结点的解路径。而回溯法则不同,找到目标结点之后,搜索路径就是一条从初始结点到目标结点的解路径。回溯法实际上是状态空间搜索中,深度优先搜索的一种改进,是更实用的一种搜索求解方法。

2、回溯的算法框架

void search(int dep){
	自定义参数 ;
	if( 当前是目标状态 ){
		输出解或者作计数和评价处理 ;
	}else
	     for(i = 1; i <= 状态的拓展可能数 ; i++)
		if( 第 i 种状态拓展可行 ){
			保存现场 ( 断点 ), 维护自定义参数 ;
			search(dep+1);
			恢复现场 , 回溯到上一个断点继续执行 ;
		}
}

或者

void search(dep:integer){
	自定义参数 ;
	for(i = 1;i <= 状态的拓展可能数 ;i++)
		if( 第 i 种状态拓展可行 ){
			保存现场 ( 断点 ), 维护自定义参数 ;
			if( 当前是目标状态 ){
				输出解或者作计数和评价处理 ;
			}else search(dep+1);
			恢复现场 , 回溯到上一个断点继续执行 ;
		}
}

3、深度优先搜索与回溯法的关系

1)深度优先搜索包含回溯,或者说回溯法是深度优先搜索的一种。
2)深度优先搜索需要控制如何实现状态之间的转移(拓展),回溯法就是深度优先搜索的一种控制策略。
3)回溯的过程中,并不需要记录整棵“搜索树”,而只需记录从初始状态到当前状态的一条搜索路径,是“线性链状”的,其最大优点是占用空间少。
4)深度优先搜索可以采用递归(系统栈)和非递归(手工栈)两种方法实现。递归搜索是系统栈实现一部分的回溯(如果需要记录一些特殊信息或较多的信息,还需要另外手工记录),而非递归是自己用手工栈模拟回溯的过程,所以实现起来略为复杂一点。

4、典型例题

(1)、数的拆分

题目描述

输入 n,输出将 n 拆分成若干正整数和的所有方案,即 n=S 1 +S 2 +…+S k 的形式,且 S 1 ≤S 2 ≤…≤S k ,n≤20,请按照字典序输出。

输入格式

一行一个整数 n。

输出格式

所有拆分方案,具体格式参见输出样例。

样例

样例输入
4
样例输出
1+1+1+1
1+1+2
1+3
2+2
4
total=5

解题思想

直接按当前状态一直分解下去,直到分解完全
之后再回溯上一步与深度优先搜索的方法类似,从 dep=1 开始,每次 dfs(dep)先拆出一个数 i,记录下来,并且修改余数 rest=rest-i,然后 dfs(dep+1)去递归拆分下去,只是每次递归结束后需要做回溯操作,即 rest=rest+i。

代码实现(核心)

void dfs(int dep){
	if(rest == 0) pr(dep-1);
	else for(int i = s[dep-1]; i <= rest; i++){
		s[dep] = i;
		rest -= i;
		dfs(dep+1);
		rest += i;
	}
}

(2)、选排列的生成

题目描述

设有 n 个整数的集合 {1,2,3,…,n},从中取出任意 r 个数进行排列(0

输入格式

一行两个整数 n 和 r,之间用一个空格隔开。

输出格式

所有排列方案,具体格式参见输出样例。

样例

输入样例
4 2
输出样例
1 2
1 3
1 4
2 1
2 3
2 4
3 1
3 2
3 4
4 1
4 2
4 3
total=12

解题思想
【问题分析】

复述一下题意就是:r 个位置,每个位置放一个 1~n 之间的整数,要求每个数不能重复,输出所有的排列方案。
可以从第 1 个位置开始,每个位置放置数 k,k 从 1 开始不断尝试直到 n,如果能放(没用过)就放,放好后再尝试下一个位置,如果第 r 个位置也放好了,则说明一种方案已经形成,则输出一种方案、计数,继续寻找下一种方案(回溯)。如果不能放置(该数已用过)就换放下一个数,如果 k大于 n 了,表示不能继续放下去了,则回溯。一个数有没有用过,只要开一个哈希表判断即可

代码实现(核心)

略,自己写
(3)、N 皇后问题

题目描述

要在 N×N(N≤8)的国际象棋棋盘中放 N 个皇后,使得任意两个皇后都不能互相吃(提示:皇后能吃同一行、同一列、同一对角线的其他皇后)。
请问有多少种方案,并按字典序输出所有的方案。每种方案输出每一行皇后的纵坐标(场宽为 5),如果无解,则输出“no solute!”。

输入格式

一个N(1<=N<=12)

输出格式

每行对应一种方案,具体格式参见输出样例。

样例

输入样例
4
输出样例
2 4 1 3
3 1 4 2

解题思想

代码实现

#include
#include
int n,i,a[101],sum,w[101][10],j;
bool judge(int N)
{
	for(int j=1;jn)
	{
		save();
		return ;
	}
	for(int j=1;j<=n;j++)
	{
		a[i]=j;
		if(judge(i))
		{
			solve(i+1);
		}
	}
}
int main()
{
	scanf("%d",&n);
	solve(1);
	for(i=1;i<=sum;i++)
	{
		for(j=1;j<=n;j++)
		{
			printf("%d ",w[i][j]);
		}
		printf("\n");
	}
}

(二)、广度搜索

1、基本思想

宽度优先搜索(Breadth First Search,BFS),简称宽搜,又称广度优先搜索。它是从初始结点开始,应用产生式规则和控制策略生成第一层结点,同时检查目标结点是否在这些生成的结点中。若没有,再用产生式规则将所有第一层结点逐一拓展,得到第二层结点,并逐一检查第二层结点是否包含目标结点。若没有,再用产生式规则拓展第二层结点。如此依次拓展,检查下去,直至发现目标结点为止。如果拓展完所有结点,都没有发现目标结点,则问题无解。搜索与回溯(深搜和广搜概念、定义、算法框架、及经典例题详讲加代码)_第2张图片
对于以上“无向图”,从顶点 V0 开始进行宽度优先搜索,得到的一个序列为 V0,V1,V2,V3,V4,V6,V5。
宽度优先搜索是一种“盲目”搜索,所有结点的拓展都遵循“先进先出”的原则,所以采用“队列”来存储这些状态。宽度优先搜索的算法框架如下:

2、广度优先搜索的算法框架

void BFS{
      while (front <= rear){
      // 当队列非空时做,front 和 rear 分别表示队列的头指针和尾指针      
             if (找到目标状态)
	   做相应处理(如退出循环输出解、输出当前解、比较解的优劣);
	    else{
		拓展头结点 ;
		if( 拓展出的新结点没出现过 ){
		     rear++;
		    将新结点插到队尾 ;
		}
	    }
	front++;// 取下一个结点
      }
}

//如果只要求任意一个解,也可以写成以下的结构:
void BFS2{
	p = true;
	while (p){
		if( 头结点是目标状态 ) p = false;
		else{
			拓展头结点 ;
			if( 拓展出的新结点没出现过 ){
				rear++;
				将新结点插到队尾 ;
			}
			front++;
			if(front > rear)p = false;
		}
	}
}

3、典型例题

(1)、瓷砖

题目描述

在一个 w×h 的矩形广场上,每一块 1×1 的地面都铺设了红色或黑色的瓷砖。小林同学站在某一块黑色的瓷砖上,他可以从此处出发,移动到上、下、左、右四个相邻的且是黑色的瓷砖上。现在,他想知道,通过重复上述移动所能经过的黑色瓷砖数。

输入格式

第 1 行为 h、w,2≤w、h≤50,之间由一个空格隔开;
以下为一个 w 行 h 列的二维字符矩阵,每个字符为“.”“#”“@”,分别表示该位置为黑色的瓷砖、红色的瓷砖、小林的初始位置。

输出格式

输出一行一个整数,表示小林从初始位置出发经过的黑色瓷砖数。

样例

样例输入
11 9
. # . . . . . . . . .
. # . # # # # # # # .
. # . # . . . . . # .
. # . # . # # # . # .
. # . # . . @ # . # .
. # . # # # # # . # .
. # . . . . . . . # .
. # # # # # # # # # .
. . . . . . . . . . .
样例输出
59

解题思想
【问题分析】
读入字符数组,找到小林的初始位置“@”,并把坐标入队,作为队头元素。宽度优先搜索,检查队头元素的上、下、左、右四个位置是否是黑色瓷砖“.”,是则入队,……不断取出队头元素进行四个方向的拓展,直到队列为空。
为了避免一个位置被多次重复走到,定义一个布尔型数组 a[i,j]用来判重,位置(i,j)为黑色瓷砖设置为 true,红色的或者走过的瓷砖设置为 false。
最后,队列的尾指针即为答案。本题是搜索的一个重要应用,所谓的求“联通块”问题。

代码实现

自己加油
(2)、最大黑区域

题目描述

二值图像是由黑和白两种像素组成的矩形点阵,图像识别的一个操作是求出图像中最大黑区域的面积。请设计一个程序完成二值图像的这个操作。黑区域由黑像素组成,一个黑区域中的每像素至少与该区域中的另一像素相邻,规定一个像素仅与其上、下、左、右的像素相邻。两个不同的黑区域没有相邻的像素。一个黑区域的面积是其所包含的像素数。

输入格式

第 1 行含两个整数 n 和 m,1≤n、m≤100,分别表示二值图像的行数与列数。后面n行,每行含m个整数0或1,其中第i行表示图像的第i行的m个像素,0表示白像素,1 表示黑像素。每行中的两个数之间用一个空格分隔。

输出格式

输出一行一个数,表示相应的图像中最大黑区域的面积。

样例

输入样例
5 6
0 1 1 0 0 1
1 1 0 1 0 1
0 1 0 0 1 0
0 0 0 1 1 1
1 0 1 1 1 0
输出样例
7

解题思想
【问题分析】
如果先找到一个黑点,那么问题就和例 2 一样了。所以,本题只要从左上角开始,利用一个两重循环穷举找到一个黑点(a[i,j]=1),然后做一遍宽搜,并且记下此黑色区域(联通块)的大小,再通过打擂台记录最大值作为答案 ans。如果题目要求的是有几个黑区域,则只要每次宽搜时“计数器”加一。

代码实现

Fighting!
(3)、飞越原野

题目描述

在一片广阔的土地上有一个鸟人,他需要从这里穿过原野回到基地。这片原野上有平地(P)、有湖泊(L)。因为鸟人可以飞,所以有的时候,他可以飞越湖泊。现在,鸟人需要用最快的时间回到基地。
假设原野是一个 m×n 的矩阵,有两种地形,用 P 和 L 表示。鸟人只能停留在平地上。他目前处在(1,1)这个位置,而目的地是(m,n)。他可以向上、下、左、右四个方向移动,或者飞行。
每移动一格需要 1 个单位时间。而飞行无论飞多远,都只需要 1 个单位时间。飞行的途中不可以改变方向。也就是说,飞行也只能是上、下、左、右四个方向,并且一次飞行最终必须降落在平地上。当然,受到能量的限制,鸟人不能无限制地飞行,他总共最多可以飞行的距离为 D。

输入格式

第 1 行是 3 个整数 m、n 和 D,3 个数都不超过 100。
下面是一个 m×n 的字符矩阵,表示原野。

输出格式

输出一行一个整数表示最短时间。如果无法到达,输出“impossible”。

样例

输入样例
4 4 2
PLLP
PPLP
PPPP
PLLP
输出样例
5

(4)、关系网络

题目描述

有 n 个人,编号为 1~n。其中有一些人相互认识,现在 x 想要认识 y,可以通过他所认识的人来认识更多的人(如果 a 认识 b、b 认识 c,那么 a 可以通过 b 来认识 c),求出 x 最少需要通过多少人才能认识 y。

输入格式

第 1 行 3 个正整数 n、x、y,其中:n≤100,1≤x、y≤n。
接下来是一个 n×n 的邻接矩阵,a[i,j]=1 表示 i 认识 j,a[i,j]=0 表示 i 不认识 j。
保证 i=j 时,a[i,j]=0,并且 a[i,j] =a[j,i],一行中的两个数之间有一个空格。

输出格式

输出一行一个数,表示 x 认识 y 最少需要通过的人数。

样例

输入样例
5 1 5
0 1 0 0 0
1 0 1 1 0
0 1 0 1 0
0 1 1 0 1
0 0 0 1 0
输出样例
2

解题思想

本题是宽度优先搜索的一个重要应用“求最优值问题”。先设答案 ans=0。把 x 加入队列并设置为队头元素,从队头开始进行宽搜,穷举邻接矩阵的第 x 行,看 x 认识谁(判断 a[x,j]=1),认识的人(j)全部依次入队,并且 ans++,如果出现了 y,则输出 ans,结束搜索,否则,取出队头元素继续宽搜。

代码实现

由于作者太过懒惰,没有诶

二、 超级重点!!

若各位D神有例题的Ac代码或问题代码
都可在评论区留言,大家共同探讨学习
也便于本博客的完善!!谢谢大家!!


你可能感兴趣的:(Turing,搜索)