C++搜索算法总结

最基础的“穷竭搜索”


💨穷竭搜索是将所有的可能性罗列出来(枚举),在其中寻找答案的方法。这里主要介绍深度优先搜索和广度优先搜索这两方法。

1.递归函数

在一个函数中再次调用该函数自身的行为叫做递归,这样的函数被称作递归函数。例如,我们想要编写一个计算阶乘的函数 i n t f a c t ( i n t n ) int fact(int n) intfact(intn),当然,用循环来实现也是可以的。循环嵌套
但是根据阶乘的递推式 n ! = n × ( n − 1 ) ! n! = n×(n - 1)! n!=n×(n1)!,我们可以写成如下形式:

int fact(int n) {
	if(n == 0) return 1; //停止条件
	return n * fact(n - 1); //递归
}

在编写一个递归函数时,函数的停止条件是必须存在的。在刚刚的例子中,当 n = 0 n = 0 n=0 f a c t fact fact并不是继续调用自身,而是直接返回 1 1 1.如果没有这一条件的存在,函数就会无限地递归下去,程序就会失控崩溃了。BONG!
fact递归过程
我们再来试试编写计算斐波那契数列的函数 i n t int int f i b ( i n t n ) fib(int\quad n) fib(intn)。斐波拉契数列的定义是 a 0 = 0 、 a 1 = 1 a_0=0、a_1= 1 a0=0a1=1以及 a n = a n − 1 + a n − 2 ( n > 1 ) a_n = a_{n-1} + a_{n-2}(n>1) an=an1+an2(n>1)。这里,初项的条件就对应了递归的终止条件。数列的定义直接写成函数就可以了。

int fib(int n) {
if(n <= 1) return n;
return fib(n - 1) + fib(n - 2);

实际使用这个函数时,即使是求 f i b ( 40 ) fib(40) fib(40)这样的 n n n较小时的结果,也要花费相当长的时间。这是因为这个函数在递归时,回想下图一样按照指数级别拓展开来。
fib(10)
在斐波那契数列中,如果 f i b ( n ) fib(n) fib(n) n n n是一定的,无论多少次调用都会得到相同的结果。因此如果计算一次后,用数列将结果储存起来,便可优化之后的空间。这种方法是出于记忆化搜索或者动态规划的想法,我们之后会介绍。

int memo[MAX_N + 1];

int fib(int n) {
if(n <= 1) return n;
if(memo[n] != 0) return memo[n];
return memo[n] = fib(n - 1) + fib(n - 2);
2.栈

栈(Stack)是支持pushpop两种操作的数据结构。push是在站的顶端放入一组数据的操作。反之,pop是从其顶端去取出一组数据的操作。因此,最后进入站的一组数据可以最先被取出(这种行为被称作LIOF:Last In First Out,即后进先出

通过使用数组或者列表等结构可以和能以实现栈,不过C++,Java等程序语言的标准库已经为我们准备好了这一常用结构。C++的标准库中,stack::pop完成的仅仅是移除最顶端的数据。如果要访问最顶端的数据,需要使用stack::top函数(这个操作通常也被称为peek)。

栈
函数调用的过程是通过使用栈实现的。因此,递归函数的递归过程也可以改用栈上的操作来实现。现实中需要如此改写的场合并不多,不过作为使用栈的练习试试看还是不错的。以下是使用stack的例子:

#include <stack>
#include <cstdio>

using namespace std;

int main() {
	stack<int> s;		    //声明储存int类型数据的栈
	s.push(1);              //{} --→ {1}
	s.push(2);              //{1} --→ {1, 2}
	s.push(3);              //{1, 2} --→ {1, 2 ,3}
	printf("%d\n",s.top()); //3
	s.pop();                //从栈顶移除{1, 2, 3} --→ {1, 2}
	printf("%d\n",s.top()); //2
	s.pop();                //{1, 2} --→ {1}
	printf("%d\n",s.top()); //1
	s.pop();                //{1} --→ {}
	
	return 0;
} 
3.队列

队列(Queue)与栈一样支持pushpop两个操作。但与栈不同的是,pop完成的不是取出最顶端的元素,而是去除最底端的元素。也就是说最初放入的元素能够最先被取出(这种行为被叫做FIFO:First In First Out,即先进先出)。

队列
如同栈一样,C++、Java等的标准库也预置了队列。Java与C++中的函数的名称与用途稍有不同,因此使用时要注意。此外,在C++中queue::front是用来访问最底端数据的函数。以下是使用queue的例子:

#include <queue>
#include <cstdio>

using namespace std;

int main() {
	queue<int> que;		         //声明储存int类型数据的队列
	que.push(1);                 //{} --→ {1}
	que.push(2);                 //{1} --→ {1, 2}
	que.push(3);                 //{1, 2} --→ {1, 2 ,3}
	printf("%d\n", que.front()); //1
	que.pop();                   //从栈顶移除{1, 2, 3} --→ {2, 3}
	printf("%d\n", que.front()); //2
	que.pop();                   //{2, 3} --→ {3}
	printf("%d\n", que.front()); //3
	que.pop();                   //{3} --→ {}
	
	return 0;
} 
4.深度优先搜索

深度优先搜索(DFS,Depth-First Search)是搜索的手段之一。它从某个状态开始,不断地转移状态直到无法转移,然后回退到前一步的状态,继续转移到其它状态,如此不断重复,直至找到最终的解。例如求解数独,首先在某个格子内填入适当的数字,然后再继续在下一个格子内填入数字,如此继续下去。如果发现某个格子无解了,就放弃前一个各自上选择的数字,改用其它可行的数字。根据深度优先搜索的特点,采用递归函数实现比较简单。
状态转移
我们来试着解答一下下面的题目:

部分和 问题

给定整数 a 1 、 a 2 . . . . . . a n a_1、a_2 ...... a_n a1a2......an,判断是否可以从中选出若干数,是他们的和正好为 k k k

⚠限制条件:
· 1 ≤ n ≤ 20 1 \le n \le 20 1n20
· − 1 0 8 ≤ a i ≤ 1 0 8 -10^8 \le a_i \le 10^8 108ai108
· − 1 0 8 ≤ k ≤ 1 0 8 -10^8 \le k \le 10^8 108k108

样例1
In:
n=4
a={1, 2, 4, 7}
k=13
————————————————————
Out:
Yes (13 = 2 + 4 + 7)
样例2
In:
n=4
a={1, 2, 4, 7}
k=15
———————————————
Out:
No

a 1 a_1 a1开始按顺序决定每个数加或不加,在全部 n n n个数都决定后再判断他们的和是不是 k k k即可。因为状态数是 2 n + 1 2^{n+1} 2n+1,所以复杂度是 O ( 2 n ) O(2^n) O(2n)。如何实现这个搜索,请参见下面的代码。注意 a a a的下标与题目描述中的下标偏移了1。在程序中使用的是0起始的下标规则,题目描述中则是1开始的,这一点要注意避免混淆。

样子

//输入
int a[MAX_N];
int n, k;

//已经从前i项得到了和sum,然后对于i项之后的进行分支
bool dfs(int i, int sum) {
	//如果前n项都算好了,则返回sum是否与k相等
	if (i == n) return sum == k;

	//不加上a[i]的情况
	if(dfs(i + 1, sum)) return true;
	//加上a[i]的情况
	if(dfs(i + 1, sum + a[i])) return true;
	
	//无论是否加上a[i]都不能凑成k就返回false
	return false;
}

void solve() {
	if(dfs(0, 0)) printf("Yes\n");
	else printf("No\n");
}

深度优先搜索从最开始的状态出发,遍历所有可以达到的状态。由此可以对所有的状态进行操作,或者列举出所有状态。
练习一下:Lake Counting(POJ No.2386)(水坑问题<一点都不水!!>

5.宽度优先搜索

宽度优先搜索(BFS,Breadth-First Search)也是搜索的手段之一。它与深度优先搜索相似,从某个状态出发探索所有可以到达的状态。
与深度优先搜索的不同之处在于搜索的顺序,宽度优先搜索总是先搜索距离初始状态近的状态。也就是说,它是按照开始状态——>只需一次转移就可以到达的所有状态——>只需2次转移就可以达到的所有状态——>…这样的顺序进行搜索。对于同一个状态,宽度优先搜索只经过一次,因此复杂度为 O O O(状态数×转移的方式)

BFS
深度优先搜索(隐式地)利用了栈进行计算,而宽度优先搜索则利用了队列。搜索时首先将初始状态添加到队列中,此后从队列的最前端不断取出状态,把从该状态可以转移到的状态中尚未访问过的部分加入队列,如此往复,直至对列被取空或找到了问题的解。通过观察这个队列,我们就可以知道所有的状态都是按照距初始状态由近及远的顺序被遍历的。

迷宫的最短路径

给定一个大小为M × N的迷宫。迷宫有通道和墙壁组成,每一步可以邻接的上下左右四格的通道移动。请求出从起点到终点所需的最小步数。请注意,本题假定从起点一定可以移动到终点。

⚠限制条件:
· N , M ≤ 100 N,M\le100 N,M100

样例
In:
N=10, M=10(迷宫如下图所示。‘#’,‘.’,‘S’,‘G’分别表示墙壁、通道、起点和终点)

#S######.#
......#..#
.#.##.##.#
.#........
##.##.####
....#....#
.#######.#
....#.....
.####.###.
....#...G#

Out:
22

宽度优先搜索按照距开始状态由近及远的顺序进行搜索,因此可以很容易地用来求最短路径、最少操作之类问题的答案。这个问题中,状态仅仅是目前所在位置的坐标,因此可以构造成pair或者编码成int来表达状态。当状态更加复杂时,就需要封装成一个类来表示状态了。转移的方式为四方向移动,状态数与迷宫的大小是相等的,所以复杂度是 O ( 4 × N × M ) = O ( N × M ) O(4×N×M) = O(N×M) O(4×N×M)=O(N×M)
宽度优先搜索中,只要将已经访问过的状态用标记管理起来,就可以很好地做到由近及远的搜索。这个问题中由于要求最短路径,不妨用d[N][M]数组把最短距离保存起来。初始时用充分大的常熟INF来初始化它,这样尚未到达的位置就是INF,也就同时起到了标记的作用。
因为要向4个不同的方向移动,用dx[4]和dy[4]两个数组来表示四个方向变量。这样通过一个循环就可以实现四方向的遍历。妙啊

const int INF = 100000000;
typedef pair<int , int> P;

char maze[MAX_N][MAX_M + 1];
int N, M;
int sx, sy;
int gx, gy;

int d[MAX_N][MAX_M];

int dx[4] = {1, 0, -1, 0}, dy[4] = {0, 1, 0, -1};

int bfs() {
	queue<P> que;
	for(int i = 0; i < N; i++)
		for(int j = 0; j < M; j++) d[i][j] = INF;
	que.push(P(sx, sy));
	d[sx][sy] = 0;
	
	while(que.size()) {
		P p = que.front();que.pop();
		if(p.first == gx && p.second == gy) break;
		
		for(int i = 0; i < 4; i++) {
			int nx = p.first + dx[i], ny = p.second + dy[i];
			if(0 <= nx && nx < N && 0 <= ny && ny < M && maze[nz][ny] != '#' && d[nx][ny] == INF) {
				que.push(P(nx, ny));
				d[nx][ny] = d[p.first][p.second] + 1;
			}
		}
	}
	return d[gx][gy];
}

void solve() {
	int res = bfs();
	printf("%d\n", res);
}

宽度优先搜索会把状态逐个加入队列,因此通常需要状态数成正比的内存空间。反之,深度优先搜索是与最大的递归深度成正比的。一般与状态数相比,递归的深度并不会太大,所以可以认为深度优先搜索更加节省内存。

好的,本篇文章就写到这里,下篇详解剪枝对搜索有非常大的帮助,关注我,你的点赞就是对我最大的鼓励!

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值