vlambda博客
学习文章列表

算法笔记第一期——深度优先搜索

深度优先搜索

概念

 深度优先搜索属于算法的一种,是一个针对图和树的遍历算法,英文缩写为DFS即 Depth First Search。

例如,在下面的树结构中找出节点1。

采取的策略是按照深度优先的方式进行,也就是一条路走到底。每次进入都先走左边,直到左边不能走了,退回一步,选择没有走过的路(右边)。

算法笔记第一期——深度优先搜索

其中搜索实际上指的是一种穷举策略,按照该策略,将所有的可行方案全部列举出来,不断进行尝试,直到找到问题的解。

举例说明

1、全排列

【题目描述】

给定一个由不同的小写字母组成的字符串,输出这个字符串的所有全排列。

我们假设对于小写字母有‘a’ <‘b’ < ... <‘y’<‘z’,而且给定的字符串中的字母已经按照从小到大的顺序排列。

【输入】

只有一行,是一个由不同的小写字母组成的字符串,已知字符串的长度在1到6之间。

【输出】

输出这个字符串的所有排列方式,每行一个排列。要求字母序比较小的排列在前面。

【样例输入】

 abc

【样例输出】

 abc
 acb
 bac
 bca
 cab
 cba

题目分析:对于3个字符,我们可以假定有3个格子,每个格子中放一个字符,要求形成所有的排序顺序。明显按照字母顺序,我们从前往后选择即可,这意味着先将a放在第一个格子,然后放b在第二个格子,然后c。当3个格子都被放满了,说明形成了第一个顺序,然后考虑第二个格子放c,第三个放b,依次类推。

算法笔记第一期——深度优先搜索

需要注意的是,我们的策略是,从没有选择过的字符中选择一个放入格子中,因此,我们需要给每一个字符做一个标记,用于表示这个字符的选择状态。我们在进行回退操作时,也要将选择状态重置为未选择。

#include <iostream>#include <cstring>using namespace std;#define N 7char a[N], b[N];int n;bool vst[N]; //用于记录选择状态
void dfs (int step) { //step当前放的格子数 if (step == n) { for (int i = 0; i < n; i++) cout << a[i]; cout << endl; return ; } for (int i = 0; i < n; i++) { // 往step格子中存    if (!vst[i]) { //选取第i个没有被用过的字符      vst[i] = 1; a[step] = b[i]; // 第step个格子中存字符b[i] dfs(step+1);      vst[i] = 0; } }}
int main () { cin >> b; n = strlen(b); dfs (0); return 0;}

2、N皇后

【问题描述】

在N*N的棋盘上放置N个皇后(n<=10)而彼此不受攻击(即在棋盘的任一行,任一列和任一对角线上不能放置2个皇后),编程求解所有的摆放方法。

【输入格式】

输入:n

【输出格式】

输出共有多少种摆放方案。

【输入样例】

 4

【输出样例】

 2

对于样例4皇后来说,我们的搜索策略一定是试着去穷举所有的可能性,那么在整个棋盘上,一定会逐行的进行尝试,尝试该放置是否能放置皇后。如下图所示

首先将皇后 放置1行1列的位置,那么相应的对角线,横竖列都不能放置。接下来从第2行找到第一个能放置的位置放。

算法笔记第一期——深度优先搜索


放完后发现第3行没有位置了,不满足条件。回退一步,放置2行末尾。

算法笔记第一期——深度优先搜索

接着从3行放置,发现4行不满足条件,继续回退到第一个皇后的放置位置,尝试放置第1行第2个位置。

继续往后放置皇后

可以发现,成功的找出了一种答案。


此时需要的考虑的问题就是,如何在程序中表示?思考上述的过程发现

1、从第一个位置开始放置皇后,尝试放置,并且放好后,在同行、同列、同对角线做好不能放置皇后的标记。

  • 对于某个位置的皇后而言,可以通过数组row[i]表示第i行是否放置了皇后,col[i]表示在第i列是否放置了皇后

  • 而对角线注意有两条,这两条对角线的特点是一条对角线横纵坐标之和相等,另外一条横纵坐标之差相等,因此可以用数组d1[i]、d2[i]表示两条对角线上是否放置了皇后。

2、当无法完成放置的时候,需要进行回退操作。实际上利用好递归的过程即可。


搜索时可以按层进行

参考程序

#include <iostream>#define N 15using namespace std;
int cnt, n;int r[N], c[N], d1[N*2], d2[N*2];
void dfs (int floor) { if (floor == n+1) { //顺利填完最后一层。方案数加1 cnt ++; return ; } for (int i = 1; i <= n; i++) { //在floor层中寻找可以放的位置 if (!c[i] && !d1[floor+i] && !d2[n+floor-i]) { // 同行、列、对角线没有皇后 c[i] = d1[floor+i] = d2[n+floor-i] = 1; dfs (floor+1); //找下一层 c[i] = d1[floor+i] = d2[n+floor-i] = 0; } }}int main () { cin >> n; dfs (1); cout << cnt; return 0;}

小结

  • 深度优先搜索的做法是从一个起点开始一直按照某种策略搜索遍历下去,直到满足边界条件或者没有数据遍历,则从第二个点开始遍历搜索,直到所有数据情况遍历完成。

  • 实际上深搜是根据初始条件和搜索策略构造一棵解答树并在过程中不断寻找目标状态的节点的过程。



题目练习

1、选数

已知 n 个整数 x1,x2,…,xn,以及1个整数k(k<n)。从n个整数中任选k个整数相加,可分别得到一系列的和。例如当n=4、k=3、4个整数分别为3、7、12、19时,可得全部的组合与它们的和为:

 3+7+12=22
 3+7+19=29
 7+12+19=38
 3+12+19=34

现在,要求你计算出和为素数共有多少种。而上述例子中只有一种和为素数:3+7+19=29。

【输入格式】

 n k
 x1 x2 ... xn

【输出格式】

输出满足条件的数有多少种

【输入样例】

 4 3
 3 7 12 19

【输出样例】

 1

易错点:从n个数中选k个数,可能会有重复,例如x1,x2,x3,也可能选成x2,x1,x3。那么为了去重,只要保证选择的数在原序列的位置是递增的即可。

参考程序

#include <iostream>using namespace std;#define N 25
int a[N];int n, k, cnt;bool vst[N];
bool is_prime (int x) { if (x < 2) return false; for (int i = 2; i * i <= x; i++) if (x % i == 0) return false; return true;}
void dfs (int sum, int step, int before) { if (step == k) { if (is_prime(sum)) cnt ++; return ; } for (int i = 1; i <= n; i++) { if (!vst[i] && i > before) { vst[i] = 1; dfs(sum+a[i], step+1, i); vst[i] = 0; } }}
int main () { cin >> n >> k; for (int i = 1; i <= n; i++) cin >> a[i]; dfs(0, 0, 0); cout << cnt; return 0;}

2、Lake Counting

【问题描述】

有一个大小为N×M的园子,雨后积起了水。八连通的积水被认为是连接在一起的。请求出园子里总共有多少水洼?假定'w'表示水洼,'.'表示没有水洼。所谓八连通指的是,在'W'的上下左右8个方向如果也有水洼,那么它们是连通的。例如下面的两个水洼是连通的。

.  .  W

. W .

.  .  .

【限制条件】

N,M <= 100

【输入格式】

第一行n和m

接下来n+1行,每行m个字符,表示园子的情况

【输出格式】

一行,表示总共多少个水洼

【样例输入】

 10 12
 W........WW.
 .WWW.....WWW
 ....WW...WW.
 .........WW.
 .........W..
 ..W......W..
 .W.W.....WW.
 W.W.W.....W.
 .W.W......W.
 ..W.......W.

【样例输出】

 3

参考程序

#include <iostream>#include <string>using namespace std;const int MAXN = 100 + 5;string a[MAXN];int n, m;void dfs(int x, int y){ a[x][y] = '.'; for (int dx = -1; dx <= 1; ++dx) { for (int dy = -1; dy <= 1; ++dy) { int nx = x + dx, ny = y + dy; if (nx >= 0 && nx < n && ny >= 0 && ny < m && a[nx][ny] == 'W') dfs(nx, ny); }    }}int main(){ int ans = 0; cin >> n >> m; for (int i = 0; i < n; ++i) cin >> a[i]; for (int i = 0; i < n; ++i)  for (int j = 0; j < m; ++j) if (a[i][j] == 'W') { dfs(i,j); ans++; } cout << ans << endl;}

框架

可以参考这个框架进行思考。


void dfs(....) { // DFS的参数由具体搜索策略而定 if(边界条件) {//打印、结束、比较 做相应处理 } else { for(...) { // 枚举同层的每一种可能的情况 if() {满足条件 设置条件 //存数组、设约束(已访问等)、加总量(求总和等) bfs(...) // 进入下一层进行搜索 恢复条件设置 } } }}