图的基础知识
邻接矩阵是一个二维表,其中横纵坐标交叉的格子值为 1 的表示这两个顶点是连通的,否则是不连通的。
其中每个顶点使用一个链表来记录与该顶点相连通的邻居顶点。邻接表可以看做是一个 Map<Integer, List> 或者 List[] 结构。
注意,不管是邻接矩阵还是邻接表,都是存储的索引坐标,而不是顶点值。
无向有权图多存储一个权值,可以使用Map。
什么是拓扑排序:
拓扑排序就是将所有的入度为 0 的顶点加入队列,然后进行BFS,每次取出队首入度为 0 的顶点将其加入结果中,然后从邻接表或邻接矩阵中取出相邻的所有顶点,并将相邻顶点的入度都减 1,如果相邻顶点的入度减为 0 了,就继续将相邻顶点加入队列。
顶点的入度:有向图中指向该顶点的边的数量。(或者该顶点所依赖的顶点数量)
拓扑排序参考代码:
210. 课程表 II(拓扑排序问题)
-
1. BFS 拓扑排序 ,使用 邻接表 来构建 有向图 (本题可用 List<List> 或 List[] 或 Map<Integer, List> ),遍历 prerequisites 将所有课程加入 邻接表 中,并将需要 依赖其他课程的课程的入度 + 1 。
-
首先将所有的 入度为 0 的课程加入队列 ,然后进行BFS,每次取出队首入度为 0 的课程将其加入结果数组 res 中,然后从 邻接表 中取出相邻的所有其他课程,并将 相邻课程的入度都减 1 ,如果入度减为 0 了,就继续将课程 加入队列 。
-
最后判断如果 结果数组 res 的大小等于总的课程数 ,则说明可以完成所有课程任务,否则说明有向图中存在环,无法完成课程任务。
本题中的课程可以理解为有向图中的顶点下标,numCourses 表示总的顶点数,课程与课程之间的依赖关系表示有向图中的边,即从被依赖的课程指向依赖它的课程。
对于存在环的情况,例如:
此时取出队首以后,将邻接节点的入度减 1,但是邻接节点的入度不为 0 不会继续入队,然后此时队列为空就结束了:
此时res中收集的结果只有1个,说明有向图中存在环。
-
2. DFS 拓扑排序 ,仍然使用邻接表来存储有向图,使用 state 数组标记每个节点的状态: 0=未搜索,1=搜索中,2=已完成 ,然后遍历所有课程,从每一个[ 未搜索 ]状态的节点出发进行一次DFS。
-
递归函数中先将当前访问的节点状态标记为[ 搜索中 ],然后从邻接表取出当前节点的邻居节点,如果邻居节点的状态是[ 未搜索 ],则对邻居节点进行DFS,如果邻居节点状态是[ 搜索中 ] ,直接返回 true 表示存在环。在当前节点的所有邻居节点都访问完后,将当前节点的状态标记为[ 已完成 ],同时将当前结果保存到结果集合 res 中。
-
注意:在对邻居节点DFS时,只要子递归返回 true 就返回 true , 同样在主函数中每调用完一次DFS就判断如果有环就返回空数组。在保存结果 res 时,可以使用一个 数组栈 ,从数组末尾开始存。
这里递归函数的返回值表示是否遇到了环,其中state相当于之前的DFS写法中的visited访问标记去重数组,访问邻居节点时如果遇到了已访问的节点,说明遇到了环,否则如果是未访问的,就继续DFS,只不过多了一个已完成的状态。
对于拓扑排序,BFS是最常见的做法,DFS了解即可,本题DFS需要状态标记,判断是否有环,res倒序存等,代码比较奇怪。
207. 课程表(拓扑排序问题)
-
同210. 课程表 II,不需要保存结果数组,只需要返回值改成 true 或 false 即可。
269. 火星词典(拓扑排序问题)
-
拓扑排序 。火星词典中的 字母 和 字母顺序 可以看成 有向图 ,字典顺序即为所有字母的一种排列,满足每一条有向边的起点字母和终点字母的顺序都和这两个字母在排列中的顺序相同,该排列即为有向图的拓扑排序。
-
只有当有向图中无环时,才有拓扑排序,且拓扑排序可能不止一种。如果有向图中有环,则环内的字母不存在符合要求的排列,因此没有拓扑排序。
-
使用拓扑排序求解时,将火星词典中的每个 字母 看成一个 节点 ,将字母之间的 顺序 关系看成 有向边 。对于火星词典中的两个相邻单词,同时从左到右遍历,当遇到第一个不相同的字母时,该位置的两个字母之间即存在顺序关系。
-
以下两种情况不存在合法字母顺序:
-
1)字母之间的顺序关系存在由至少 2 个字母组成的环,例如 words =[“a”,“b”,“a”];
-
2)相邻两个单词满足后面的单词是前面的单词的前缀,且后面的单词的长度小于前面的单词的长度,例如 words = ["ab”,“a”]。
-
其余情况下都存在合法字母顺序,可以使用拓扑排序得到字典顺序。
使用HashSet存储每个节点的邻接节点:
329. 矩阵中的最长递增路径(记忆优化问题)
-
DFS + 记忆优化搜索 ,遍历矩阵从 每一个节点 出发进行一次DFS,每次DFS返回 最长递增路径长度 ,答案记最大值。
-
在递归函数中,每次访问当前节点的上下左右四个邻居坐标,如果 邻居的值比当前节点的值大 ,就对邻居进行DFS,并将 邻居递归的返回值 + 1 (当前节点长度)和当前的 res 比较取最大值。
-
注意:当前递归的 res 初始值为 1 (例如上下左右没有值时,也就是矩阵只有 一个 节点,至少有 一个节点的长度 ,说白了题目求的就是路径上的 节点数量 )。
-
记忆优化: 使用 memo 数组,每次保存 res 返回时同时存到 memo 中,每次递归进入先判断 memo 中是否已经有值,已经有值就直接返回。
注意这个代码,不需要显示的写递归终止条件,因为假如当前格子的四个方向的邻居都越界或者都比当前格子的值大,那么 for 中的内容不会被执行,当前递归函数返回的值是 1,这个就是递归终止条件。因为沿着四个方向不断DFS搜索,最终一定会发生越界。递归函数中的返回值需要拿四个方向的返回值 + 1 跟当前的 res 比最大。而主函数中则每次递归返回值都要比最大。
面试题 08.14. 布尔运算(记忆优化问题)
-
DFS + 记忆优化 ,递归函数返回两个信息,返回 str[L...R] 这一段,为 true 的 方法数 和 false 的 方法数 。
-
隐含的前提: [L...R] 上的字符一定有 奇数个 ,如 "0&0&1" , L 位置和 R 位置的字符,非 0 即 1 ,不可能是逻辑字符。
-
递归函数中判断:
-
1)如果 L == R ,说明只有一个字符,则只需要判断该字符是 0 还是 1 ,即可得到表达式为 true 和 false 的方法数。
-
2)如果 L != R, 说明 [L...R] 至少有 3 个字符,因为必须有 奇数 个字符 ,遍历[L+1...R] 上的每个 逻辑字符 做分割符 split ,每次 split 步长加 2 ,针对每个固定位置的分隔符,分别递归尝试 [L..split - 1] 上的方法数和 [split + 1..R] 上的方法数,相当于在分隔符的两侧区间放置括号,得到左右两部分的递归调用结果之后,再根据当前分割符是 & 或