基础算法系列之深度优先搜索
Posted 一米阳光213
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了基础算法系列之深度优先搜索相关的知识,希望对你有一定的参考价值。
在计算机的基础算法领域,深度优先搜索无疑是非常重要的一种编程方法,它的作用和价值,类似于常规算法中的排序,具有两个特点:
(1)常用。当我们需要搜索问题的解决方案时,最常用的只有两种方法:深度优先搜索和广度优先搜索;
(2)标准化。与快速排序类似,虽然实现的方法千变万化,但是遵循固定的步骤,第一步做什么,第二步做什么,都非常的明确。所以呢,只要吃透几个经典的问题,有了深刻的体会之后,所有这类问题都很 easy 了。
这篇文章的使命:由浅入深,讲透深度优先搜索算法。所以,这篇文章我会认真对待,持续地修改,最后的目的是:
(1)从入门到熟练:在本文的基础部分,即使是不了解深度优先搜索算法的童鞋,也能够准确、快速和熟练地掌握深度优先搜索;
(2)从熟练到精通:在本文的进阶部分,即使是熟悉深度优先搜索算法的童鞋,也能够进一步加深加强对深度优先搜索的理解和掌握。
为了不断提高本文的质量和水平,同时也因为我自己水平有限,所以希望各位提出宝贵意见 ~
例题 POJ 2251 综合了3D深度优先和3D广度优先搜索
题目传送门:http://poj.org/problem?id=2251
// 非常经典的深度优先搜索和广度优先搜索
//
#include<iostream>
#include<vector>
#include<queue>
#include<string.h>
using namespace std;
/*********** 全局变量 ******************/
int L, R, C;
char G[31][31][31];
int ans = 0; // 走出迷宫的最少步数
int start_row, start_col, start_level;
int exit_row, exit_col, exit_level;
int min(int a, int b)
return a > b ? b : a;
// 如果被访问过,则标记为'T'
void dfs(int level, int row, int col, int steps)
// Step 1: Termination condition
if (level == exit_level && row == exit_row && col == exit_col)
ans = min(ans, steps);
return;
// Step 2: 确定搜索范围
int dl[6] = 1, -1, 0, 0, 0, 0 ;
int dr[6] = 0, 0, 1, -1, 0, 0 ;
int dc[6] = 0, 0, 0, 0, 1, -1 ;
for (int i = 0; i < 6; i++)
int new_level = level + dl[i];
int new_row = row + dr[i];
int new_col = col + dc[i];
// Step 3: 排除无效的搜索
if (new_level < 0 || new_level >= L || new_row < 0 || new_row >= R || new_col < 0 || new_col >= C || G[new_level][new_row][new_col] == '#' || G[new_level][new_row][new_col] == 'T')
continue;
// Step 4: 使用当前结点
G[new_level][new_row][new_col] = 'T';
// Step 5: 访问下一步的结点
dfs(new_level, new_row, new_col, steps + 1);
// Step 6: 弃用当前结点,换用下一个
G[new_level][new_row][new_col] = '.';
/*************** BFS ****************/
struct node
int level;
int row;
int col;
int steps; // 已经走了多少步
node(int l, int r, int c,int s)
level = l;
row = r;
col = c;
steps = s;
node()
level = row = col = 0;
;
int bfs()
// Step 1: 放入第一个结点并标记
node start(start_level, start_row, start_col,0);
G[start_level][start_row][start_col] = 'T';
queue<node> Q;
Q.push(start);
// Step 2: 开始主循环
while (!Q.empty())
// Step 3: 取出队列中第一个元素,并判断是否满足终止条件
node cur = Q.front();
Q.pop();
if (cur.level == exit_level && cur.row == exit_row && cur.col == exit_col)
return cur.steps;
// Step 4: 访问下一步的结点
int dl[6] = 1, -1, 0, 0, 0, 0 ;
int dr[6] = 0, 0, 1, -1, 0, 0 ;
int dc[6] = 0, 0, 0, 0, 1, -1 ;
for (int i = 0; i < 6; i++)
int new_level = cur.level + dl[i];
int new_row = cur.row + dr[i];
int new_col = cur.col + dc[i];
// Step 5: 排除无效的搜索
if (new_level < 0 || new_level >= L || new_row < 0 || new_row >= R || new_col < 0 || new_col >= C || G[new_level][new_row][new_col] == '#' || G[new_level][new_row][new_col] == 'T')
continue;
// Step 6: 使用当前结点
G[new_level][new_row][new_col] = 'T';
Q.push(node(new_level, new_row, new_col, cur.steps + 1));
return -1; // 无法找到出口
int main(int argc, char * argv[])
while (scanf("%d%d%d", &L, &R, &C) && L != 0)
// Initialization
memset(G, 0, sizeof(G));
ans = 99999999;
/*************** Input /***************/
for (int l = 0; l < L; l++)
for (int r = 0; r < R; r++)
cin >> (G[l][r]);
// 找到起始点和终止点
for (int c = 0; c < C;c++)
if (G[l][r][c] == 'S')
start_level = l;
start_row = r;
start_col = c;
else if (G[l][r][c] == 'E')
exit_level = l;
exit_row = r;
exit_col = c;
//char tmp[30];
//cin.getline(tmp,30);
/*************** Process /***************/
int ans = bfs();
/*************** Output /***************/
if (ans == -1)
cout << "Trapped!" << endl;
else
cout << "Escaped in " << ans << " minute(s)." << endl;
return 0;
例题1:POJ 1011 Sticks. 题目链接:题目描述
无比经典的一道题目。可以说是必须弄懂的一道题目了。
思路分析:
(1)为什么要用深度优先搜索而不是广度优先搜索呢?
解释:如果大家学过二叉树的深度优先遍历和广度优先遍历就知道,不管是深度优先还是广度优先,都是对整个解空间进行搜索,其结果是一样的,但是呢,广度优先搜索必须借助一种数据结构--队列,如果数据的规模较大,那么它必须消耗大量的内存。在本题中,最多有 64 根小木棍,假设最多需要 8 根小木棍,那么队列中的元素数量为:64 * 63 * 62 * ... * 57 = 1784629 亿,假设一个元素为 int 占 4 个字节,那么内存消耗将是 712 TB ! 显然,广度优先行不通。
(2)有哪些剪枝方法?
一. 既然题目中没有指定这些小木棍的顺序,那么我们不妨对这些小木棍排序。升序 or 降序?取决于哪一种顺序有助于压缩搜索空间(即减少搜索次数)。假设小木棍长度分别为 1,2,3,4,5(升序排列),初始长度 init_len = 5,那么我们选中 1 之后,第二根选2,第三根选3,4,5都会导致失败,那么就会回溯到第二步,.... 省略后续。假设是降序排列,5,4,3,2,1,那么第一步选5,直接就凑好了一根原始木棍!第二步选4,再选1,又是一根!从这个例子可以看出,升序排列有助于压缩搜索空间,所以我们在输入所有小木棍长度之后,首先对其从大到小排序。
二. 假设,第1-3根小木棍长度都为x,如果第1根凑失败了,后面的第2-3根小木棍也一定失败,不需要再搜索一次,换句话说,如果某一根长度为 x 的小木棍导致了失败,则跳过后面的相同长度的小木棍。
三. 由基本常识可知,设初始木棍的长度为 init_len,所有小木棍的长度之和为 sum_len, 最长的小木棍长度为 max_len,则:init_len >= max_len,且 init_len 必定整除 sum_len。
基于以上分析,用 C++ 代码实现如下:
// poj 1011 Sticks 深度优先搜索.cpp : 定义控制台应用程序的入口点。
//
#include "stdafx.h"
#include<iostream>
#include<string.h>
#include<algorithm>
using namespace std;
/******************* 全局变量定义区 ***********************/
int a[66], n; // 小木棍的数量,以及每一根小木棍的长度
bool visit[66]; // 因为是多个阶段的搜索(凑齐一根初始木棍可以看作是一个阶段),所以需要记忆哪些小木棍已经被使用了,下次搜索直接跳过这些已经被使用的小木棍
int max_len = 0;
int sum_len = 0;
// 为了从大到小排序,必须定义这个函数
bool cmp(int a, int b)
return a > b;
bool dfs(int num_finished_sticks, int init_len, int cur_len, int start_index)
// Step 1: 确定我们 dfs 递归函数的终止条件
if (num_finished_sticks == sum_len / init_len) // 成功凑齐所有的初始木棍
return true;
// Step 2: 确定搜索范围: 从 (start_index) 到 (最后一根) 小木棍,都是可以进行搜索的。
int cur_stick_length = -1; // 例如,第1-3根小木棍长度都为x,如果第一根凑失败了,后面的第2-3根小木棍也一定失败,不需要再搜索一次
for (int i = start_index; i < n; i++)
// 剪枝: 剔除以下情况的新木棍
// 第一种情况:已经使用过了。
// 第二种情况:目前已经凑好的木棍长度 + 新木棍长度 > 初始木棍长度
// 第三种情况:新木棍长度 = 已经证明失败的木棍的长度
if ( visit[i] || cur_len + a[i] > init_len || a[i] == cur_stick_length ) continue;
// Step 3: 访问当前的搜索结点
visit[i] = true;
// Step 4: 确定下一步的搜索策略
if (cur_len + a[i] == init_len) // 恰好凑出一根初始木棍
if ( dfs(num_finished_sticks + 1, init_len, 0, 0)) // 请注意:搜索新一根初始木棍时,从头开始搜
return true;
else
cur_stick_length = a[i];
else // cur_len + a[i] < init_len
if (dfs(num_finished_sticks, init_len, cur_len + a[i], i + 1)) // 请注意:搜索下一根小木棍时,不是从头开始搜,而是直接从下一根开始搜。为什么?请看下面的分析
return true;
else
cur_stick_length = a[i];
// Step 4: 处理失败的情况
visit[i] = false;
if (cur_len == 0) // 剪枝:如果新棍子的第一根凑失败了,那么直接可以判定,整个凑棍过程一定失败。
return false;
return false;
int main(int argc, char * argv[])
while (scanf("%d", &n) && n != 0)
for (int i = 0; i < n; i++)
scanf("%d", &a[i]);
/**************** 剪枝:由于我们的 dfs 总是从第一根小木棍开始搜索,所以降序排列可以有效地压缩搜索空间 *****************/
sort(a, a + n, cmp);
/**************** 剪枝:由基本常识可知,初始木棍长度 >= 最长的小木棍 And 初始木棍长度一定能整除(所有小木棍的总长度) And 初始木棍至少有两根(至少本题的数据是这样的) *****************/
max_len = 0; // 所有小木棍中,最长的小木棍的长度
sum_len = 0; // 所有小木棍的总长度
for (int i = 0; i < n; i++)
if (a[i] >= max_len)
max_len = a[i];
sum_len += a[i];
bool found = false;
for (int init_len = max_len; init_len <= sum_len / 2; init_len++)
if (sum_len % init_len != 0) continue; // 剔除不符合条件的初始木棍长度
memset(visit, 0, sizeof(visit));
if (dfs(0, init_len, 0, 0)) // 成功地找到了初始木棍长度
cout << init_len << endl;
found = true;
break;
if (found == false)
cout << sum_len << endl;
return 0;
例题:牛客网 --> 好未来笔试题 --> 求和 题目链接:牛客网-好未来笔试题-求和
题目解读:从 1,2,...n 中,找若干个数使得它们和为 m ,打印出所有的组合。
分析: 基本的思路是,对 所有可能的组合 进行深度优先搜索遍历,一旦当前的方案恰好和为 m,打印之,并停止继续往更深的层次进行搜索。
思考:为什么这道题要用深度优先搜索而不是广度优先搜索?其实这题 dfs 和 bfs 都能解决问题,只是 dfs 在编写代码的时候很方便,容易写。
#include<iostream>
#include<stack>
#include<algorithm>
#include<vector>
#include<string>
using namespace std;
/*********** 题目描述:
输入两个整数 n 和 m,从数列1,2,3.......n 中随意取几个数,使其和等于 m ,要求将其中所有的可能组合列出来
************/
/******************** 分析思路: 采用深度优先搜索,一旦搜索到满足条件的组合,就输出 ********************/
int n, m, *a;
vector<int> st; // 存储当前搜索方案的数字栈
// next_index: the next index to search the array
// cur_sum: 当前已经凑好的数值
// 为了剪纸,压缩搜索空间,设置 failure 表示当前搜索是否成功
bool failure = false;
void dfs_search(int start_position, int cur_sum)
// Step 1: Termination conditions
if ( start_position >=n ) 因为已经升序排列了,所以后面的肯定不会满足要求,直接PASS,剪枝
return;
// Step 2: 确定下一步的搜索方案
for (int i = start_position; i < n; i++)
// 是否使用当前方案
if (cur_sum + a[i] > m) // 注意,回溯一步
break;
// 两种可能,一种是满足了目标要求,另一种是没有
else if (cur_sum + a[i] == m) // 满足了目标要求
st.push_back(a[i]);
// 输出当前的方案
for (int j = 0; j < (int)st.size(); j++)
if (j != st.size() - 1)
printf("%d ", st[j]);
else
printf("%d\\n", st[j]);
// 达到目标了,不要再继续往后面搜索。
//break;
else if(cur_sum + a[i] < m) //
st.push_back(a[i]);
dfs_search(i + 1, cur_sum+a[i]);
//st.erase(st.end() - 1);
//cur_sum -= a[i];
// Step 3: 处理失败的情况: 替换当前这个搜索结点
if (st.empty()==false)
st.erase(st.end() - 1);
int main(int argc, char * argv[])
// Input
cin >> n >> m;
a = new int[n]; //memset(a, 0, sizeof(int)*n);
for (int i = 0; i < n; i++)
a[i] = i + 1;
// Process
// 1. Sort
sort(a, a + n);
// 2. DFS
dfs_search(0, 0);
return 0;
例题:POJ 3984 迷宫问题 题目链接:POJ 3984 迷宫问题
这道题的难点在于需要打印整个搜索的路径。深度优先搜索 or 广度优先搜索都是OK的。 因为广度优先搜索天然适合于求解最短路径问题,所以 bfs 比 dfs 在编写代码上更容易一些。
#include<iostream>
#include<queue>
#include<vector>
#include<stack>
using namespace std;
/* 因为要打印最短路径,所以这个问题用广度优先搜索妥妥的
* 该问题的难点:需要记忆搜索的路径。怎么存储?什么数据结构?
* 可能的解决方案: 链表,一条路径就是一条链子。每一个结点只需要记住自己的父亲结点是谁,就足够我们进行回溯了!
*/
/***************** 全局变量定义区 ********************/
int G[5][5];
struct node
node(int a, int b)
x = a;
y = b;
prev = NULL;
node(int a, int b, struct node * c)
x = a;
y = b;
prev = c;
node()
x = 0;
y = 0;
prev = NULL;
int x, y; // 当前位置的坐标
int steps; // 当前走了多少步
struct node * prev; // 记录前一步的位置
;
typedef node * pnode;
struct link_node
;
/***************** 广度优先搜索 ********************/
void bfs()
queue<pnode> Q;
// Step 1: 初始化
pnode cur = new node(0, 0, NULL);
Q.push( cur );
bool visit[5][5] = false ;
visit[0][0] = true;
// Step 2: 进入主循环
while (!Q.empty())
// Step 3: 取出队列首部的元素
pnode cur = Q.front(); Q.pop();
// 判断是否达到结束条件
if (cur->x == 4 && cur->y == 4)
// 打印路径: 不断地进行回溯,直到空为止
stack<node> trace;
while (!(cur==NULL))
trace.push( node(cur->x,cur->y) );
cur = cur->prev;
//trace.push(node(0, 0));
while (!trace.empty())
node tmp = trace.top();
trace.pop();
printf("(%d, %d)\\n", tmp.y, tmp.x);
break; // 成功完成任务,直接强制退出
// Step 4: 确定下一步的搜索策略
int next_x, next_y;
// left
next_x = cur->x - 1;
next_y = cur->y;
if (next_x >= 0 && visit[next_y][next_x] == false && G[next_y][next_x] == 0)
visit[next_y][next_x] = true;
Q.push(new node(next_x, next_y,cur));
// right
next_x = cur->x + 1;
next_y = cur->y;
if (next_x <5 && visit[next_y][next_x] == false && G[next_y][next_x] == 0)
visit[next_y][next_x] = true;
Q.push(new node(next_x, next_y, cur));
// up
next_x = cur->x;
next_y = cur->y-1;
if (next_y>=0 && visit[next_y][next_x] == false && G[next_y][next_x] == 0)
visit[next_y][next_x] = true;
Q.push(new node(next_x, next_y, cur));
// down
next_x = cur->x;
next_y = cur->y+1;
if (next_y<5 && visit[next_y][next_x] == false && G[next_y][next_x] == 0)
visit[next_y][next_x] = true;
Q.push(new node(next_x, next_y, cur));
/***************** 深度优先搜索 ********************/
struct solution
solution(int step, vector<node> t)
steps = step;
trace = t;
int steps; // 从左上角一直走到右下角的步数
vector<node> trace; // 从左上角一直走到右下角的所有步骤,都存储于 trace 中
;
bool visited[5][5] = false ;
vector<node> record;
vector<solution> solutions;
void dfs(int row, int col)
// Step 1: Termination conditions
if (row == 4 && col == 4)
// 此时,这个方案不一定是最好的
solutions.push_back(solution(record.size(), record));
return;
// Step 2: 确定搜索范围: 上,下,左,右
int delta_x[4] = 0, 0, -1, 1 ;
int delta_y[4] = -1, 1, 0, 0 ;
for (int i = 0; i < 4; i++)
int next_x = col + delta_x[i];
int next_y = row + delta_y[i];
// Step 3: Visit the node !
if (next_x <0 || next_x >4 || next_y<0 || next_y>4 || visited[next_y][next_x]|| G[next_y][next_x] != 0)
continue;
// 标记之
visited[next_y][next_x] = true;
// 存储之
record.push_back(node(next_x, next_y));
// Step 3: 确定下一步的搜索策略
dfs(next_y, next_x);
// Step 4: 处理失败策略
// 反标记之
visited[next_y][next_x] = false;
// 反存储之
record.pop_back();
int main(int argc, char * argv[])
// Input
for (int i = 0; i < 5;i++)
for (int j = 0; j < 5; j++)
scanf("%d", &G[i][j]);
// Process
//bfs();
visited[0][0] = true;
record.push_back(node(0, 0));
dfs(0, 0);
// find the shortest path
int min_len = 99999;
int min_index = -1;
for (int i = 0; i < solutions.size(); i++)
if (solutions[i].steps < min_len)
min_len = solutions[i].steps;
min_index = i;
// Output
for (int i = 0; i < solutions[min_index].steps; i++)
printf("(%d, %d)\\n", solutions[min_index].trace[i].y, solutions[min_index].trace[i].x);
return 0;
例题:POJ 1088 滑雪 深度优先搜索 题目链接:POJ 1088 滑雪 最大滑雪长度
#include<iostream>
using namespace std;
// 一个基本的先验知识是:设 G[x][y] 表示 (x,y) 位置的高度,设 dp[x][y]表示以(x,y)为起始点,所能滑雪的最大长度,那么 dp[x][y] = max( dp[x-1][y], dp[x+1][y], dp[x][y-1], dp[x][y+1] ) + 1,其中,G[x-1][y], G[x+1][y], G[x][y-1], G[x][y+1] 都小于 G[x][y]
//
/***************** 全局变量定义区 ********************/
int H, W, G[102][102];
bool visit[102][102];
int ans = 0; // 最长的滑雪长度
/*************** 这道题也可以用动态规划的思路 ******************/
int max(int a, int b)
return a > b ? a : b;
int dp[102][102]; // 重点:dp[x][y]表示以(x,y)为起始点,所能滑雪的最大长度
// POJ 运行时间:32 ms
int dfs_max_len(int x, int y)
// Step 1: Termination conditions
if (x < 0 || x >= W || y < 0 || y >= H )
return 0;
// Step 2: Visit the node
visit[y][x] = true;
// Step 3: Go deeper
int max_len = 1;
// left
if (x - 1 >= 0 && G[y][x - 1] < G[y][x])
max_len = max(max_len, dp[x-1][y] ==1 ? 1+ dfs_max_len(x - 1, y) : 1+ dp[x-1][y] );
// right
if (x + 1 <W && G[y][x + 1] < G[y][x])
max_len = max(max_len, dp[x+1][y]==1 ? 1+ dfs_max_len(x + 1, y) : 1+ dp[x+1][y]);
// up
if (y - 1 >= 0 && G[y-1][x] < G[y][x])
max_len = max(max_len, dp[x][y-1]==1 ? 1+ dfs_max_len(x, y-1):1+ dp[x][y-1] );
// down
if (y + 1 < H && G[y + 1][x] < G[y][x])
max_len = max(max_len, dp[x][y+1]==1 ? 1+ dfs_max_len(x, y + 1):1+ dp[x][y+1] );
return max_len;
// POJ运行时间: 47 ms
int dfs_max_len_brute_force(int x, int y)
// Step 1: Termination conditions
if (x < 0 || x >= W || y < 0 || y >= H)
return 0;
// Step 2: Visit the node
visit[y][x] = true;
// Step 3: Go deeper
int max_len = 1;
// left
if (x - 1 >= 0 && G[y][x - 1] < G[y][x])
max_len = max(max_len, 1 + dfs_max_len(x - 1, y) );
// right
if (x + 1 <W && G[y][x + 1] < G[y][x])
max_len = max(max_len, 1 + dfs_max_len(x + 1, y));
// up
if (y - 1 >= 0 && G[y - 1][x] < G[y][x])
max_len = max(max_len, 1 + dfs_max_len(x, y - 1));
// down
if (y + 1 < H && G[y + 1][x] < G[y][x])
max_len = max(max_len, 1 + dfs_max_len(x, y + 1));
return max_len;
int DP()
int ans = 0; // 最长的滑雪长度
for (int y = 0; y < H;y++)
for (int x = 0; x < W; x++)
// 记忆这个结果
dp[x][y] = dfs_max_len_brute_force(x,y);
ans = max(ans, dp[x][y]);
return ans;
int main(int argc, char ** argv[])
// Input
cin >> H >> W;
for (int i = 0; i < H;i++)
for (int j = 0; j < W; j++)
scanf("%d", &G[i][j]);
// 初始化 dp 数组
dp[j][i] = 1;
// Process
int ans = DP();
// Output
cout << ans << endl;
return 0;
例题:POJ 2386 数数地图上有多少个水坑 题目链接:POJ 2386 地图上有多少个水坑
非常简单的深搜问题了,每搜到一个位置,就标记一下。
// 内存空间消耗量:732 K
// 运行时间: 63 ms
#include<iostream>
using namespace std;
/************** 全局变量定义 *****************/
int n, m; // 本题中地图有 n 行 m 列
char G[102][102];
/******* 基于深度优先搜索的“填水坑”函数
* 这道题目非常简单,不需要记忆任何的状态,也没有其他的限制条件
*/
void dfs_fill_pond(int row, int col)
// Step 1 : Termination conditions
if (row < 0 || row >= n || col < 0 || col >= m)
return;
if (G[row][col] == '.')
return;
// Step 2: 确定搜索范围:相邻的8个位置
G[row][col] = '.';
dfs_fill_pond(row - 1, col - 1);
dfs_fill_pond(row - 1, col );
dfs_fill_pond(row - 1, col + 1);
dfs_fill_pond(row , col - 1);
dfs_fill_pond(row , col + 1);
dfs_fill_pond(row + 1, col - 1);
dfs_fill_pond(row + 1, col );
dfs_fill_pond(row + 1, col + 1);
int main(int argc, char * argv[])
// Input
cin >> n >> m;
for (int row = 0; row < n;row++)
for (int col = 0; col < m; col++)
cin >> G[row][col];
// Process
int num_ponds = 0;
for (int row = 0; row < n;row++)
for (int col = 0; col < m; col++)
if (G[row][col] == 'W')
num_ponds++;
dfs_fill_pond(row, col);
// Output
cout << num_ponds << endl;
return 0;
未完待续。持续改进中......
以上是关于基础算法系列之深度优先搜索的主要内容,如果未能解决你的问题,请参考以下文章