前言

深度遍历和广度遍历在算法占比很大,主要是解决图的问题(树也是图的一种)

一、深度优先遍历

1、深度遍历(DFS)解决什么问题

DFS解决的是连通性的问题,即给定两⼀个起始点(或某种起始状态)和⼀个终点(或某种最终状态),判断是否有⼀条路径能从起点连接到终点。很多情况下,连通的路径有很多条,只需要找出⼀条即可,DFS 只关⼼路径存在与否,不在乎其⻓短。
所以dfs一般都是找路径的

2、算法思想:

图的深度优先搜索(Depth First Search),和树的先序遍历比较类似。
它的思想:假设初始状态是图中所有顶点均未被访问,则从某个顶点v出发,首先访问该顶点,然后依次从它的各个未被访问的邻接点出发深度优先搜索遍历图,直至图中所有和v有路径相通的顶点都被访问到。 若此时尚有其他顶点未被访问到,则另选一个未被访问的顶点作起始点,重复上述过程,直至图中所有顶点都被访问到为止。
显然,深度优先搜索是一个递归的过程
深度优先遍历特点是,选定一个出发点后进行遍历,能前进则前进,若不能前进,回退一步再前进,或再回退一步后继续前进。依此重复,直到所有与选定点相通的所有顶点都被遍历。

3、例题讲解:

  1. 给定⼀个⼆维矩阵代表⼀个迷宫
  2. 迷宫⾥⾯有通道,也有墙壁,有墙壁的地⽅不能通过
  3. 通道由数字0表示,⽽墙壁由-1表示
  4. 现在问能不能从A点⾛到B点?

    核心代码:
boolean dfs(int maze[][], int x, int y) {##判断是否抵达了⽬的地B,是则⽴即返回if (x == B[0] && y == B[1]) {return true;} ##标记当前点已经被访问过了maze[x][y] = -1;##在规定的四个⽅向上进⾏尝试for (int d = 0; d < 4; d++) {int i = x + dx[d], j = y + dy[d];##如果有⼀条路径被找到了,则返回trueif (isSafe(maze, i, j) && dfs(maze, i, j)) { return true; } } ##尝试了所有可能还没找到B,则返回falsereturn false;
}

4、实站经典:

79. 单词搜索
给定一个二维网格和一个单词,找出该单词是否存在于网格中。
单词必须按照字母顺序,通过相邻的单元格内的字母构成,其中“相邻”单元格是那些水平相邻或垂直相邻的单元格。同一个单元格内的字母不允许被重复使用。
示例:

board =
[['A','B','C','E'],['S','F','C','S'],['A','D','E','E']
]
给定 word = "ABCCED", 返回 true
给定 word = "SEE", 返回 true
给定 word = "ABCB", 返回 false

思路:
设函数check(i,j,k) 判断以网格的 (i, j)(i,j) 位置出发,能否搜索到单词 word[k…],其中word[k…] 表示字符串 word 从第 k 个字符开始的后缀子串。如果能搜索到,则返回 true,反之返回 false。函数check(i,j,k) 的执行步骤如下:
如果 board[i][j] =s[k]当前字符不匹配,直接返回 false。
如果当前已经访问到字符串的末尾,且对应字符依然匹配,此时直接返回true。
否则,遍历当前位置的所有相邻位置。如果从某个相邻位置出发,能够搜索到子串 word[k+1…],则返回 \texttt{true}true,否则返回 false。
这样,我们对每一个位置 (i,j)(i,j) 都调用函数check(i,j,0) 进行检查:只要有一处返回 true,就说明网格中能够找到相应的单词,否则说明不能找到。
为了防止重复遍历相同的位置,需要额外维护一个与 board 等大的 visited 数组,用于标识每个位置是否被访问过。每次遍历相邻位置时,需要跳过已经被访问的位置。
代码:

class Solution {public boolean exist(char[][] board, String word) {if(board==null || board.length==0 || board[0].length==0) return false;int m=board.length;int n=board[0].length;boolean[][] visited=new boolean[m][n];for(int i=0;i<m;i++){for(int j=0;j<n;j++){if(dfs(board,word,i,j,0,visited)){return true;}}}return false;}private boolean dfs(char[][] board, String word, int i, int j, int start, boolean[][] visited){//首先确立剪枝条件//注意下标的特点不能够是word.length()-1if(start==word.length()){return true;}if(i<0 || i>=board.length || j<0 || j>=board[0].length || word.charAt(start)!=board[i][j] || visited[i][j] ){return false;}visited[i][j]=true;if( dfs(board,word,i,j+1,start+1,visited) || dfs(board,word,i,j-1,start+1,visited) || dfs(board,word,i-1,j,start+1,visited) || dfs(board,word,i+1,j,start+1,visited)){return true;}else{//回溯visited[i][j]=false;return false;}}}

200. 岛屿数量
给你一个由 ‘1’(陆地)和 ‘0’(水)组成的的二维网格,请你计算网格中岛屿的数量。
岛屿总是被水包围,并且每座岛屿只能由水平方向和/或竖直方向上相邻的陆地连接形成。
此外,你可以假设该网格的四条边均被水包围。
示例 1:

输入:grid = [["1","1","1","1","0"],["1","1","0","1","0"],["1","1","0","0","0"],["0","0","0","0","0"]
]
输出:1

思想:
我们可以将二维网格看成一个无向图,竖直或水平相邻的 1 之间有边相连。
为了求出岛屿的数量,我们可以扫描整个二维网格。如果一个位置为 1,则以其为起始节点开始进行深度优先搜索。在深度优先搜索的过程中,每个搜索到的 1 都会被重新标记为 0。
最终岛屿的数量就是我们进行深度优先搜索的次数。
代码:

class Solution {public int numIslands(char[][] grid) {int row=grid.length,col=grid[0].length;if(row==0||col==0) return 0;int sum=0;for(int i=0;i<row;i++){for(int j=0;j<col;j++){if(grid[i][j]=='1'){dfs(grid,i,j,row,col);sum++;}}}return sum;}void dfs(char[][]grid,int i,int j,int row,int col){if(i<0||i>=row||j<0||j>=col||grid[i][j]=='0') return;grid[i][j]='0';dfs(grid,i-1,j,row,col);dfs(grid,i+1,j,row,col);dfs(grid,i,j-1,row,col);dfs(grid,i,j+1,row,col);}
}

补充:
「岛屿问题」是一个系列系列问题,比如:
200. 岛屿数量 (Easy)
463. 岛屿的周长 (Easy)
695. 岛屿的最大面积 (Medium)
827. 最大人工岛 (Hard)
多加练习。

二、广度优先

1.广度遍历(BFS)解决什么问题?

bfs一般解决最短路径的问题,广度优先的搜索的是从起始点出发,一层一层的进行,每层当中的点距离起始点的步数都是相同的。
补充:
双端BFS
同时从起始点和终点开始进行的广度优先的搜索成为双端bfs;
双端bfs可以大大提高效率:
例如:想判断社交应用程序中两个人之间需要多少朋友介绍才能互相认识。

2.算法思想:

在广度优先搜索算法中,解答树上结点的扩展是按它们在树中的层次进行的。首先生成第一层结点,同时检查目标结点是否在所生成的结点中,如果不在,则将所有的第一层结点逐一扩展,得到第二层结点,并检查第二层结点是否包含目标结点,……,对层次为n+1的任一结点进行扩展之前,必须先考虑层次完层次为n的结点的每种可能的状态。因此,对于同一层结点来说,求解问题的价值是相同的,可以按任意顺序来扩展它们。通常采用的原则是先生成的结点先扩展
为了便于进行搜索,要设置一个表存储所有的结点。由于在广度优先搜索算法中,要满足先生成的结点先扩展的原则,所以存储结点的表一般采用队列这种数据结构。
在编写程序时,可用数组q模拟队列。front和rear分别表示队头指针和队尾指针,初始时front=rear=0。
元素x入队操作为 q[rear++]=x;
元素x出队操作为 x =q[front++];
广度优先搜索算法的搜索步骤一般是:
(1)从队列头取出一个结点,检查它按照扩展规则是否能够扩展,如果能则产生一个新结点。
(2)检查新生成的结点,看它是否已在队列中存在,如果新结点已经在队列中出现过,就放弃这个结点,然后回到第(1)步。否则,如果新结点未曾在队列中出现过,则将它加入到队列尾。
(3)检查新结点是否目标结点。如果新结点是目标结点,则搜索成功,程序结束;若新结点不是目标结点,则回到第(1)步,再从队列头取出结点进行扩展。
最终可能产生两种结果:找到目标结点,或扩展完所有结点而没有找到目标结点。
如果目标结点存在于解答树的有限层上,广度优先搜索算法一定能保证找到一条通向它的最佳路径,因此广度优先搜索算法特别适用于只需求出最优解的问题。当问题需要给出解的路径,则要保存每个结点的来源,也就是它是从哪一个节点扩展来的。
对于广度优先搜索算法来说,问题不同则状态结点的结构和结点扩展规则是不同的,但搜索的策略是相同的。广度优先搜索算法的框架一般如下:

void  BFS(){队列初始化;初始结点入队;while (队列非空) {  队头元素出队,赋给current;while  (current 还可以扩展) {由结点current扩展出新结点new;if  (new 重复于已有的结点状态) continue;new结点入队;if  (new结点是目标状态){置flag= true;    break; }}}
}

对于不同的问题,用广度优先搜索法的算法基本上都是一样的。但表示问题状态的结点数据结构、新结点是否为目标结点和是否为重复结点的判断等方面则有所不同。对具体的问题需要进行具体分析,这些函数要根据具体问题进行编写。

3、例题讲解:

运⽤⼴度优先搜索在迷宫中寻找最短的路径?
核心代码及思想:

void bfs(int[][] maze, int x, int y) {##创建⼀个队列,将起始点加⼊队列中Queue<Integer[]> queue = new LinkedList<>();queue.add(new Integer[] {x, y});##只要队列不为空,就⼀直循环下去while (!queue.isEmpty()) {从队列中取出当前要处理的点Integer[] pos = queue.poll();x = pos[0]; y = pos[1];##在四个⽅向上进⾏BFS搜索for (int d = 0; d < 4; d++) {int i = x + dx[d], j = y + dy[d];## 判断⼀下该⽅向上的点是否已经访问过了if (isSafe(maze, i, j)) {##被访问过,则记录步数,并加⼊队列中maze[i][j] = maze[x][y] + 1;queue.add(new Integer[] {i, j});##找到⽬的地后⽴即返回if (i == B[0] && j == B[1]) return; } } }
}

Step 1:创建⼀个队列,将起始点加⼊队列中
Step 2:只要队列不为空,就⼀直循环下去
Step 3:从队列中取出当前要处理的点
Step 4:在四个⽅向上进⾏BFS搜索
Step 5:判断⼀下该⽅向上的点是否已经访问过了
Step 6:被访问过,则记录步数,并加⼊队列中
Step 7:找到⽬的地后⽴即返回

4、实站经典:

279. 完全平方数
给定正整数 n,找到若干个完全平方数(比如 1, 4, 9, 16, …)使得它们的和等于 n。你需要让组成和的完全平方数的个数最少。
给你一个整数 n ,返回和为 n 的完全平方数的 最少数量 。
完全平方数 是一个整数,其值等于另一个整数的平方;换句话说,其值等于一个整数自乘的积。例如,1、4、9 和 16 都是完全平方数,而 3 和 11 不是。

示例 1:
输入:n = 12
输出:3
解释:12 = 4 + 4 + 4
示例 2:
输入:n = 13
输出:2
解释:13 = 4 + 9

解题思想:
给定一个 N 元树,其中每个节点表示数字 n 的余数减去一个完全平方数的组合,我们的任务是在树中找到一个节点,该节点满足两个条件:

(1) 节点的值(即余数)也是一个完全平方数。
(2) 在满足条件(1)的所有节点中,节点和根之间的距离应该最小。
下面是这棵树的样子:

从上到下逐层构造 N 元树。我们以 BFS(广度优先搜索)的方式遍历它。在 N 元树的每一级,我们都在枚举相同大小的组合。
遍历的顺序是 BFS,而不是 DFS(深度优先搜索),这是因为在用尽固定数量的完全平方数分解数字 n 的所有可能性之前,我们不会探索任何需要更多元素的潜在组合。
算法步骤:
首先,我们准备小于给定数字 n 的完全平方数列表。
然后创建 queue 遍历,该变量将保存所有剩余项在每个级别的枚举。
在主循环中,我们迭代 queue 变量。在每次迭代中,我们检查余数是否是一个完全平方数。如果余数不是一个完全平方数,就用其中一个完全平方数减去它,得到一个新余数,然后将新余数添加到 next_queue 中,以进行下一级的迭代。一旦遇到一个完全平方数的余数,我们就会跳出循环,这也意味着我们找到了解。
注意:在典型的 BFS 算法中,queue 变量通常是数组或列表类型。但是,这里我们使用 set 类型,以消除同一级别中的剩余项的冗余。事实证明,这个小技巧甚至可以增加 5 倍的运行加速。
代码:

class Solution {public int numSquares(int n) {ArrayList<Integer> square_nums = new ArrayList<Integer>();for (int i = 1; i * i <= n; ++i) {square_nums.add(i * i);}Set<Integer> queue = new HashSet<Integer>();queue.add(n);int level = 0;while (queue.size() > 0) {level += 1;Set<Integer> next_queue = new HashSet<Integer>();for (Integer remainder : queue) {for (Integer square : square_nums) {if (remainder.equals(square)) {return level;} else if (remainder < square) {break;} else {next_queue.add(remainder - square);}}}queue = next_queue;}return level;}
}

三、拓扑排序

1、解决问题

利用bfs和dfs进行判环

2、实战经典:

207. 课程表
你这个学期必须选修 numCourses 门课程,记为 0 到 numCourses - 1 。
在选修某些课程之前需要一些先修课程。 先修课程按数组 prerequisites 给出,其中 prerequisites[i] = [ai, bi] ,表示如果要学习课程 ai 则 必须 先学习课程 bi 。

例如,先修课程对 [0, 1] 表示:想要学习课程 0 ,你需要先完成课程 1 。
请你判断是否可能完成所有课程的学习?如果可以,返回 true ;否则,返回 false 。
解题思路:
本题可约化为: 课程安排图是否是 有向无环图(DAG)。即课程间规定了前置条件,但不能构成任何环路,否则课程前置条件将不成立。
思路是通过 拓扑排序 判断此课程安排图是否是 有向无环图(DAG) 。 拓扑排序原理: 对 DAG 的顶点进行排序,使得对每一条有向边 (u, v)(u,v),均有 uu(在排序记录中)比 vv 先出现。亦可理解为对某点 vv 而言,只有当 vv 的所有源点均出现了,vv 才能出现。
通过课程前置条件列表 prerequisites 可以得到课程安排图的 邻接表 adjacency,以降低算法时间复杂度,以下两种方法都会用到邻接表。
方法一:入度表(广度优先遍历)
算法流程:
统计课程安排图中每个节点的入度,生成 入度表 indegrees。
借助一个队列 queue,将所有入度为 0的节点入队。
当 queue 非空时,依次将队首节点出队,在课程安排图中删除此节点 pre:
并不是真正从邻接表中删除此节点 pre,而是将此节点对应所有邻接节点 cur 的入度 -1−1,即 indegrees[cur] -= 1。
当入度 -1后邻接节点 cur 的入度为 0,说明 cur 所有的前驱节点已经被 “删除”,此时将 cur 入队。
在每次 pre 出队时,执行 numCourses–;
若整个课程安排图是有向无环图(即可以安排),则所有节点一定都入队并出队过,即完成拓扑排序。换个角度说,若课程安排图中存在环,一定有节点的入度始终不为 0。
因此,拓扑排序出队次数等于课程个数,返回 numCourses == 0 判断课程是否可以成功安排。
代码:

class Solution {public boolean canFinish(int numCourses, int[][] prerequisites) {int[] indegrees = new int[numCourses];List<List<Integer>> adjacency = new ArrayList<>();Queue<Integer> queue = new LinkedList<>();for(int i = 0; i < numCourses; i++)adjacency.add(new ArrayList<>());// Get the indegree and adjacency of every course.for(int[] cp : prerequisites) {indegrees[cp[0]]++;adjacency.get(cp[1]).add(cp[0]);}// Get all the courses with the indegree of 0.for(int i = 0; i < numCourses; i++)if(indegrees[i] == 0) queue.add(i);// BFS TopSort.while(!queue.isEmpty()) {int pre = queue.poll();numCourses--;for(int cur : adjacency.get(pre))if(--indegrees[cur] == 0) queue.add(cur);}return numCourses == 0;}
}

方法二:深度优先遍历
算法流程:
借助一个标志列表 flags,用于判断每个节点 i (课程)的状态:
未被 DFS 访问:i == 0;
已被其他节点启动的 DFS 访问:i == -1;
已被当前节点启动的 DFS 访问:i == 1。
对 numCourses 个节点依次执行 DFS,判断每个节点起步 DFS 是否存在环,若存在环直接返回 FalseFalse。DFS 流程;
终止条件:
当 flag[i] == -1,说明当前访问节点已被其他节点启动的 DFS 访问,无需再重复搜索,直接返回 TrueTrue。
当 flag[i] == 1,说明在本轮 DFS 搜索中节点 i 被第 22 次访问,即 课程安排图有环 ,直接返回 FalseFalse。
将当前访问节点 i 对应 flag[i] 置 11,即标记其被本轮 DFS 访问过;
递归访问当前节点 i 的所有邻接节点 j,当发现环直接返回 FalseFalse;
当前节点所有邻接节点已被遍历,并没有发现环,则将当前节点 flag 置为 -1−1 并返回 TrueTrue。
若整个图 DFS 结束并未发现环,返回 TrueTrue。
代码:

class Solution {public boolean canFinish(int numCourses, int[][] prerequisites) {List<List<Integer>> adjacency = new ArrayList<>();for(int i = 0; i < numCourses; i++)adjacency.add(new ArrayList<>());int[] flags = new int[numCourses];for(int[] cp : prerequisites)adjacency.get(cp[1]).add(cp[0]);for(int i = 0; i < numCourses; i++)if(!dfs(adjacency, flags, i)) return false;return true;}private boolean dfs(List<List<Integer>> adjacency, int[] flags, int i) {if(flags[i] == 1) return false;if(flags[i] == -1) return true;flags[i] = 1;for(Integer j : adjacency.get(i))if(!dfs(adjacency, flags, j)) return false;flags[i] = -1;return true;}
}

深度优先,广度优先,拓扑排序(实战题解)相关推荐

  1. 菜肴制作(拓扑排序) 题解

    题目描述 知名美食家小 A被邀请至ATM 大酒店,为其品评菜肴. ATM 酒店为小 A 准备了 N 道菜肴,酒店按照为菜肴预估的质量从高到低给予1到N的顺序编号,预估质量最高的菜肴编号为1. 由于菜肴 ...

  2. 图 相关算法~从头学算法【广搜、 深搜、 拓扑排序、 并查集、 弗洛伊德算法、迪杰斯特拉算法】

    图的相关主流算法主要有: 广度优先搜索 深度优先搜索 拓扑排序 并查集 多源最短路径(弗洛伊德算法) 单源最短路径(迪杰斯特拉算法) 其中呢,最基本的是前两种,也就是平时常用的广搜和深搜,本文中将概要 ...

  3. networkx 有向图强连通_leetcode刷题(四):搜索(深度优先搜索,广度优先搜索)拓扑排序,强连通分量...

    在开始今天的话题之前,我们先了解一个概念,什么是图的遍历? 图的遍历就是从图中某一点出发访遍图中其余剩余定点,且每个顶点仅被访问一次,这个过程叫做图的遍历. 图的遍历主要被分为深度优先遍历和广度优先遍 ...

  4. Algorithm:C++语言实现之图论算法相关(图搜索广度优先BFS、深度优先DFS,最短路径SPF、带负权的最短路径Bellman-ford、拓扑排序)

    Algorithm:C++语言实现之图论算法相关(图搜索广度优先BFS.深度优先DFS,最短路径SPF.带负权的最短路径Bellman-ford.拓扑排序) 目录 一.图的搜索 1.BFS (Brea ...

  5. 图的深度优先搜索及拓扑排序

    本文将介绍图的深度优先搜索,并实现基于深度优先搜索的拓扑排序(拓扑排序适用于有向无环图,下面详细介绍). 1. 图的深度优先遍历要解决的问题 图的深度优先搜索与树的深度优先搜索类似,但是对图进行深度优 ...

  6. 洛谷 P3244 / loj 2115 [HNOI2015] 落忆枫音 题解【拓扑排序】【组合】【逆元】

    组合计数的一道好题.什么非主流题目 题目背景 (背景冗长请到题目页面查看) 题目描述 不妨假设枫叶上有 \(n​\) 个穴位,穴位的编号为 \(1\sim n​\).有若干条有向的脉络连接着这些穴位. ...

  7. 通过深度优先算法进行拓扑排序(算法导论)

    通过深度优先算法进行拓扑排序(Java) package graph; //拓扑排序用深度优先算法实现 import java.io.IOException; import java.util.Sca ...

  8. 图——深度优先遍历(DFS)实现有向无环图的逆拓扑排序

    对图的深度遍历与对树的深度遍历思想类似,采用的是递归函数的办法. 如果是非连通图,则DFS遍历所有顶点即可. //Graph 图 //vertex 顶点,用一个int型变量表示//返回有向图G中顶点v ...

  9. 算法导论:dfs深度优先搜索算法及基于dfs的拓扑排序以及宽度优先搜索算法bfs

    1.dfs深度优先搜索算法 算法导论中是通过三种标记颜色来介绍dfs的,white代表还没被搜过,grey代表被搜了一些,还没结束,white表示已经搜索完成的状态. c/c++复现dfs代码 #in ...

最新文章

  1. 嵌入式Web服务器移植
  2. linux c++ 得到 指定进程名 线程数
  3. 从零开始学习jQuery (七) jQuery动画-让页面动起来!
  4. myeclipse无法启动tomcat的一种情况
  5. caffe 提取特征并可视化(已测试可执行)及在线可视化
  6. 安徽破获以虚拟数字货币增值为幌子网络传销案,涉案逾2亿元
  7. Java中interface接口与abstract class抽象类的区别
  8. 抓鸡 抓服务器 1433 3306 全自动效率抓鸡
  9. 安防区块链技术的落地应用
  10. Flutter Text文本删除线、下划线设置
  11. win7禁用驱动签名验证_如何在64位Windows 8或10上禁用驱动程序签名验证(以便可以安装未签名的驱动程序)...
  12. 你没有见过的6个创意CSS链接悬停效果
  13. Windows驱动的彻底删除
  14. 计算机颜色学---CIE 色度图以及饱和度处理
  15. 聚乙烯基吡啶阳离子功能化聚苯乙烯微球/二氧化锆/聚苯乙烯阳离子微球研究步骤
  16. 我的物联网项目之摇摇车三种消费模式和城市合伙人
  17. 2016年蓝桥杯A组 第九题 密码脱落
  18. 相干与非相干FSK解调和Viterbi软硬判性能的仿真对比
  19. Linux系统漏洞本地提权
  20. GPIO口,上下拉电阻与推挽输出

热门文章

  1. 创造自己的专属免费网盘:Nextcloud
  2. cx是什么简称_80年的5角,在纸币收藏界简称为8005
  3. sql server 2005技术大全_3年2.16~3.2万吨!上机数控子公司与新疆大全签订多晶硅采购长单...
  4. vm linux数据恢复,VMWARE虚拟机数据恢复
  5. MySQL对字符集_对MySQL字符集的认识
  6. kubernetes 磁盘、PV、PVC
  7. 转行进入测试一年有感
  8. 重修 mongoDB 系列(一) 配置环境
  9. [BAT] 执行xcopy命令后出现Invalid num of parameters错误的解决办法
  10. 剑桥offer(41~50)