BFS与DFS算法

BFS

基本概念

首先,我们先了解一下BFS,BFS又称广度优先搜索,一般都是用于解决一些图,树的遍历问题。

算法思路

其实广度优先搜索就类似与二叉树的层序遍历过程,需要借助C++中STL里面的queue队列容器来实现这个过程。它其实就是一种分层查找的过程,每次向前走一步,都会去访问一批可以访问的节点,不会存在DFS里面的回退情况。
大致的算法思路是这样:

  1. 首先,需要定义一个是否判重的数组visited[ ]以及一个队列q
  2. 初始节点最先放在队列中去,并标记判重。
  3. 执行循环,循环条件为队列是否为空
  4. 取出队列最上面的一个节点,并删除这个节点。
  5. 如果遇到目标节点,直接输出,退出循环,否则的话,继续循环。
  6. 分别遍历可以访问的节点,并判重标记。

算法实现

在这里,我们就举一个经典的例题来具体讲解一下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则为目标节点。用队列来处理这个扩散过程清晰易懂。

  1.  3进队,当前队列是{3}
    
  2. 3出队,2,4,6进队,当前队列是{2,4,6}
    
  3. 2出队,1进队,当前队列是{4,6,1}
    
  4. 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也可以用栈这种数据结构来实现,栈和递归在算法思想上是一致的。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值