图论基础
图的逻辑结构和具体实现
⼀幅图是由节点和边构成的,逻辑结构如下:
什么叫「逻辑结构」?就是说为了⽅便研究,我们把图抽象成这个样⼦。
该逻辑结构等价于「多叉树」,其中每个节点可以定义为:
/* 图节点的逻辑结构 */
class Vertex {
int id;
Vertex[] neighbors;
}
但为了方便和易于理解,通常使用邻接表和邻接矩阵来实现:
「邻接表」很直观,我把每个节点 x 的邻居都存到⼀个列表⾥,然后把 x 和这个列表关联起来,这样就可以通过⼀个节点 x 找到它的所有相邻节点。
「邻接矩阵」则是⼀个⼆维布尔数组,我们权且称为 matrix,如果节点 x 和 y 是相连的,那么就把 matrix[x][y] 设为 true(上图中绿⾊的⽅格代表 true)。如果想找节点 x 的邻居,去扫⼀圈 matrix[x][…] 就⾏了。
两种实现方式的优缺点为,对于邻接表,好处是占⽤的空间少。但是,邻接表⽆法快速判断两个节点是否相邻。所以说,使⽤哪⼀种⽅式实现图,要看具体情况。
好了,对于「图」这种数据结构,能看懂上⾯这些就绰绰够⽤了。下⾯通过实例来看看所有数据结构都逃不过的问题:遍历。
797. 所有可能的路径
解法:深度优先搜索
有关图的遍历,可以参考多叉树的遍历框架,
图和多叉树最⼤的区别是,图是可能包含环的,你从图的某⼀个节点开始遍历,有可能⾛了⼀圈⼜回到这个节点。
所以,如果图包含环,遍历框架就要⼀个 visited 数组进⾏辅助:
由于该题是无环的,因此可以直接套用多叉树的遍历框架,进行深度优先遍历求解即可。
class Solution:
def allPathsSourceTarget(self, graph: List[List[int]]) -> List[List[int]]:
result = []
n = len(graph) - 1
# path 用于维护递归过程中经过的路径
def traverse(value, path):
nonlocal result
nonlocal n
# 遍历到终点,添加路径
if value == n:
result.append(path + [value])
return
# 添加节点到当前路径
path.append(value)
# 递归每个相邻节点
for num in graph[value]:
traverse(num, path)
# 从路径中移除节点
path.pop()
traverse(0, [])
return result
1189. "气球"的最大数量(每日一题)
解法:数学
统计"a",“b”,“l”,“o”,“n"的出现次数。由于"l”,"o"在"balloon"出现2次,需要对统计次数整除2才为其能够拼凑次数。最后取所有单词拼凑次数的最小值作为结果。
class Solution:
def maxNumberOfBalloons(self, text: str) -> int:
count = {
"a": 0,
"b": 0,
"l": 0,
"o": 0,
"n": 0
}
for char in text:
if char in count:
count[char] += 1
count['l'] //= 2
count['o'] //= 2
return min(count.values())
总结
最后总结⼀下,图的存储⽅式主要有邻接表和邻接矩阵,⽆论什么花⾥胡哨的图,都可以⽤这两种⽅式存储。
在笔试中,最常考的算法是图的遍历,和多叉树的遍历框架是⾮常类似的。