//这章必备的知识点:散列表,队列。
概念:
BFS是盲目型的搜索,展开所有的节点,直到找到目标节点为止。使用队列来保存已经遍历过的节点。
和深度优先搜索的区别:
- 概念上的区别:BFS是盲目的展开节点,就像是层次遍历方法一样,而DFS是从一个根节点走到一个叶节点,然后通过回溯没有走过的路径从而达到全部的遍历。
- 实现原理的区别:BFS是使用队列来完成存储,DFS是使用栈来完成存储,其实没有多大的区别就是方式不一样罢了。
实现代码
代码的实现十分简单易懂,就是用队列来保存每个根节点,然后判断根节点是否为目标节点,不是则使用递归来对根节点的左右节点进行遍历,以此类推。
代码:
class Node<E extends Comparable<E>> {
E value;
Node<E> left;
Node<E> right;
Node <E value> {
this.value=value;
Node<E> left=NULL;
Node<E> right=NULL;
}
}
public void BFS(Node<E> root) {
Queue<Node<E>> queue=new LinkedList<Node<E>>();
Node<E> curNode=NULL;
queue.offer(root);
while(!queue.empty()) {
curNode=queue.poll();
System.out.println(curNode.value);
if(curNode.left)
queue.offer(curNode.left);
if(curNode.right)
queue.offer(curNode.right);
}
}
这次我使用的是java来编译,很明显代码使用的遍历就是和层次遍历一样,所以BFS的搜索方法和层次遍历是一样的。
走迷宫的应用:
走迷宫
题目链接:http://acm.fzu.edu.cn/problem.php?pid=2285
说到BFS的最直观经典的应用就是走迷宫了,走迷宫的思想就是从起点开始,上下左右移动,走过的地方需要标记经过,直到走出为止。
代码:
#include<iostream>
#include<string.h>
#include<cstdio>
#include<set>
#include<memory.h>
#include<algorithm>
#include<vector>
#include<math.h>
#include<queue>
#define LL long long
using namespace std;
const LL maxn=1000000009;
struct Index //位置的结构体
{
int x,y;
}tmp1,tmp2;
char a[1010][1010]; //来存放迷宫
int vised[1010][1010]; //标记走过的位置
queue<Index> q; //队列,使用BFS必备的
int mt[4][2]={1,0,-1,0,0,1,0,-1}; //上下左右移动
int flag=false; //返回能不能走出迷宫
int main(){
int n;
cin>>n;
int strx,stry,endx,endy; //分别记录,起点的x,y坐标和终点的x,y
for(int i=0;i<n;i++) {
cin>>a[i];
}
for(int i=0;i<n;i++) {
for(int j=0;j<n;j++) {
vised[i][j]=0; //初始化
if(a[i][j]=='S') {
strx=i;
stry=j;
} else if (a[i][j]=='E') {
endx=i;
endy=j;
}
}
}
tmp1.x=strx;
tmp1.y=stry;
q.push(tmp1);
vised[tmp1.x][tmp1.y]=1;//起点标记1
while(!q.empty()) {
tmp1=q.front();
q.pop();
for(int i=0;i<4;i++) {
tmp2.x=tmp1.x+mt[i][0];
tmp2.y=tmp1.y+mt[i][1];
if(tmp2.x>=0 && tmp2.x<n && tmp2.y>=0 && tmp2.y<n && !vised[tmp2.x][tmp2.y] && a[tmp2.x][tmp2.y]!='#') {
vised[tmp2.x][tmp2.y]=vised[tmp1.x][tmp1.y]+1; //当可以上或下或左或右移动时,我们把步数加一
q.push(tmp2);
}
}
if(vised[endx][endy]) {
flag=true;
break;
}
}
if(flag) {
cout<<vised[endx][endy]-1<<endl;
} else {
cout<<-1<<endl;
}
return 0;
}
解析:代码需要注意的细节:
- 我们要在BFS操作前就把起点标记为已经走过
- 实际vised[endx][endy]的值包括了终点这个点,所以输出的时候我们要减去这个点,就等于走的步数了
- 在如何上下左右移动,就是使用for循环来实现,在记录上下左右的数组也需要理解
疑惑:
- 这个真的是最短的步数嘛
- 为什么定义char类型就不需要双重循环输入,是系统自动定义成字符串了嘛
移动
题目链接:http://acm.hrbust.edu.cn/index.php?m=ProblemSet&a=showProblem&problem_id=1181
这题其实就是走迷宫的另一个例子,这个是考察移动的用法
#include<iostream>
#include<string.h>
#include<cstdio>
#include<set>
#include<memory.h>
#include<algorithm>
#include<vector>
#include<math.h>
#include<queue>
#define LL long long
using namespace std;
const LL maxn=305;
struct Index
{
int x,y;
}tmp1,tmp2;
int vised[maxn][maxn];
int mt[8][2]={1,2,2,1,2,-1,1,-2,-1,-2,-2,-1,-2,1,-1,2}; //移动的规定位置,就像是象棋中马的移动方式
int strx,stry,endx,endy;
queue<Index> q;
int main(){
cin>>strx>>stry>>endx>>endy;
vised[strx][stry]=1;
tmp1.x=strx;
tmp1.y=stry;
q.push(tmp1);
while(!q.empty()) {
tmp1=q.front();
q.pop();
for(int i=0;i<8;i++) {
tmp2.x=tmp1.x+mt[i][0];
tmp2.y=tmp1.y+mt[i][1];
if(tmp2.x>=0 && tmp2.x<=300 && tmp2.y>=0 && tmp2.y<=300 && !vised[tmp2.x][tmp2.y]) {
vised[tmp2.x][tmp2.y]=vised[tmp1.x][tmp1.y]+1;
q.push(tmp2);
}
}
if(vised[endx][endy]) {
cout<<vised[endx][endy]-1;
break;
}
}
return 0;
}
这题和上题差不多,就是移动的方式不同罢了,而且这题没有无法的移动到的位置,就好像在象棋中,有马不能移动到的位置嘛。
实例3:
链接:http://poj.org/problem?id=3984
题目的要求迷宫,但是要求出经过的路径,这样我们就要存储经过的路径
代码:
#include<iostream>
#include<string.h>
#include<cstdio>
#include<set>
#include<memory.h>
#include<algorithm>
#include<vector>
#include<math.h>
#include<queue>
#define LL long long
using namespace std;
const LL maxn=6;
struct Index
{
int x,y;
}tmp1,tmp2;
int vised[maxn][maxn],a[maxn][maxn];
int mt[4][2]={1,0,-1,0,0,1,0,-1};
Index pre[10][10];
Index res[30];
queue<Index> q;
int main(){
for(int i=0;i<5;i++) {
for(int j=0;j<5;j++) {
cin>>a[i][j];
}
}
tmp1.x=0;
tmp1.y=0;
vised[0][0]=1;
q.push(tmp1);
while(!q.empty()) {
tmp1=q.front();
q.pop();
for(int i=0;i<4;i++) {
tmp2.x=tmp1.x+mt[i][0];
tmp2.y=tmp1.y+mt[i][1];
if(tmp2.x<5 && tmp2.x>=0 && tmp2.y<5 && tmp2.y>=0 && !vised[tmp2.x][tmp2.y] && a[tmp2.x][tmp2.y]==0) {
vised[tmp2.x][tmp2.y]=vised[tmp1.x][tmp1.y]+1;
q.push(tmp2);
pre[tmp2.x][tmp2.y].x=tmp1.x;
pre[tmp2.x][tmp2.y].y=tmp1.y;
}
}
if(vised[4][4]) {
break;
}
}
int lastx=4,lasty=4,num=0;
int x,y;
while(lastx || lasty) {
res[num].x=lastx;
res[num++].y=lasty;
x=lastx;
y=lasty;
lastx=pre[x][y].x;
lasty=pre[x][y].y;
}
cout<<"(0,0)"<<endl;
for(int i=num-1;i>=0;i--) {
cout<<"("<<res[i].x<<","<<res[i].y<<")"<<endl;
}
return 0;
}
解析:我们使用pre来记录在tmp2.x,tmp2.y的位置的前一个点为tmp1.x和tmp1.y,我们在使用res来存储数据,输出要手动输出(0,0)这个点和倒叙输出。
树的应用
103. 二叉树的锯齿形层次遍历(leetcode)
我们在用层次遍历的过程中,也要把深度给记录下来,以便在偶数的深度进行翻转;
我的思路:我一开始想到的的是使用vector<int>来存储每个层次的数据,但是这个很难边进行层次遍历,边记录深度。
网上的思路:使用depth来表示深度,在递归时再把depth+1,这样就可以记录深度也不用使用到vectot<int>了
代码:
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode(int x) : val(x), left(NULL), right(NULL) {}
* };
*/
class Solution {
public:
vector<vector<int>> zigzagLevelOrder(TreeNode* root) {
vector<vector<int>> ivec;
inorder(root,ivec,0);
for(int i=0;i<ivec.size();i++) {
if(i%2!=0) {
reverse(ivec[i].begin(),ivec[i].end());
}
}
return ivec;
}
void inorder(TreeNode *root,vector<vector<int>> &ivec,int depth) {
if(!root) {
return ;
}
if(depth>=ivec.size()) { //这个代码十分重要,很容易忘记,没有就会报错空指针
ivec.push_back(vector<int>{ });
}
ivec[depth].push_back(root->val);
inorder(root->left,ivec,depth+1);
inorder(root->right,ivec,depth+1);
}
};
107. 二叉树的层次遍历 II
这题是层次遍历的倒序输出,就是交换以下位置即可。
代码:
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode(int x) : val(x), left(NULL), right(NULL) {}
* };
*/
class Solution {
public:
vector<vector<int>> levelOrderBottom(TreeNode* root) {
vector<vector<int>> ivec;
if(!root) {
//ivec.push_back(vector<int> {});
return ivec;
}
inorder(root,ivec,0);
for(int i=0,j=ivec.size()-1;i!=j&&i<j;i++,j--) {
swap(ivec[i],ivec[j]);
}
return ivec;
}
void inorder(TreeNode *root,vector<vector<int>> &ivec,int depth) {
if(!root) {
return;
}
if(depth>=ivec.size()) {
ivec.push_back(vector<int> {});
}
ivec[depth].push_back(root->val);
inorder(root->left,ivec,depth+1);
inorder(root->right,ivec,depth+1);
}
};
string的广度搜索
127. 单词接龙
我的思路:这题其实和走迷宫差不多,只要找到只有一个字母不同的字符串且没有被访问过,就加入队列中,最后输出走过的步数即可,我使用的是map来存储string(单词)和int(步数)的关系
代码:
class Solution {
public:
int ladderLength(string beginWord, string endWord, vector<string>& wordList) {
int res=0,k,num=0;
string tmp;
for(k=0;k<wordList.size();k++) {
if(endWord==wordList[k]) {
break;
}
}
if(k>wordList.size()-1) {
return res;
}
map<string,int> simap,vised;
queue<string> q;
for(int i=0;i<wordList.size();i++) {
simap[wordList[i]]=0;
}
simap[beginWord]=1;
q.push(beginWord);
while(!q.empty()) {
tmp=q.front();
q.pop();
for(int i=0;i<wordList.size();i++) {
for(int j=0;j<wordList[i].size();j++) {
if(tmp[j]!=wordList[i][j]) {
num++;
}
}
if(num==1 && !simap[wordList[i]]) {
simap[wordList[i]]=simap[tmp]+1;
q.push(wordList[i]);
}
num=0;
}
if(simap[endWord]) {
break;
}
}
if(!simap[endWord]) {
return 0;
} else {
return simap[endWord];
}
}
};
我这个方法虽然也是可以做,但是很明显,效率很低,运行时间为2443ms。
网上代码:
class Solution {
public:
int ladderLength(string beginWord, string endWord, vector<string>& wordList) {
unordered_set<string> wordDirct(wordList.begin(),wordList.end());
if(wordDirct.find(endWord)==wordDirct.end()) { //相等于end,证明没有找到
return 0;
}
unordered_set<string> beginSet{beginWord};
unordered_set<string> endSet{endWord};
int res=1;
for(;!beginSet.empty();) {
unordered_set<string> tmp;
res++;
for(auto s:beginSet) { //在字典中删除beginSet的单词,至于为什么嘛,不要重复寻找??
wordDirct.erase(s);
}
for(auto s:beginSet) {
for(int i=0;i<s.size();i++) {
string str=s;
for(char a='a';a<='z';a++) {//每次只改动一个字母
str[i]=a;
if(wordDirct.find(str)==wordDirct.end()) {
continue;
}
if(endSet.find(str)!=endSet.end()) {
return res;
}
tmp.insert(str);
}
}
}
//这个搜索下一个单词的方式很特别
if(tmp.size()<endSet.size()) {
beginSet=tmp;//当tmp值为0时
} else {
beginSet=endSet; //把endSet赋值给beginSet
endSet=tmp; //把tmp值赋值给endSet
}
//这样最后endSet还是回溯到原来的值,当回溯到最后的值时就是endSet和tmp一样了
}
return 0;
}
};
这个使用到了C++11新增的unordered_set方法,这个方法是基于散列表所建立的,所以效率很快。
解析:这个代码虽然效率很快,但是很难想到:
- 要熟悉使用C++11新功能,unordered_set
- 每次只改一个字母的方法
- 在寻找下一个单词的方法
126. 单词接龙 II
网上代码:
class Solution {
public:
vector<vector<string>> findLadders(string beginWord, string endWord, vector<string>& wordList) {
unordered_set<string> wordDirct(wordList.begin(),wordList.end());
vector<vector<string>> ans;
queue<vector<string>> paths;
//wordDirct.insert(endWord);
paths.push({beginWord});
int level = 1;
int minLevel = INT_MAX;
//"visited" records all the visited nodes on this level
//these words will never be visited again after this level
//and should be removed from wordList. This is guaranteed
// by the shortest path.
unordered_set<string> visited;
while (!paths.empty()) {
vector<string> path = paths.front();
paths.pop();
if (path.size() > level) {
//reach a new level
for (string w : visited) wordDirct.erase(w);
visited.clear();
if (path.size() > minLevel)
break;
else
level = path.size();
}
string last = path.back();
//find next words in wordList by changing
//each element from 'a' to 'z'
for (int i = 0; i < last.size(); ++i) {
string news = last;
for (char c = 'a'; c <= 'z'; ++c) {
news[i] = c;
if (wordDirct.find(news) != wordDirct.end()) {
//next word is in wordList
//append this word to path
//path will be reused in the loop
//so copy a new path
vector<string> newpath = path;
newpath.push_back(news);
visited.insert(news);
if (news == endWord) {
minLevel = level;
ans.push_back(newpath);
}
else
paths.push(newpath);
}
}
}
}
return ans;
}
};
这题是上一题的升级版,主要是需要输出路径。
待续 -3.20 22:16