在开始今天的话题之前,我们先了解一个概念,什么是图的遍历?
图的遍历就是从图中某一点出发访遍图中其余剩余定点,且每个顶点仅被访问一次,这个过程叫做图的遍历。
图的遍历主要被分为深度优先遍历和广度优先遍历。
此外,我们还要了解两个概念,邻接链表和邻接矩阵:
邻接链表的存储方法跟树的孩子链表示法相类似,是一种顺序分配和链式分配相结合的存储结构。如这个表头结点所对应的顶点存在相邻顶点,则把相邻顶点依次存放于表头结点所指向的单向链表中。
邻接矩阵的逻辑结构分为两部分:V和E集合。因此,用一个一维数组存放图中所有顶点数据;用一个二维数组存放顶点间关系(边或弧)的数据,这个二维数组称为邻接矩阵。邻接矩阵又分为有向图邻接矩阵和无向图邻接矩阵。
邻接链表的一个潜在缺陷就是无法快速判断一条边(u,v)是否是图中的某一边,唯一的解决办法就是再A[u]里面搜索节点v。而邻接矩阵就克服了这个缺陷,而付出的代价是更大的存储空间消耗。
深度优先搜索
深度优先搜索总是对最近才发现的点v的出发搜索,直到该点所有的出发边都被发现为止。一旦该节点v所有的出发边都被发现,搜索则回溯到v的前驱结点,来搜索该节点的出发边。该过程一直持续到从源节点可以到达的所有节点都被发现为止。如果存在还未发现的节点,则深度优先搜索将会从这些尚未发现的节点中任选一个作为新的源节点,并重复整个过程,直到所有的节点都被搜索到为止。
DFS实现算法:
- 先创建一个visited数组,初始化为false。
- 调用遍历函数,实现递归。
- 当相邻节点为false时(即没有被访问过),以该节点进行递归。
- 否则返回上一节点。
我们上伪代码吧!
n = len(M)
visited = [False]*n #创建一个长度为n的visited数组
def dfs(i):
visited[i] = True #将visited数组上对应的i设置为True
for i in range(n):
if M[i][j] == 1 and not visited[j]: #前提是没被访问过
dfs(j) #递归
count = 0
for i in range(n):
if visited[i] == False:
dfs(i)
count += 1
return count
深度优先遍历有点像二叉树的前序遍历过程:
- 以某一节点开始,访问该节点
- 以该节点开始,重复1.(这里需要定义一个节点数大小的bool类型数组visited,来记录哪些节点访问过了,哪些节点没有访问过.)
- 当某个节点的邻接节点都访问过了,回退到上一个节点,访问上一个节点的其他相邻节点.
- 重复3.直至返回开始节点.
这是连通图的遍历方法,对于非连通图,我们只需循环调用递归,直至所有节点都访问过.
广度优先搜索
如果说,深度优先遍历类似于树的前序遍历,那么广度优先遍历就类似于树的层序遍历了。
广度优先遍历通过队列实现:
- 从某一节点开始,将该节点入队,找到该节点的所有相邻节点,将他们入队。
- 将该节点出队,再将队头节点的所有相邻节点入队。(这里也需要一个visited数组,已经入队过的节点不再入队.)
- 检查队列,对队头元素进行操作,出队。
为了更好的说明,我们上伪代码把!
void BFSTraverse(Graph G, Status(*Visit)(int v))
{/*按广度优先非递归遍历图G。使用辅助队列Q 和访问标志数组visited*/
for (v=0;v<G,vexnum;++v)
visited[v]=FALSE;
InitQueue(Q); /*置空的国债队列Q*/
if (!visited[v]) /*v 尚未访问*/
{EnQucue(Q,v); /*v 入队列*/
while (!QueueEmpty(Q))
{ DeQueue(Q,u); /*队头元素出队并置为u*/
visited[u]=TRUE; visit(u); /*访问u*/
for(w=FistAdjVex(G,u); w; w=NextAdjVex(G,u,w))
if (!visited[w])
EnQueue(Q,w); /*u 的尚未访问的邻接顶点w 入队列Q*/
}
}
}/*BFSTraverse*/
两个算法的总结
遍历的目的是为了找到合适的定点,那么选择哪一种遍历就要仔细斟酌了,深度优先遍历适合于目标明确,以找到目标为主要目的的情况,而广度优先遍历适合于在不断扩大遍历范围时,找到相对最优的情况。
下面是几道真题,我特意把深度优先搜索和广度优先搜索的真题混在一起了,大家在看真题的时候先要自己判断下是哪种类型的搜索哦!~!
Leetcode : 695. Max Area of Island (Easy)
给定一个包含了一些 0 和 1的非空二维数组 grid , 一个 岛屿 是由四个方向 (水平或垂直) 的 1 (代表土地) 构成的组合。你可以假设二维矩阵的四个边缘都被水包围着。
找到给定的二维数组中最大的岛屿面积。(如果没有岛屿,则返回面积为0。)
示例 1:
[[0,0,1,0,0,0,0,1,0,0,0,0,0],
[0,0,0,0,0,0,0,1,1,1,0,0,0],
[0,1,1,0,1,0,0,0,0,0,0,0,0],
[0,1,0,0,1,1,0,0,1,0,1,0,0],
[0,1,0,0,1,1,0,0,1,1,1,0,0],
[0,0,0,0,0,0,0,0,0,0,1,0,0],
[0,0,0,0,0,0,0,1,1,1,0,0,0],
[0,0,0,0,0,0,0,1,1,0,0,0,0]]
对于上面这个给定矩阵应返回 6。注意答案不应该是11,因为岛屿只能包含水平或垂直的四个方向的‘1’。
class Solution(object):
def dfs(self,grid,i,j):
cnt = 1
m = len(grid)
n = len(grid[0])
grid[i][j] = -1
out = [[-1,0],[1,0],[0,-1],[0,1]]
for k in range(4):
p = i + out[k][0]
q = j + out[k][1]
if(p>=0 and p<m and q>=0 and q<n and grid[p][q] == 1):
cnt += self.dfs(grid,p,q)
return cnt
def maxAreaOfIsland(self, grid):
"""
:type grid: List[List[int]]
:rtype: int
"""
big = 0
m = len(grid)
n = len(grid[0])
for i in range(0,m):
for j in range(0,n):
if grid[i][j] == 1:
big = max(big,self.dfs(grid,i,j))
return big
Leetcode : 547. Friend Circles (Medium)
班上有 N 名学生。其中有些人是朋友,有些则不是。他们的友谊具有是传递性。如果已知 A 是 B 的朋友,B 是 C 的朋友,那么我们可以认为 A 也是 C 的朋友。所谓的朋友圈,是指所有朋友的集合。
给定一个 N * N 的矩阵 M,表示班级中学生之间的朋友关系。如果M[i][j] = 1,表示已知第 i 个和 j 个学生互为朋友关系,否则为不知道。你必须输出所有学生中的已知的朋友圈总数。
示例 :
输入:
[[1,1,0],
[1,1,0],
[0,0,1]]
输出: 2
说明:已知学生0和学生1互为朋友,他们在一个朋友圈。
第2个学生自己在一个朋友圈。所以返回2。
class Solution(object):
def findCircleNum(self, M):
"""
:type M: List[List[int]]
:rtype: int
"""
if M == [] or M[0] == []:
return 0
n = len(M)
visited = [False]*n
def dfs(i):
visited[i] = True
for j in range(n):
if M[i][j] == 1 and not visited[j]:
dfs(j)
count = 0
for i in range(n):
if visited[i] == False:
dfs(i)
count += 1
return count
Leetcode : 130. Surrounded Regions (Medium)
给定一个二维的矩阵,包含 'X' 和 'O'(字母 O)。
找到所有被 'X' 围绕的区域,并将这些区域里所有的 'O' 用 'X' 填充。
示例:
X X X X
X O O X
X X O X
X O X X
运行你的函数后,矩阵变为:
X X X X
X X X X
X X X X
X O X X
class Solution:
def solve(self, board):
"""
:type board: List[List[str]]
:rtype: void Do not return anything, modify board in-place instead.
"""
if board == None :
return []
for i in range(len(board)):
for j in range(len(board[0])):
if board[i][j] == 'O':
if i == 0 or i == len(board) -1 or j == 0 or j == len(board[0]) -1:
self.robot(board,i,j,len(board),len(board[0]))
for i in range(len(board)):
for j in range(len(board[0])):
if board[i][j] == 'O':
board[i][j] = 'X'
elif board[i][j] == '*':
board[i][j] = 'O'
def robot(self,board,i,j,m,n):
dx = [0,0,1,-1]
dy = [1,-1,0,0]
board[i][j] = '*'
for idx in range(4):
if i + dx[idx] >= 0 and i + dx[idx] < m and j + dy[idx] >= 0 and j + dy[idx] < n and board[i + dx[idx]][j + dy[idx]] == "O":
self.robot(board,i+dx[idx],j+dy[idx],m,n)
接下来我们再顺带介绍下拓扑排序和强连通分量吧,之所以把它们和上面两个算法放在一起讲,是因为都属于图的范畴,大家可以了解下。
拓扑排序
对于一个有向无环图G来说,拓扑排序是G中所有节点的一种先行次序,该次序满足以下条件:如果图G包含(u,v),那么节点u处于在节点v的前面。可以将拓扑排序看作是将图的节点在一条水平线上排开,图的所有有向边都从左指向右。
强连通分量
在有向图G中,如果两点互相可达,则称这两个点强连通,如果G中任意两点互相可达,则称G是强连通图。
- 一个有向图是强连通的,当且仅当G中有一个回路,它至少包含每个节点一次。
- 非强连通有向图的极大强连通子图,称为强连通分量
我们有两种方法来判断是否为强连通图,我们来分别介绍下:
方法1:Korasaju算法
首先理解一下转置图的定义:将有向图G中的每一条边反向形成的图称为G的转置GT 。(注意到原图和GT 的强连通分支是一样的)
算法流程:
- 深度优先遍历G,算出每个结点u的结束时间f[u],起点如何选择无所谓。
- 深度优先遍历G的转置图GT ,选择遍历的起点时,按照结点的结束时间从大到小进行。遍历的过程中,一边遍历,一边给结点做分类标记,每找到一个新的起点,分类标记值就加1。
- 第2步中产生的标记值相同的结点构成深度优先森林中的一棵树,也即一个强连通分量
简而言之,就是先深度优先遍历,再将图转置后进行深度优先遍历,如果标记相同,可以理解为两点可以相互到达,就是强连通分量。
方法二:Tarjan算法
Tarjan算法是基于对图深度优先搜索的算法,每个强连通分量为搜索树中的一棵子树。
算法思想如下:
dfn[u]表示dfs时达到顶点u的次序号,low[u]表示以u为根节点的dfs树中次序号最小的顶点的次序号,所以当dfn[u] = low[u]时,以u为根的搜索子树上所有节点是一个强连通分量。 先将顶点u入栈,dfn[u] = low[u] = ++idx,扫描u能到达的顶点v,如果v没有被访问过,则dfs(v),low[u] = min(low[u],low[v]),如果v在栈里,low[u] = min(low[u],dfn[v]),扫描完v以后,如果dfn[u] = low[u],则将u及其以上顶点出栈。
简而言之,就是根据深度优先将节点压到栈中,然后观察栈顶的节点能到达的距离栈底最近的那个节点,将其提取出来,就是强连通分量。
差不多就是这样了,希望本文能帮到你!~!
最后打个小广告,我的公众号,喜欢写点学习中的小心得,不介意可以关注下!~!