leetcode-图总结

leetcode-200-岛屿的个数 (number of islands)-java

方法1:深度优先遍历

设置hasGone二维数组,使用深度优先遍历,将周围是1的都遍历到,并且设置hasGone为true。

从主方法开始,遍历过几次,就有几个岛屿(因为遍历第一个岛屿,会将岛屿所有连接的地方都设为true,那些地方不会再从主方法遍历)

也可以把访问过的改为‘0’,继续遍历。

方法2:并查集

 

leetcode-127-单词接龙-java

解法1(别人的)

拥有一个 beginWord 和一个 endWord,分别表示图上的 start node 和 end node。我们希望利用一些中间节点(单词)从 start node 到 end node,中间节点是 wordList 给定的单词。我们对这个单词接龙每个步骤的唯一条件是相邻单词只可以改变一个字母。

我们将问题抽象在一个无向无权图中,每个单词作为节点,差距只有一个字母的两个单词之间连一条边。问题变成找到从起点到终点的最短路径,如果存在的话。因此可以使用广度优先搜索方法。

算法中最重要的步骤是找出相邻的节点,也就是只差一个字母的两个单词。为了快速的找到这些相邻节点,我们对给定的 wordList 做一个预处理,将单词中的某个字母用 * 代替。

这个预处理帮我们构造了一个单词变换的通用状态。例如:Dog ----> D*g <---- Dig,Dog 和 Dig 都指向了一个通用状态 D*g。

这步预处理找出了单词表中所有单词改变某个字母后的通用状态,并帮助我们更方便也更快的找到相邻节点。否则,对于每个单词我们需要遍历整个字母表查看是否存在一个单词与它相差一个字母,这将花费很多时间。预处理操作在广度优先搜索之前高效的建立了邻接表。

例如,在广搜时我们需要访问 Dug 的所有邻接点,我们可以先生成 Dug 的所有通用状态:

    Dug => *ug
    Dug => D*g
    Dug => Du*

第二个变换 D*g 可以同时映射到 Dog 或者 Dig,因为他们都有相同的通用状态。拥有相同的通用状态意味着两个单词只相差一个字母,他们的节点是相连的。

利用广度优先搜索搜索从 beginWord 到 endWord 的路径。

对给定的 wordList 做预处理,找出所有的通用状态。将通用状态记录在字典中,键是通用状态,值是所有具有通用状态的单词。

将包含 beginWord 和 1 的元组放入队列中,1 代表节点的层次。我们需要返回 endWord 的层次也就是从 beginWord 出发的最短距离。

为了防止出现环,使用访问数组记录。

当队列中有元素的时候,取出第一个元素,记为 current_word。

找到 current_word 的所有通用状态,并检查这些通用状态是否存在其它单词的映射,这一步通过检查 all_combo_dict 来实现。

从 all_combo_dict 获得的所有单词,都和 current_word 共有一个通用状态,所以都和 current_word 相连,因此将他们加入到队列中。

对于新获得的所有单词,向队列中加入元素 (word, level + 1) 其中 level 是 current_word 的层次。

最终当你到达期望的单词,对应的层次就是最短变换序列的长度。

解法2(别人的)

根据给定字典构造的图可能会很大,而广度优先搜索的搜索空间大小依赖于每层节点的分支数量。假如每个节点的分支数量相同,搜索空间会随着层数的增长指数级的增加。考虑一个简单的二叉树,每一层都是满二叉树的扩展,节点的数量会以 2 为底数呈指数增长。

如果使用两个同时进行的广搜可以有效地减少搜索空间。一边从 beginWord 开始,另一边从 endWord 开始。我们每次从两边各扩展一个节点,当发现某一时刻两边都访问了某一顶点时就停止搜索。这就是双向广度优先搜索,它可以可观地减少搜索空间大小,从而降低时间和空间复杂度。

算法与之前描述的标准广搜方法相类似。

唯一的不同是我们从两个节点同时开始搜索,同时搜索的结束条件也有所变化。

我们现在有两个访问数组,分别记录从对应的起点是否已经访问了该节点。

如果我们发现一个节点被两个搜索同时访问,就结束搜索过程。因为我们找到了双向搜索的交点。过程如同从中间相遇而不是沿着搜索路径一直走。

双向搜索的结束条件是找到一个单词被两边搜索都访问过了。

最短变换序列的长度就是中间节点在两边的层次之和。因此,我们可以在访问数组中记录节点的层次。


leetcode-130-被围绕的区域-java

如何寻找和边界联通的O? 从边界出发,对图进行 dfs 和 bfs 即可。这里简单总结下 dfs 和 dfs。

    bfs 递归。可以想想二叉树中如何递归的进行层序遍历。
    bfs 非递归。一般用队列存储。
    dfs 递归。最常用,如二叉树的先序遍历。
    dfs 非递归。一般用 stack。

那么基于上面这种想法,我们有四种方式实现。

dfs递归:
使用dfs递归,将边的O以及连接这些O变成A(相当于,没有与边上的O相连的O没有被改变,还是O,与边上的O相连的O变成A),然后遍历数组,将O变成X,A变成O

dsf 非递归:

非递归的方式,我们需要记录每一次遍历过的位置,我们用 stack 来记录,因为它先进后出的特点。而位置我们定义一个内部类 Pos 来标记横坐标和纵坐标。注意的是,在写非递归的时候,我们每次查看 stack 顶,但是并不弹出 stack,直到这个位置上下左右都搜索不到的时候弹出 Stack。

bfs 非递归:

dfs 非递归的时候我们用 stack 来记录状态,而 bfs 非递归,我们则用队列来记录状态。和 dfs 不同的是,dfs 中搜索上下左右,只要搜索到一个满足条件,我们就顺着该方向继续搜索,所以你可以看到 dfs 代码中,只要满足条件,就入 Stack,然后 continue 本次搜索,进行下一次搜索,直到搜索到没有满足条件的时候出 stack。而 dfs 中,我们要把上下左右满足条件的都入队,所以搜索的时候就不能 continue。大家可以对比下两者的代码,体会 bfs 和 dfs 的差异。

bfs 递归:

bfs 一般我们不会去涉及,而且比较绕,之前我们唯一过的用 bfs 递归的方式是层序遍历二叉树的时候可以用递归的方式。

并查集:

并查集这种数据结构好像大家不太常用,实际上很有用,我在实际的 production code 中用过并查集。并查集常用来解决连通性的问题,即将一个图中连通的部分划分出来。当我们判断图中两个点之间是否存在路径时,就可以根据判断他们是否在一个连通区域。 而这道题我们其实求解的就是和边界的 O 在一个连通区域的的问题。

并查集的思想就是,同一个连通区域内的所有点的根节点是同一个。将每个点映射成一个数字。先假设每个点的根节点就是他们自己,然后我们以此输入连通的点对,然后将其中一个点的根节点赋成另一个节点的根节点,这样这两个点所在连通区域又相互连通了。
并查集的主要操作有:

find(int m):这是并查集的基本操作,查找 m 的根节点。

isConnected(int m,int n):判断 m,n 两个点是否在一个连通区域。

union(int m,int n):合并 m,n 两个点所在的连通区域。

我们的思路是把所有边界上的 O 看做一个连通区域。遇到 O就执行并查集合并操作,这样所有的 O就会被分成两类

和边界上的 O在一个连通区域内的。这些 O 我们保留。
不和边界上的 O 在一个连通区域内的。这些 O 就是被包围的,替换。

由于并查集我们一般用一维数组来记录,方便查找 parants,所以我们将二维坐标用 node 函数转化为一维坐标(i*row+j)。
 

leetcode-547-朋友圈-java

解法1 :并查集

使用并查集,将认识的节点连通起来,最后看有几个parent[i]==i的情况(这个代表是根节点)

解法2 :dfs

给定的矩阵可以看成图的邻接矩阵。这样我们的问题可以变成无向图连通块的个数。

在这个图中,点的编号表示矩阵 M 的下标,i 和 j 之间有一条边当且仅当 M[i][j]为 1。

为了找到连通块的个数,一个简单的方法就是使用深度优先搜索,从每个节点开始,我们使用一个大小为 N 的 visited数组(M 大小为 N×N),这样 visited[i]表示第 i 个元素是否被深度优先搜索访问过。

我们首先选择一个节点,访问任一相邻的节点。然后再访问这一节点的任一相邻节点。这样不断遍历到没有未访问的相邻节点时,回溯到之前的节点进行访问。

因此,连通块的个数,我们从每个未被访问的节点开始深搜,每开始一次搜索就增加 count计数器一次。

解法3 :bfs

上面的算法中提到,如果我们把矩阵看成图的邻接矩阵,我们可以使用图算法很快的算出连通块的个数。这可以用到图中的广度优先搜索。

在广度优先搜索中,我们从一个特定点开始,访问所有邻接的节点。然后对于这些邻接节点,我们依然通过访问邻接节点的方式,知道访问所有可以到达的节点。因此,我们按照一层一层的方式访问节点,广搜的例子如下:

我们从任一个节点开始广搜,使用 visited数组记录是否被访问过。增加 count 变量当一个连通块已经访问完但是还有节点没有被访问的时候。
 

leetcode-207-课程表-java

解法1

本题可约化为:课程安排图是否是 有向无环图(DAG)。即课程间规定了前置条件,但不能构成任何环路,否则课程前置条件将不成立。
思路是通过 拓扑排序 判断此课程安排图是否是 有向无环图(DAG)。
拓扑排序是对 DAG 的顶点进行排序,使得对每一条有向边 (u,v),均有 u(在排序记录中)比 v先出现。亦可理解为对某点 v而言,只有当 v的所有源点均出现了,v才能出现。
通过课程前置条件列表 prerequisites 可以得到课程安排图的 邻接矩阵 adjacency,以下两种方法都会用到邻接矩阵。

入度表(广度优先遍历)

统计课程安排图中每个节点的入度,生成 入度表 indegrees。
借助一个队列 queue,将所有入度为 0的节点入队。
当 queue 非空时,依次将队首节点出队,在课程安排图中删除此节点 pre:
并不是真正从邻接表中删除此节点 pre,而是将此节点对应所有邻接节点 cur 的入度 −1,即 indegrees[cur] -= 1。
当入度 −1后邻接节点 cur 的入度为 0,说明 cur 所有的前驱节点已经被 “删除”,此时将 cur 入队。
在每次 pre 出队时,执行 numCourses--;
若整个课程安排图是有向无环图(即可以安排),则所有节点一定都入队并出队过,即完成拓扑排序。换个角度说,若课程安排图中存在环,一定有节点的入度始终不为 0。
因此,拓扑排序出队次数等于课程个数,返回 numCourses == 0 判断课程是否可以成功安排。

时间复杂度 O(N+M),遍历一个图需要访问所有节点和所有临边,N 和 M分别为节点数量和临边数量;
空间复杂度 O(N),为建立邻接矩阵所需额外空间。

解法2

深度优先遍历

借助一个标志列表 flags,用于判断每个节点 i (课程)的状态:

未被 DFS 访问:i == 0;
已被其他节点启动的DFS访问:i == -1;
已被当前节点启动的DFS访问:i == 1。
对 numCourses 个节点依次执行 DFS,判断每个节点起步 DFS 是否存在环,若存在环直接返回 False。DFS 流程;
终止条件:
当 flag[i] == -1,说明当前访问节点已被其他节点启动的 DFS 访问,无需再重复搜索,直接返回 True。
当 flag[i] == 1,说明在本轮 DFS 搜索中节点 i 被第 2次访问,即 课程安排图有环,直接返回 False。
将当前访问节点 i 对应 flag[i] 置 1,即标记其被本轮 DFS 访问过;
递归访问当前节点 i 的所有邻接节点 j,当发现环直接返回 False;
当前节点所有邻接节点已被遍历,并没有发现环,则将当前节点 flag 置为 −1 并返回 True。
若整个图 DFS 结束并未发现环,返回 True。
 

leetcode-210-课程表 II-java

解法1

bfs算法,第一个是入的,第二个是出的,[1,0] 方向是0->1,不断寻找入度为0的点,那是第一个可以出现的点

首先,寻找入度为0的点,加入队列。

队列拉出头结点,加入结果数组,然后nowVertex是入度为0的点,但是可能有出度,删除由nowVertex出发的边(对应的点,入度-1),将从而入度为0的点加入队列

可以建立一个Map> adjList  ,存储每个节点出发的边,从而删除边的时候,不用遍历所有的边,可以直接找到对应的边。

解法2

dfs

初始化栈 S,它将存储图中课程的拓扑排序。
使用输入中提供的边构建邻接表。注意输入中如 [a, b] 的边代表课程 b是课程 a的先修课程。这代表边 b ➔ a。在实现算法时,请记住这一点。
对于图中的每个结点,都运行一次深度优先搜索,以防该结点没有在其他结点的深度优先搜索中被访问到过。
假设我们正在执行结点 N的深度优先搜索。我们将递归地遍历结点 N 所有未被处理过的邻接结点。
处理完了所有邻接结点后,将结点N入栈。我们利用栈来模拟所需要的顺序。当结点 N入栈时,所有以N 为先修的课程结点均已经入栈。
在所有的结点被处理过后,从栈顶到栈底顺序依次返回结点元素。
 

leetcode-329-矩阵中的最长递增路径-java

记忆化深度优先搜索

将递归的结果存储下来,这样每个子问题只需要计算一次。

从上面的分析中,在淳朴的深度优先搜索方法中有许多重复的计算。

一个优化途径是我们可以用一个集合来避免一次深度优先搜索中的重复访问。该优化可以将一次深度优先搜索的时间复杂度优化到 O(mn),总时间复杂度 O(m^2 * n^2)。

下面介绍一个更有力的优化方法,记忆化。

    在计算中,记忆化是一种优化技术,它通过存储“昂贵”的函数调用的结果,在相同的输入再次出现时返回缓存的结果,以此加快程序的速度。

在本问题中,我们多次递归调用 dfs(x, y) 。但是,如果我们已经知道四个相邻单元格的结果,就只需要常数时间。在搜索过程中,如果未计算过单元格的结果,我们会计算并将其缓存;否则,直接从缓存中获取之。

即int[ ] [ ] cache 里面存储从(i,j)出发的最长递增序列,如果当前节点小于下一个节点,则更新从这个节点出发的最长递增序列(不包括该节点)
 

 

 

 

 

 

 

 

 

 

 

你可能感兴趣的:(数据结构-图,leetcode总结)