深度优先搜索(DFS,Depth First Search)简称深搜或者爆搜,DFS 的搜索顺序是按照深度优先搜索,简单来说就是 “一条路走到黑”,搜索是把所有方案都试一遍,再判断是不是一个可行解。搜索与 “递归” 和 “栈” 有很大的联系,递归是实现搜索的一种方式,而栈则是计算机实现递归的方式。每个搜索过程都对应着一棵递归搜索树,递归搜索树可以让我们更加容易的理解 DFS。 整个搜索过程就是基于该搜索树完成的,为了不重复遍历每个结点,会对每个结点进行标记,也可以对树中不可能是答案的分支进行删除,从而更高效的找到答案,这种方法被称为剪枝。如果搜索树在某个子树中搜索到了叶结点,想继续搜索只能返回上个或多个状态,返回的过程称为回溯,回溯要记得恢复状态,才能保证接下来的搜索过程可以正常进行。
回溯法(探索与回溯法)是一种选优搜索法,又称为试探法,按选优条件向前搜索,以达到目标。但当探索到某一步时,发现原先选择并不优或达不到目标,就退回一步重新选择,这种走不通就退回再走的方法为回溯法,而满足回溯条件的某个状态的点称为“回溯点”。
深度优先遍历图的方法是,从图中某顶点v出发:
- 访问顶点v。
- 依次从v的未被访问的邻接点出发,对图进行深度优先遍历;直至图中和v有路径相通的顶点都被访问。
- 若此时图中尚有顶点未被访问,则从一个未被访问的顶点出发,重新进行深度优先遍历,直到图中所有顶点均被访问过为止。
DFS一般用于求解问题有多少种情况,多少条路径,最大路径等等。
void dfs(int step)
{
if(满足回溯条件)
{
对应操作
return;
}
尝试每一种可能
{
if(满足选择条件)
{
进行选择与标记
继续下一步dfs(step+1)
撤销选择与标记
}
}
}
/*
也可以这样理解:
void dfs(int k) { // k代表递归层数,或者说要填第几个空
if (所有空已经填完了) {
判断最优解/记录答案;
return;
}
for (枚举这个空能填的选项)
if (这个选项是合法的) {
记录下这个空(保存现场);
dfs(k + 1);
取消这个空(恢复现场);
}
}
*/
// 1.算法竞赛中,如果无法找到高效求解的方法(如贪心、递推、动态规划、公式推导等),使用搜索也可以解决一些规模较小的情况。
// 2.但不管怎么说,时间复杂度往往是指数级别的,效率相比于多项式时间复杂度还是要低。
给定一个 N×M 方格的迷宫,迷宫里有 T 处障碍,障碍处不可通过。
在迷宫中移动有上下左右四种方式,每次只能移动一个方格。数据保证起点上没有障碍。
给定起点坐标和终点坐标,每个方格最多经过一次,问有多少种从起点坐标到终点坐标的方案。
#include
using namespace std;
int n,m,t,a[10][10],v[10][10],ans;
int d[][2]={{0,1},{1,0},{0,-1},{-1,0}};
struct point
{
int x,y;
};
point start,fi;
void dfs(point s)
{
point temp;
if(s.x==fi.x&&s.y==fi.y)//到达终点,可达路径加一
{
ans++;
return;//返回上个点
}
for(int i=0;i<4;i++)//遍历每个点的四个方向
{
temp.x=s.x+d[i][1];
temp.y=s.y+d[i][0];
if(temp.y>=1&&temp.y<=n&&temp.x>=1&&temp.x<=m&&a[temp.y][temp.x]==0&& v[temp.y][temp.x]==0)//若下个点在迷宫范围内且为没有被访问过的非障碍点
{
v[temp.y][temp.x]=1;//标记选择的点,防止重复遍历
dfs(temp);//以选择的点进行新的遍历
v[temp.y][temp.x]=0;//撤销标记
}
}
}
int main()
{
cin >> n >> m>> t;
cin >> start.x >> start.y >> fi.x >> fi.y;
point temp;
for(int i=0;i<t;i++)
{
cin >> temp.x >> temp.y;
a[temp.y][temp.x]=1;
}
v[start.y][start.x]=1;
dfs(start);
cout << ans;
return 0;
}
按照字典序输出自然数 1 到 n 所有不重复的排列,即 nn 的全排列,要求所产生的任一数字序列中不允许出现重复的数字。
#include
using namespace std;
int gd[15],a[15],n;
void pt(){
for(int i=1;i<=n;i++)
printf("%5d",a[i]);
printf("\n");
}
void dfs(int dee){
if(dee>n){
pt();
return;
}
for(int i=1;i<=n;i++){
if(!gd[i]){
gd[i]=1;
a[dee]=i;
dfs(dee+1);
gd[i]=0;
}
}
return;
}
int main(){
scanf("%d",&n);
dfs(1);
return 0;
}
// 详解版:
#include
using namespace std;
int n,a[10],q[50000],v[10],p;
void dfs(int step)
{
if(step==n+1)//第n+1步时,表示已经选择了n个数
{
for(int i=1; i<=n; i++)//进行输出
{
printf("%5d",q[i]);//%5d是保留五个场宽
}
cout << '\n';
return;//返回上步继续选择
}
for(int i=1; i<=n; i++)//遍历所有可选择的数字
{
if(v[i]==0)//满足未被使用的条件
{
q[step]=a[i];//选择数字i,放入数组q
v[i]=1;//将数字i进行标记
dfs(step+1);//进行下步选择
v[i]=0;//撤销数字i的标记
}
}
return ;
}
int main()
{
cin >>n;
for(int i=1; i<=n; i++)a[i]=i;
dfs(1);
return 0;
}
争对全排列问题,我们还可以使用函数——next_permutation(),该函数位于头文件algorithm中。
next_permutation()有三个参数(默认是字典序排列):序列的首地址、序列的尾地址、比较函数(可选)
C++ STL 全排列
宽度优先搜索(Breadth First Search,简称BFS):同样是一种遍历搜索树或图的算法。遍历方式为选定一个节点,接着访问所有与当前节点连接的满足条件的点。接着从这些可访问点中,按照相同的遍历方式访问每个节点,直到所有节点都被访问,这与树的层次遍历相同,时间复杂度与DFS相同,与搜索树和图的节点树相关。
深度优先搜索用栈(stack)来实现,整个过程可以想象成一个倒立的树形:
1、把根节点压入栈中。
2、每次从栈中弹出一个元素,搜索所有在它下一级的元素,把这些元素压入栈中。并把这个元素记为它下一级元素的前驱。
3、找到所要找的元素时结束程序。
4、如果遍历整个树还没有找到,结束程序。
广度优先搜索使用队列(queue)来实现,整个过程也可以看做一个倒立的树形:
1、把根节点放到队列的末尾。
2、每次从队列的头部取出一个元素,查看这个元素所有的下一级元素,把它们放到队列的末尾。并把这个元素记为它下一级元素的前驱。
3、找到所要找的元素时结束程序。
4、如果遍历整个树还没有找到,结束程序。
BFS一般用于解决最短路径,最短步骤等最优问题。
以下两种模板是用队列的形式来实现的:
Q.push(初始状态); // 将初始状态入队
while (!Q.empty()) {
State u = Q.front(); // 取出队首
Q.pop();//出队
for (枚举所有可扩展状态) // 找到u的所有可达状态v
if (是合法的) // v需要满足某些条件,如未访问过、未在队内等
Q.push(v); // 入队(同时可能需要维护某些必要信息)
}
struct node //结构体用于保存每一状态信息
{
......
};
void bfs(){
queue<node> Q; // 定义存放结构体的队列Q
起点入队
标记起点
while(!Q.empty()) // 队非空
{
node u=Q.front(); //获取队首信息(结构体)
for(拓展接下来所有可能的状态)
{
得到并记录新的状态信息
判断状态是否合法
若合法
{
当前标记为已访问
Q.push(合法节点);//状态入队
判断是否到达目标
若满足,输出答案,return ;
}
}
Q.pop();//每次从队首把所有可能的状态走完,队首要出队
}
}
下面一种模板是用栈的形式来实现的:
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* struct TreeNode *left;
* struct TreeNode *right;
* };
*/
//深度优先
int minDepth(struct TreeNode* root)
{
if(!root) return 0;
if(!root->left && !root->right) return 1;
int min_dep = pow(2, sizeof(min_dep) * 8) - 1;
if(root->left)
{
int tmp = minDepth(root->left);
min_dep = tmp < min_dep ? tmp : min_dep;
}
if(root->right)
{
int tmp = minDepth(root->right);
min_dep = tmp < min_dep ? tmp : min_dep;
}
return min_dep + 1;
}
呵呵,有一天我做了一个梦,梦见了一种很奇怪的电梯。大楼的每一层楼都可以停电梯,而且第 i层楼( 1≤ i ≤ N )上有一个数字 K i K_{i} Ki(0 ≤ K i K_{i} Ki≤ N)。电梯只有四个按钮:开,关,上,下。上下的层数等于当前楼层上的那个数字。当然,如果不能满足要求,相应的按钮就会失灵。例如: 3, 3, 1, 2, 5 代表了 K i K_{i} Ki( K 1 K_{1} K1=3, K 2 K_{2} K2=3,……),从 1 楼开始。在 1 楼,按“上”可以到 4 楼,按“下”是不起作用的,因为没有 −2 楼。那么,从 A 楼到 B 楼至少要按几次按钮呢?
本题使用 STL 的 queue 实现队列。建立结构体数组存储扩展的结点。
让起点入队,然后在队列逐个扩展。每个点被扩展到时步数最小。
#include
using namespace std;
int n,a,b,s[205],visited[205];
struct node
{
int id,step;
};
int bfs(node start)
{
queue<node> q;
q.push(start);//将起始楼层入队
visited[start.id]=1;//标记楼层
while (!q.empty())
{
node front=q.front(),down,up;//获取现在所在楼层的状态
down.id=front.id-s[front.id];//当前楼层向下可到达的楼层
up.id=front.id+s[front.id];//当前楼层向上课到达的楼层
down.step=front.step+1;//步数加一
up.step=front.step+1;
if (front.id==b)//如果当前楼层为目标楼层,返回结果
{
return front.step;
}
if (down.id>=1 && visited[down.id]==0)//判断当前楼层向上是否是可达的
{
visited[down.id]=1;
q.push(down);//入队
}
if (up.id<=n && visited[up.id]==0)//与上相同
{
visited[up.id]=1;
q.push(up);
}
q.pop();//在遍历了所有可能后,将队首元素出队
}
return -1;
}
int main()
{
cin >> n >> a >> b;
for (int i = 1; i <= n; i++)
{
cin >> s[i];
}
node s;
s.id=a;
s.step=0;
cout << bfs(s);
return 0;
}
有一个 n × m 的棋盘,在某个点 (x, y) 上有一个马,要求你计算出马到达棋盘上任意一个点最少要走几步。
求 “ 最少 ” 步数,使用洪泛法。广度优先搜索使用队列实现。每次从队首取出元素,将该元素所能扩展到的结果插入队尾。这样即可保证,在同一层的其他元素均被取出之前,不会访问到下层的新元素。
本题的思路与上一题一样。仅是空间由一维变为二维,以及移动规则由给定的数字变为马步。
struct coord { //一个结构体存储x,y两个坐标
int x, y;
};
queue<coord> Q;//队列
int ans[maxn][maxn];//记录答案,-1表示未访问
int walk[8][2] = {{2, 1}, {1, 2}, {-1, 2}, {-2, 1},{-2, -1}, {-1, -2}, {1, -2}, {2, -1}}
coord tmp = {sx, sy};
Q.push(tmp); // 使起点入队扩展
ans[sx][sy] = 0;
while (!Q.empty()) { // 循环直到队列为空
coord u = Q.front(); // 拿出队首以扩展
int ux = u.x, uy = u.y;
Q.pop();
for (int k = 0; k < 8; k++) {
int x = ux + walk[k][0], y = uy + walk[k][1];
int d = ans[ux][uy];
if (x < 1 || x > n || y < 1 || y > m || ans[x][y] != -1)
continue; // 若坐标超过地图范围或者该点已被访问过则无需入队
ans[x][y] = d + 1; // 记录答案,是上一个点多走一步的结果。
coord tmp = {x, y};
Q.push(tmp);
}
}
// 复杂度是 O(mn)
爱与愁大神买完东西后,打算坐车离开中山路。现在爱与愁大神在 x 1 x_{1} x1, y 1 y_{1} y1处,车站在 x 2 x_{2} x2, y 2 y_{2} y2处。现在给出一个 n × n ( n ≤ 1000) 的地图,0 表示马路,1 表示店铺(不能从店铺穿过),爱与愁大神只能垂直或水平着在马路上行进。爱与愁大神为了节省时间,他要求最短到达目的地距离(每两个相邻坐标间距离为 1 )。你能帮他解决吗?
#include
using namespace std;
int a, b, c, d, n, vis[1005][1005];
int dc[] = {0, 0, -1, 1};
int dr[] = {-1, 1, 0, 0};
char s[1005][1005];
struct point
{
int c, r, step;
};
int bfs(point start)
{
queue<point> q;
q.push(start);
vis[start.r][start.c] = 1;
while (!q.empty())
{
point front = q.front(),p;
q.pop();
for (int i = 0; i < 4; i++)
{
p.r = front.r + dr[i];
p.c = front.c + dc[i];
p.step = front.step + 1;
if (p.r >= 0 && p.r <= n && p.c >= 0 && p.c <= n && s[p.r][p.c] == '0' && vis[p.r][p.c] == 0)
{
vis[p.r][p.c] = 1;
q.push(p);
}
if (p.r == c && p.c == d)
{
return p.step;
}
}
}
return 0;
}
int main()
{
cin >> n;
for (int i = 1; i <= n; i++)
{
for (int j = 1; j <= n; j++)
{
cin >> s[i][j];
}
}
cin >> a >> b >> c >> d;
point s;
s.r = a;
s.c = b;
s.step = 0;
cout << bfs(s);
return 0;
}