BFS
基本概念
首先,我们先了解一下BFS,BFS又称广度优先搜索,一般都是用于解决一些图,树的遍历问题。
算法思路
其实广度优先搜索就类似与二叉树的层序遍历过程,需要借助C++中STL里面的queue队列容器来实现这个过程。它其实就是一种分层查找的过程,每次向前走一步,都会去访问一批可以访问的节点,不会存在DFS里面的回退情况。
大致的算法思路是这样:
- 首先,需要定义一个是否判重的数组visited[ ]以及一个队列q。
- 把初始节点最先放在队列中去,并标记判重。
- 执行循环,循环条件为队列是否为空。
- 取出队列最上面的一个节点,并删除这个节点。
- 如果遇到目标节点,直接输出,退出循环,否则的话,继续循环。
- 分别遍历可以访问的节点,并判重标记。
算法实现
在这里,我们就举一个经典的例题来具体讲解一下BFS的具体实现过程。
我们来看一下POJ的3278题。
链接 : POJ 3278
这道题的大意其实就是农夫想知道一头牛的位置,农夫和牛都在数轴上,农夫起始点为N(0<=N<=100000),牛位于点K(0<=K<=100000),农夫有两种移动方式。
1.从x移动到x+1或者x-1,花费一分钟。
2.从x移动到2*x,花费一分钟。
求解找到牛所花费的最短时间。
对于这道题,可以看出来它的目的就是找出最短时间,这其实就类似与咱们的走迷宫模型,这个其实就是BFS的经典算法模型的应用,我们来分析一下这道题的算法实现思路。
我们以N=3,K=5为例来分析,那么N就为初始节点,而K则为目标节点。用队列来处理这个扩散过程清晰易懂。
-
3进队,当前队列是{3}
-
3出队,2,4,6进队,当前队列是{2,4,6}
-
2出队,1进队,当前队列是{4,6,1}
-
4出队,5进队,找到目标节点,退出循环。
对于这道题,我们想要实时记录到达每个节点所需要的时间,我们需要去自定义一个结构体,可以记录该节点的位置以及所需时间。我们直接上AC代码(C++)。
#include <iostream>
#include <cstring>
#include <queue>
using namespace std;
int N,K;
const int MAXN = 100000;
int visited[MAXN+10]; //判重标记,visited[i] = true表示i已经扩展过
struct Step{
int x; //位置
int steps; //到达x所需的步数
Step(int xx,int s):x(xx),steps(s) { }
};
queue<Step> q; //队列,即Open表
int main() {
cin >> N >> K;
memset(visited,0,sizeof(visited));
q.push(Step(N,0));
visited[N] = 1;
while(!q.empty()) {
Step s = q.front();
if( s.x == K ) { //找到目标
cout << s.steps <<endl;
return 0;
}
else {
if( s.x - 1 >= 0 && !visited[s.x-1] ) {
q.push(Step(s.x-1,s.steps+1));
visited[s.x-1] = 1;
}
if( s.x + 1 <= MAXN && !visited[s.x+1] ) {
q.push(Step(s.x+1,s.steps+1));
visited[s.x+1] = 1;
}
if( s.x * 2 <= MAXN &&!visited[s.x*2] ) {
q.push(Step(s.x*2,s.steps+1));
visited[s.x*2] = 1;
}
q.pop();
}
}
return 0;
}
(注意: 这里我没有用C++的万能头文件#include<bits.stdc++.h>的原因是:POJ网站本身是不支持这个头文件的,但是在编译器里写可以的,只不过在POJ提交的时候要修改一下哈)
我这里推荐了几道经典的BFS算法题,可以去写一下。
链接1:HDU 1312
链接2:POJ 3984
DFS
基本概念
DFS又称深度优先搜索,是沿着数的深度去遍历树的节点,从一个根节点出发,遍历所有的子节点,从而得出最优解,我们其实就是用递归来实现的。
算法思路
我们在概念中提到,用递归列举出所有的路径,这种方法显然是不太可行的,可能会因为数量太大而超时,由于很多的子节点是不符合条件的,所以在递归的时候要学会“撤退”,这种方法就叫做回溯,在回溯中用于减少子节点扩展的函数就叫做剪枝函数。
其实我们在大部分的DFS题目中,都会用到回溯的思想,难度主要在于如何在扩展子节点的同时,构造停止递归并且返回的条件。
算法实现
一提到DFS,就想到了经典的N皇后问题,N皇后问题就是经典的回溯与剪枝的应用。
我们先来看一个简化版的N皇后问题,给出N,求出对应的解决方案个数(这里我们先不要求求出具体方案的图案)
题目链接:HDU 2553
我们首先要明确子节点符合的条件,已经放好的皇后为(i,j),新皇后节点的坐标为(r,c):
(1) 横向 i != r
(2) 纵向 j != c
(3) 斜对角 |i-r| != |j-c|
则这道题的AC代码如下(带有注释):
#include<bits/stdc++.h>
using namespace std;
int n,total=0;
int col[12]={0};
bool check(int c,int r){
for(int i=0;i<r;i++){
if(col[r]=c||(abs(col[i]-c)==abs(i-r))){
return false;
}
}
return true;
}
void DFS(int r){
if(r==n){ // 所有皇后都放好了,递归返回
total++; // 记录棋局的个数
return;
}
for(int c=0;c<n;c++){
if(check(c,r)){ // 检查是否合法
col[r]=c; // 在第r行c列放上皇后
DFS(r+1); // 继续放下一行
}
}
}
int main(){
int ans[12]={0};
for(n=0;n<=10;n++){ // 算出每种情况的答案,先打表,不然会超时
memset(col,0,sizeof(col)); // 清空,准备下一个问题
total=0;
DFS(0);
ans[n]=total; // 打表
}
while(scanf("%d",&n)&&n){
printf("%d\n",ans[n]);
}
return 0;
}
那如果要具体输出方案的格式呢?我们来看一下这道进阶版的N皇后问题。
链接:力扣51 N皇后
这也是经典的回溯算法,要在适当的地方剪枝,其实思路和前面的题目相同,无非就是记录一下它的路径。
我们再来举一个经典的回溯的例子来加深对DFS的理解。
链接:力扣22 括号生成
class Solution {
public:
vector<string> res;
int N;
vector<string> generateParenthesis(int n) {
N = n;
string s = "";
backtrack(1, 0, s + "(");
return res;
}
void backtrack(int left, int right, const string &s) {
if(left + right == 2 * N) {
res.push_back(s);
return;
}
if(left < N && left > right) {
backtrack(left + 1, right, s + "(");
backtrack(left, right + 1, s + ")");
} else if(left == N) {
backtrack(left, right + 1, s + ")");
} else if(left == right) {
backtrack(left + 1, right, s + "(");
}
}
};
我们来看一下这道题的AC代码,这里我们就运用了经典的回溯算法。
对于backtrack函数,第一个参数代表左括号的个数,第二个参数代表右括号的个数,第三个参数代表实时的字符串。
那现在我们就要讨论什么时候加左括号或者右括号啊
第一种:可以放置’(‘或者’)'
条件:当 ‘(’ 的数量大于 ‘)’ 的数量 并且 ‘(’ 的数量小于 n 时,即 if (left < N && left > right)。
可以思考 s = ( ( ) _ _ _ 和 s = ( ( ( ) _ _这两种情况。
第二种:只能放置’('
条件:当 ‘(’ 的数量等于 ‘)’ 的数量时,我们只能向后添加 ‘(’ 。
可以思考 s = ( ( ) ) _ _ 和 s = ( ) ( ) _ _这两种情况。
第三种:只能放置’)'
条件:当 ‘(’ 的数量等于 n 时,我们只能向后添加 ‘)’ 。
可以思考 s = ( ( ( _ _ _ 和 s = ( ) ( ( _ _这两种情况。
我们回溯的终止条件:
回溯函数中,left 和 right 代表的是 s 中左右括号的数量。
当 left + right == 2 * N 时,回溯结束。
总结
在具体编程的时候,一般用队列这种数据结构来具体实现BFS,甚至可以说“BFS=队列”。也可以说“DFS=递归”,毕竟用递归实现DFS是最普遍的。当然,DFS也可以用栈这种数据结构来实现,栈和递归在算法思想上是一致的。