算法学习基础篇(一):搜索(DFS、BFS)
参考书籍《挑战程序设计》,本文实质为该书的学习笔记,结合了笔者自己的理解,欢迎指错~
在正式开始学搜索之前,先巩固一些基础
一 . 递归
定义:函数调用自身
要求:要有停止条件(边界条件)
e.g. 斐波那契数列(从第三位开始,每一位等于前两位之和。第一位=0,第二位=1)
int f(int n)
{
if(n <= 1) return n;
return f(n - 1) + f(n - 2);
}
优化(保存中间结果):
int m[MAX_N + 1];
int f(int n)
{
if(n <=1) return n;
if(m[n] != 0) return m[n];
return m[n] = f(n - 1) + f(n -2);
}
二 . 栈(Stack)
栈:后进先出(LIFO)的一种数据结构,一般使用数组或列表实现。
操作:
push:在栈的顶端放入一组数据
pop:从栈顶取出一组数据
C++的标准库中
stack::pop:移除最顶端的数据
stack::top:访问最顶端的数据
e.g.(使用C++标准库)
#include <stack>
#include <cstdio>
int main()
{
stack<int> s;
s.push(1);
s.push(2);
printf("%d\n", s.top());
s.pop();
printf("%d\n", s.top());
s.pop();
return 0;
}
结果:
2
1
三 . 队列(Queue)
队列:先进先出(FIFO)的一种数据结构。
操作:
push:将一组数据压入队列
pop:取出队列中最底端的元素
C++标准:
queue::front:访问最底端数据的函数
e.g.
#include <cstdio>
#include <queue>
int main()
{
queue<int> que;
que.push(1);
que.push(2);
printf("%d\n", que.front());
que.pop();
printf("%d\n", que.front());
que.pop();
return 0;
}
结果:
1
2
四 ※ 深度优先搜索(DFS)
深度优先搜索:从某个状态开始,不断地转移状态,直到无法继续转移时回退到前一步的状态,继续向其他可转移的状态转移,如此不断重复,直到找到最终解。可采用递归函数实现。
例如,求解数独,首先在某个格子内填入适当的数字,然后再继续在下一个格子内填入另一数字,如此继续下去。若发现某个格子无解了,就放弃前一个格子上选择的数字,改用其他可行的数字。
e.g.1.部分和问题:给定整数a1,a2,…,an(1≤n≤20;-10^8≤ai≤10^8),判断是否可以从中选出若干数,使它们的和恰好为k(-10^8≤k≤10^8)。
思路:从a1开始按顺序决定每个数加或不加(可理解为:对于每一个数都有两种选择,加或不加。类比数独问题中每个小格子都有10种选择(0-9)),即先确定a1是选择加/不加,再确定a2是选择加/不加,如此继续下去。若发现直到an,和不为k,则回退到上一步,如此往复,直到所得的和为k为止。
int a[MAX_N];
int n, k;
bool dfs(int i, int sum)
{
if(i == n) return sum == k;
if(dfs(i + 1, sum)) return true;//不加上a[i]
if(dfs(i + 1, sum + a[i]) return ture;//加上a[i]
reture false;
}
状态数2^(n+1),复杂度O(2^n)
e.g.2.Lake Counting(POJ No.2386)
有一个大小为N×M(N,M≤100)的园子,雨后积起了水。八连通的积水被认为是连接在一起的。请求出园子里共有多少水洼?(八连通指的是下图中相对W的+部分)
+++
+W+
+++
思路自己想罢!建议去看看原题
练习题:POJ1979, POJ3009
五 ※ 宽度优先搜索(BFS)
宽度优先搜索:与深度优先搜索类似,从某个状态出发探索所有可以到达的状态。它与深度优先搜索的不同之处在于搜索的顺序,宽度优先搜索总是先搜索距离初始状态近的状态。也就是说,它是按照开始状态->只需1次转移就可以到达的所有状态->只需2次转移就可以到达的所有状态->……以此顺序进行搜索。对于同一个状态,宽度优先搜索只经过一次,因此复杂度为O(状态数×转移的方式)。
宽度优先搜索利用了队列(深度优先搜索隐式利用了栈)。搜索时先将初始状态添加到队列里,此后从队列的最前端不断取出状态,把从该状态可以转移到的状态中尚未访问过的部分加入队列,如此往复,直至队列被取空或找到了问题的解。通过观察这个队列,我们可以很容易就知道所有的状态都是按照初始状态由近及远的顺序被遍历的。
e.g.迷宫的最短路径
给定一个大小为N×M(N,M≤100)的迷宫迷宫有通道和墙壁组成,每一步可以向邻接的上下左右四格的通道移动。请求出从起点到终点所需的最小步数。请注意,本题假设从起点一定可以移动到终点。
输入:
10 10
#S######.#
......#..#
.#.##.##.#
.#........
##.##.####
....#....#
.#######.#
....#.....
.####.###.
....#...G#
输出:
22
思路:宽度优先搜索按照据开始状态由近及远的顺序进行搜索,只要将已访问过的状态用标记管理起来,就可以很好地做到由近及远的搜索。用d[N][M]数组把最短距离保存起来。其实是用充分大的常数INF初始化它。这个问题中,状态仅仅是目前所在位置的坐标,因此可以构造成pair或者编码成int来表达状态。转移的方式为四个方向的移动,状态数与迷宫的大小是相等的, 所以复杂度是O(4×N×M)= O(N×M)。
代码:
#include <iostream>
#include <cstdio>
#include <queue>
using namespace std;
const int INF = 100000000;
typedef pair<int, int> P;
char maze[105][105];
int n, m;
int sx, sy;
int gx, gy;
int d[105][105];
int dx[4] = {1, 0, -1, 0}, dy[4] = {0, 1, 0, -1};
int bfs()
{
queue<P> que;
//把所有位置都初始化为INF
for(int i = 0; i < n; i++)
for(int j = 0; j < m; j++)
d[i][j] = INF;
//起点加入队列,并把这一点的距离设置为0
que.push(P(sx, sy));
d[sx][sy] = 0;
//不断循环直到队列的长度为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++)
{
//移动之后的位置标记为(nx,ny)
int nx = p.first + dx[i], ny = p.second + dy[i];
//判断是否可以移动以及是否已经访问过
if(0 <= nx && nx < n && 0 <= ny && ny < m && maze[nx][ny] != '#' && d[nx][ny] == INF)
{
//可以移动的话则加入队列,并且到该位置的距离确定为到p的距离+1
que.push(P(nx, ny));
d[nx][ny] = d[p.first][p.second] + 1;
}
}
}
return d[gx][gy];
}
int main()
{
scanf("%d%d", &n, &m);
for(int i = 0; i < n; i++)
for(int j = 0; j < m; j++)
{
cin>>maze[i][j];
if(maze[i][j] == 'S')
{
sx = i;
sy = j;
}
else if(maze[i][j] == 'G')
{
gx = i;
gy = j;
}
}
int res = bfs();
cout<<res<<endl;
return 0;
}
练习题:POJ3669
深度优先搜索与广度优先搜索的对比
深度优先搜索与广度优先搜索一样,都会生成所有能够遍历到的状态,因此需要对所有状态进行处理时也可以使用深度优先搜索,且递归函数编写简短,同时状态的管理也更简单,所以大多数情况下使用深度优先搜索实现。
但在求取最短路时, 深度优先搜索需要反复经过同样的状态,所以此时使用宽度优先搜索较好。
宽度优先搜索会把状态逐个加入队列,因此通常需要与状态数成正比的内存空间。
深度优先搜索是与最大的递归深度成正比的。
一般与状态数相比,递归的深度并不会太大,所以可以认为深度优先搜索更加节省内存。