[leetcode] 图论算法(DFS和BFS)
一、DFS
1.连通块
547. 省份数量 [Medium]
1971. 寻找图中是否存在路径 [Simple]
797. 所有可能的路径 [Medium]
841. 钥匙和房间 [Medium]
2316. 统计无向图中无法互相到达点对数 [Medium]
1319. 连通网络的操作次数 [Medium]
2492. 两个城市间路径的最小分数 [Medium]
3310. 移除可疑的方法 [Medium]
2685. 统计完全连通分量的数量 [Medium]
2192. 有向无环图中一个节点的所有祖先 [Medium]
399. 除法求值 [Medium]
133. 克隆图 [Medium]
# 2685. 统计完全连通分量的数量
class Solution:def countCompleteComponents(self, n: int, edges: List[List[int]]) -> int:d=defaultdict(list)for a,b in edges:d[a].append(b)d[b].append(a)vis=[False]*ndef dfs(i):vis[i]=Truenonlocal e,vv+=1e+=len(d[i])for j in d[i]:if not vis[j]:dfs(j)ans=0 for i in range(n):if not vis[i]:e=v=0dfs(i)ans+=(e==v*(v-1))return ansclass Solution:def countCompleteComponents(self, n: int, edges: List[List[int]]) -> int:# 建图g = defaultdict(list)for a,b in edges:g[a].append(b)g[b].append(a)# dfsdef dfs(i):vis.add(i)nonlocal e,vv += 1# e必须放在外面加e += len(g[i])for j in g[i]:if j not in vis:# e不能在里面加,因为标记访问过后,最后一条边进不来dfs(j)# 创建访问标记vis,ans = set(),0for i in range(n):if i not in vis:e = v = 0dfs(i)ans += (e == v*(v-1))return ans
连通块相关的题目主要是三个步骤:1>建图;2>创建访问标记变量;3>dfs。
访问标记变量有两种方法,一种是创建数组vis=[False]*n
,另一种是创建集合vis = set()
,两种都可以看个人喜好。dfs中的步骤基本也是固定的,先标记访问当前节点,然后遍历当前节点的下游连通节点,如果下游连通节点还没访问过,就递归dfs。
2.标记访问进阶
207. 课程表 [Medium]
802. 找到最终的安全状态 [Medium]
210. 课程表 II [Medium]
2115. 从给定原材料中找到所有可以做出的菜 [Medium]
第一类中的标记访问方法,可以避免重复访问以及防止图中有环出现无限递归。但是无法处理下面这种情况:
比如说我想要求出节点1出发的所有路径,如果采用第一类中的标记方法,节点3在第一条路径访问时被标记过了,那么在遍历第二条路径时就会跳过,从而漏掉第二条路径。解决方法是三色标记法(具体见灵神题解)
class Solution:def canFinish(self, numCourses: int, prerequisites: List[List[int]]) -> bool:g=defaultdict(list)for a,b in prerequisites:g[a].append(b)def dfs(i):vis[i]=1for j in g[i]:# 出现互相依赖关系,即出现环if vis[j]==1 or (vis[j]==0 and dfs(j)):return Truevis[i]=2return Falsevis=[0]*numCourses for i in range(numCourses):if not vis[i] and dfs(i):return Falsereturn True
采用三色标记法,1表示中间状态,2表示成功状态。如果在一次遍历中,访问到了状态为1的节点,则表示出现了循环依赖关系,即出现了环,那么当前这条路径就是非法的。可以看出这类题目只能用创建数组来标记访问,不适用创建集合来标记访问。
二、BFS
210. 课程表 II [Medium]
3243. 新增道路查询后的最短距离 I [Medium]
1311. 获取你好友已观看的视频 [Medium]
909. 蛇梯棋 [Hard]
127. 单词接龙 [Medium]
433. 最小基因变化 [Medium]
# 1311. 获取你好友已观看的视频
class Solution:def watchedVideosByFriends(self, watchedVideos: List[List[str]], friends: List[List[int]], id: int, level: int) -> List[str]:all,vis=[],[False]*len(friends)q=deque([id])vis[id]=Truefor i in range(level):cur_len=len(q)tmp=[]for j in range(cur_len):front=q.popleft()for k in friends[front]:if not vis[k]:vis[k]=Truetmp.append(k)q.extend(tmp)for j in q:all+=watchedVideos[j] ans=Counter(all)ans=sorted(ans.items(),key=lambda x:(x[1],x[0]))return [k for k,v in ans]
这题就是常规的BFS题目,套路就是先求当前level的长度cur_len,然后遍历队首,将符合条件的元素再插到队尾。
# 127. 单词接龙
class Solution:def ladderLength(self, beginWord: str, endWord: str, wordList: List[str]) -> int:if endWord not in set(wordList):return 0def diff(a,b):cnt=0for x,y in zip(a,b):cnt+=1 if x!=y else 0if cnt>1:return Falsereturn True if cnt==1 else Falseg=defaultdict(list)for a,b in combinations(wordList+[beginWord],2):if diff(a,b):g[a].append(b)g[b].append(a)q,vis=deque([(beginWord,1)]),set([beginWord])while q:front,v=q.popleft()for j in g[front]:if j==endWord:return v+1if j not in vis:# 防止重复访问vis.add(j)q.append((j,v+1))return 0
剩下的题目跟传统的BFS题思路略有不同,它不会先去取当前level的个数,而是只看队首,只要满足条件了立即返回,所以题目中会有类似“最短距离”、“最少次数”这样的字眼。
这类BFS题目几个注意点:
1.队列中存的是元组(元素,次数/步数);
2.取出队首元素,遍历队首元素的连通节点,只要找到目标节点了,立即返回;
3.元素在被添加到队列末尾的时候就加到vis集合中,第一时间标记访问,防止后面重复添加。不要在取出队首元素的时候加到vis集合中,这样有可能会重复添加同一个节点,造成重复访问。
题目参考自leetcode 灵神题单:
https://leetcode.cn/discuss/post/3581143/fen-xiang-gun-ti-dan-tu-lun-suan-fa-dfsb-qyux/