赞
踩
DFS就是回溯,每次先搜索到底,然后再回溯搜索其他路径
优点:效率高,时间复杂度低
BFS把当前能做的所有选择都选一遍,先选当前的选择,选完了再往后推进
优点:如果所有路径权重一样,那么能够选出最短路
有一种问题是:
让你在一幅「图」中找到从起点 start
到终点 target
的最近距离,这个例子听起来很枯燥,但是 BFS 算法问题其实都是在干这个事儿
其实在二叉树中就已经提到过bfs和dfs,dfs就是前中后序遍历,而bfs就是二叉树的层序遍历(只是注意二叉树没有环,所以不需要用visited数组来判断访问过的点)
其实bfs比dfs更好理解,因为不是递归,不过就是写起麻烦且效率低:
整体框架如下:
- int BFS(Node start, Node target) {
- queue<Node> q;
- set<Node> visited;
-
- q.push(start);
- visited.insert(start);
-
- while (!q.empty()) {
- int size = q.size();
- for (int i = 0; i < size; i++) {
- Node cur = q.front();
- q.pop();
- if (cur == target)
- return step;
- for (Node x : cur.adj()) {
- if (visited.find(x) == visited.end()) {
- q.push(x);
- visited.insert(x);
- }
- }
- }
- }
- // 如果走到这里,说明在图中没有找到目标节点
- }
时间复杂度:
为O(V+E),其中V为顶点数,E为边数,需要访问所有的顶点和边才能完成遍历
空间复杂度:
在最坏情况下,即当图为完全二叉树时,BFS算法需要存储的元素数量达到O(V)级别,因此也是O(V)。
主体思想为:
1:先用一个容器(用任何容器都可以,不同容器对应不同的出入顺序,但是都能一个进一个出,只是出的顺序是顺时针还是逆时针的区别,这一点不影响代码的整体逻辑,这里选用的是使用比较广泛的队列,其实栈和数组也没问题)
2:先往队列中放入一个元素保证队列不为空,由于第一个点可以类似于根节点,所以当第一个点放入后第一层就满了(树里面的概念,对于图问题会意即可),此时得到本层的size
3:在for循环中遍历本层的所有节点的所有孩子(邻居),记得要把本层节点弹出,当遇到visited没有访问过的就进行操作,并记得将节点也放入队列中
4:由于遍历了本层的所有节点的孩子(邻居),且本层节点全部弹出,孩子全部加入到队列中,所以第一个for循环结束后q.size()仍然是本层节点数目
我们需要做这样一件事,通过start对当前位置的四个方向进行搜索
到达边界即终止,如果没有障碍,如图:
如果有障碍,如图:
具体代码如下(没有障碍版):
- int dir[4][2] = {0, 1, 1, 0, -1, 0, 0, -1}; // 表示四个方向
- // grid 是地图,也就是一个二维数组
- // visited标记访问过的节点,不要重复访问
- // x,y 表示开始搜索节点的下标
- void bfs(vector<vector<char>>& grid, vector<vector<bool>>& visited, int x, int y) {
- queue<pair<int, int>> que; // 定义队列
- que.push({x, y}); // 起始节点加入队列
- visited[x][y] = true; // 只要加入队列,立刻标记为访问过的节点
- while(!que.empty()) { // 开始遍历队列里的元素
- pair<int ,int> cur = que.front(); que.pop(); // 从队列取元素
- int curx = cur.first;
- int cury = cur.second; // 当前节点坐标
- for (int i = 0; i < 4; i++) { // 开始想当前节点的四个方向左右上下去遍历
- int nextx = curx + dir[i][0];
- int nexty = cury + dir[i][1]; // 获取周边四个方向的坐标
- if (nextx < 0 || nextx >= grid.size() || nexty < 0 || nexty >= grid[0].size()) continue; // 坐标越界了,直接跳过
- if (!visited[nextx][nexty]) { // 如果节点没被访问过
- que.push({nextx, nexty}); // 队列添加该节点为下一轮要遍历的节点
- visited[nextx][nexty] = true; // 只要加入队列立刻标记,避免重复访问
- }
- }
- }
-
- }
可以是看出,在实际使用中,出了之前说的一些注意点,还要注意我们实现选择每个节点的多个孩子(主要是可以放入循环中自动多次实现)
上述代码中,我们使用dir来定义一个数组实现
此题在二叉树章节做过,不多赘述,具体代码如下:
- class Solution {
- public:
- vector<vector<int>> levelOrder(TreeNode* root) {
- queue<TreeNode*> que;
- vector<vector<int>> res;
- TreeNode* cur = root;
- if(root == nullptr) return res;
- que.push(cur);
- while(!que.empty()){
- vector<int> res1;
- int size = que.size();
- while(size--){
- //处理当前节点
- cur = que.front();
- que.pop();
- res1.push_back(cur->val);
- //使用队列进行遍历
- if(cur->left)que.push(cur->left);
- if(cur->right)que.push(cur->right);
- }
- res.push_back(res1);
- }
- return res;
- }
- };
此处引入是为方便理解bfs的框架:
可以看到此代码第一眼看上去还是和给出框架有些差别,但是仔细分析发现,其实和给出的框架如出一辙
1:给出框架内有两个for循环,用于遍历所有节点的孩子,这里只有一层循环,因为每个二叉树节点只有两个孩子,这一句便是第二层循环的简化
- if(cur->left)que.push(cur->left);
- if(cur->right)que.push(cur->right);
2:由于二叉树没有环,所以不需要使用visited数组进行去重判断,然后对比其余操作也都几乎一样
这一题其实才体现bfs的思想,因为集齐众多条件
1:首先我们分析如何转换为bfs问题,那么我们要先确定每一次拨动锁盘能够进行的选择,我们每次可以拨动四个位置总共八种选择。其次我们只需要把当前的锁的状态看作是1.3中的一个点即可
2:我们要如何做出选择,也就是定义八种转动锁盘的操作
我们其实只需要定义锁盘每一位向上和向下拨动即可,八种选择就是每一位执行这两种操作而已,写出如下代码:
- string up(string s,int j){
- if(s[j]!='9')s[j]+=1;
- else s[j]='0';
- return s;
- }
- string down(string s,int j){
- if(s[j]!='0')s[j]-=1;
- else s[j]='9';
- return s;
- }
3:我们如何排除死亡数字,其实这里的死亡数字就是1.3中的障碍坐标,只是由于每一数字都是一个字符串,我们不能直接定位,需要用一个数据结构来查询是否遇到了障碍,存放障碍其实用什么都行,但是查询时使用比如数组会非常麻烦,那么我们考虑使用set(因为不需要返回查询结果的下标,不使用map)
同时遍历过的位置其实也是不能再次遍历的,所以和死亡数组是一个性质,我们直接放在一起即可
4:如何判断最短选择次数,因为每条路径都是相对权重(长度),所以bfs第一次访问到的target时就是最短路径,问题时在何时进行计数,一开始我错误的将sum++放在了取cur的时候
- string cur = q.front();q.pop();
- sum+=1;
- if(cur == target)return sum;
但其实这样严重重复了,并不是一共取了多少个节点路径就是多少,我们路径应该实在每一圈结束后更新,也就是在第一个大的for循环后(可以理解位二叉树的每一层)
5:其余部分参考走格子问题和二叉树问题,整体代码如下:
- class Solution {
- public:
- string up(string s,int j){
- if(s[j]!='9')s[j]+=1;
- else s[j]='0';
- return s;
- }
- string down(string s,int j){
- if(s[j]!='0')s[j]-=1;
- else s[j]='9';
- return s;
- }
- int openLock(vector<string>& deadends, string target) {
- int sum = 0;
- queue<string> q;
- unordered_set<string> visited;
- for(int i=0;i<deadends.size();i++)visited.insert(deadends[i]);
- q.push("0000");
- if(visited.find("0000")!=visited.end())return -1;
- visited.insert("0000");
- while(!q.empty()){
- int size = q.size();
- for(int i = 0;i < size;i++){
-
- for(int j = 0;j<4;j++){
- string up_str = up(cur,j);
- string down_str = down(cur,j);
- if(visited.find(up_str)==visited.end()){
- visited.insert(up_str);
- q.push(up_str);
- }
- if(visited.find(down_str)==visited.end()){
- visited.insert(down_str);
- q.push(down_str);
- }
- }
- }
- sum+=1;
- }
- return -1;
- }
- };
传统的 BFS 框架就是从起点开始向四周扩散,遇到终点时停止;而双向 BFS 则是从起点和终点同时开始扩散,当两边有交集的时候停止。
为什么这样能够能够提升效率呢?其实从 Big O 表示法分析算法复杂度的话,它俩的最坏复杂度都是 O(N)
,但是实际上双向 BFS 确实会快一些,我给你画两张图看一眼就明白了:
从两端收集路径,碰头后汇总为总路径
同时我们可以发现双向队列优化的一个必要条件:
必须知道终点位置
代码实现是通过set存放节点,当两个set有交集碰头时就结束搜索
具体如下:
-
- #include <vector>
- #include <string>
- #include <unordered_set>
- #include <queue>
- using namespace std;
-
- class Solution {
- public:
- string up(string s, int j) {
- if (s[j] != '9') s[j] += 1;
- else s[j] = '0';
- return s;
- }
-
- string down(string s, int j) {
- if (s[j] != '0') s[j] -= 1;
- else s[j] = '9';
- return s;
- }
-
- int openLock(vector<string>& deadends, string target) {
- unordered_set<string> dead(deadends.begin(), deadends.end());
- unordered_set<string> begin, end, visited;
- begin.insert("0000");
- end.insert(target);
-
- if (dead.find("0000") != dead.end() || dead.find(target) != dead.end()) return -1;
- int sum = 0;
- while (!begin.empty() && !end.empty()) {
- // 交换begin和end,总是扩展较小的集合
- if (begin.size() > end.size()) swap(begin, end);
-
- unordered_set<string> temp;
- for (auto it = begin.begin(); it != begin.end(); it++) {
- string cur = *it;
- if (end.find(cur) != end.end()) return sum;
- visited.insert(cur);
-
- for (int j = 0; j < 4; j++) {
- string up_str = up(cur, j);
- string down_str = down(cur, j);
-
- if (visited.find(up_str) == visited.end() && dead.find(up_str) == dead.end()) {
- temp.insert(up_str);
- }
- if (visited.find(down_str) == visited.end() && dead.find(down_str) == dead.end()) {
- temp.insert(down_str);
- }
- }
- }
- swap(begin, temp);
- sum++;
- }
- return -1;
- }
- };
我们从起点和终点两个位置,分别建立一个bfs,在同一个while中,进行交替更新,这样就能同时从两个点进行搜索了
如何进行更新?
代码中并没有显式的对begin和end的更新,但是我们声明了一个中间变量temp,类似层序遍历,每次temp搜集当前的最优set(begin)中的所有可选择点,然后通过swap的方式和begin进行交换,将数据存储在begin中
如果begin不是最优的,就会交换赋值给end
那么何时停止呢?
因为set中会存放接下来要检索的k个节点,那么如果两个set有交集,就代表两次搜索相交了,这时可以停下了
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。