图的基础算法6 - 有向图/无向图 找环

1,977 阅读4分钟

无向图找环

  • DFS + parent node

image.png

  • union find
  • topological sort(无向图改有向图)❌

DFS + parent node

因为是双向的edge,所以用parent node保持单向的dfs遍历

对于当前节点cur的每一个neighbour邻居节点:

  • 没被visted过,看dfs(neighbour)是否为true,是的话dfs(cur)也return true
  • 被visted过,需要check是一个真的cycle还是说只是回头路。不可以走回头路,只能单向行进
    • if visited[neighbour] and neighbour != parent, then cycle found!
def isCyclicUndirectedGraph(graph: List[List[int]], n: int) -> bool:
    visited = [False] * n
    
    def isCyclicUntil(cur, parent):
        visited[cur] = True
        for neighbour in graph[cur]:
            if not visited[neighbour]:
                if isCyclicUntil(neighbour, cur):
                    return True
            elif parent != neighbour:  # cycle detected
                return True
        return False
    
    for i in range(n):
        if not visited[i]:
            if isCyclicUntil(i, -1):
                return True
    return False

union find

  • 拓扑排序
    • 对edge做去重处理,把双向的两个edge变成只剩下一个
    • 遍历所有edge,如果存在一条edge的两端已经connected,就是环
def isCyclicUndirectedGraph(graph: List[List[int]], n: int) -> bool:
    uf = UF(n)
    edges = set()
    for i in range(n):
        for nei in graph[i]:
            edge = tuple(sorted([i, nei]))
            edges.add(edge)
    for x, y in edges:
        if uf.find(x) == uf.find(y):  # if x and y are already in the same component
            return True
        uf.union(x, y)
    return False


class UF:
    def __init__(self, n):
        self.parent = [i for i in range(n)]
    
    def find(self, x) -> int:
        if self.parent[x] != x:
            self.parent[x] = self.find(self.parent[x])
        return self.parent[x]
    
    def union(self, x, y) -> None:
        self.parent[self.find(x)] = self.find(y)

261. 以图判树(Medium)

image.png

Solu 1:DFS + parent nod

照抄dfs + parent node模版

Code 1:

class Solution:
    def validTree(self, n: int, edges: List[List[int]]) -> bool:
        def buildGraph():
            graph = {i: [] for i in range(n)}
            for x, y in edges:
                graph[x].append(y)
                graph[y].append(x)
            return graph
        
        def dfs(cur, prev):
            visited[cur] = True
            for nei in graph[cur]:
                if not visited[nei]:
                    if dfs(nei, cur):
                        return True
                elif nei != prev:  # cycle detected
                    return True
            return False
        
        if len(edges) != n - 1:
            return False
        graph = buildGraph()
        visited = [False] * n
        if dfs(0, -1):  # start from 0 (the only root)
            return False
        return all(visited)  # tree只能有一个component

Solu 2:union find

照抄union-find模版

Code 2:

class Solution:
    def validTree(self, n: int, edges: List[List[int]]) -> bool:
        if len(edges) != n - 1:
            return False
        uf = UF(n)
        for x, y in edges:
            if uf.find(x) == uf.find(y):
                return False
            uf.union(x, y)
        return True


class UF:
    def __init__(self, n):
        self.parent = [i for i in range(n)]
    
    def find(self, x) -> int:
        if self.parent[x] != x:
            self.parent[x] = self.find(self.parent[x])
        return self.parent[x]
    
    def union(self, x, y) -> None:
        self.parent[self.find(x)] = self.find(y)


有向图找环

拓扑排序

  • 有环的图是无法做“拓扑排序”的
  • “拓扑排序”通常用来排序具有依赖关系的任务
    • 一个DAG的“拓扑排序”可能有多个
class Solution:
    def isCyclicDirectedGraph(self, graph: List[List[int]], n: int) -> bool:
        inDeg = [0] * n
        for i in range(n):
            for j in graph[i]:
                inDeg[j] += 1
        queue = [i for i in range(n) if inDeg[i] == 0]
        count = 0
        while queue:
            node = queue.pop(0)
            count += 1
            for nei in graph[node]:
                inDeg[nei] -= 1
                if inDeg[nei] == 0:
                    queue.append(nei)
        return count == n

210. 课程表 II(Medium)

image.png

Solu:拓扑排序

照抄拓扑排序模版

Code:

class Solution:
    def findOrder(self, numCourses: int, prerequisites: List[List[int]]) -> List[int]:
        indeg = [0] * numCourses
        graph = [[] for _ in range(numCourses)]
        for course, prerequisite in prerequisites:
            indeg[course] += 1
            graph[prerequisite].append(course)
        queue = [i for i in range(numCourses) if indeg[i] == 0]
        res = []
        while queue:
            course = queue.pop(0)
            res.append(course)
            for neighbor in graph[course]:
                indeg[neighbor] -= 1
                if indeg[neighbor] == 0:
                    queue.append(neighbor)
        return res if len(res) == numCourses else []


DFS + 双array标记

  • visited:如果一个node已经被visited过了(被check过了),就没必要再DFS一次了
    • 即:代表是否已被其他节点启动的DFS访问
  • recStack:当前recursion中,如果再次遇到了已经遇到过的node,那么有环
    • 即:代表是否已被当前节点启动的DFS访问
  • 如果是一个环哪具体包含哪些节点 or 计算cycle的大小,可以加一个prev[cur] = prevNode来保存每一步怎么走的

image.png

def isCyclicDirectedGraph(graph: List[List[int]], n: int) -> bool:
    def dfs(cur) -> bool:  # cur启动的DFS中有没有环
        if recStack[cur]:  # 被当前node启动的dfs遍历过
            return True
        if visited[cur]:  # 被其他node启动的dfs遍历过
            return False
        visited[cur] = True
        recStack[cur] = True
        for next in graph[cur]:
            if dfs(next):
                return True
        recStack[cur] = False  # backtrack
        return False
    
    visited = [False] * n
    recStack = [False] * n
    for i in range(n):
        if not visited[i] and dfs(i):
            return True
    return False

2127. 参加会议的最多员工数(Hard)

image.png

Solu:DFS + 双array

  • step 1: DFS+双array找最大环
    • 因为size = 2的cycles们可以拼在一桌说,所以要单独记下
  • step 2: DFS找size = 2的每个cycle中的两个node分别可以延长出的最长path是多少,把他们都拼到一桌上
  • step 3: res = max{最大环,所有size = 2的cycles及其extended paths的拼接}

Code:

class Solution:
    def __init__(self):
        self.sizeTwoCycle = []
        self.maxCycle = 0
        self.favorite = None
    
    def maximumInvitations(self, favorite: List[int]) -> int:
        def buildGraph():
            beloved = {i: [] for i in range(len(favorite))}
            for i, fav in enumerate(favorite):  # 被谁喜欢
                beloved[fav].append(i)
            return beloved
        
        beloved = buildGraph()
        self.favorite = favorite
        self.countCycle(beloved)
        print(self.maxCycle)
        # 尝试在size=2的cycle中,对两个node都尝试extend一条最长的path (NOT cycle)
        return max(self.maxCycle, self.sizeTwoExtend(beloved))
    
    def countCycle(self, beloved) -> None:  # find the max cycle size, and store all size=2 cycles
        def dfs(cur, cycleSize):
            if recStack[cur]:  # cycle found
                self.maxCycle = max(self.maxCycle, cycleSize)
                if cycleSize == 2:
                    self.sizeTwoCycle.append((cur, self.favorite[cur]))
                return
            if visited[cur]:
                return
            visited[cur] = True
            recStack[cur] = True
            for next in beloved[cur]:
                dfs(next, cycleSize + 1)
            recStack[cur] = False  # backtracking
        
        visited = [False] * len(beloved)
        recStack = [False] * len(beloved)
        for i in range(len(beloved)):
            if not visited[i]:
                dfs(i, 0)
    
    def sizeTwoExtend(self, beloved) -> int:  # extend the longest path from both two nodes in size=2 cycle
        def dfs(cur) -> int:  # longest path from cur
            if visited[cur]:
                return 0
            res = 0
            for next in beloved[cur]:
                res = max(res, dfs(next))
            return res + 1
        
        visited = [False] * len(beloved)
        res = 0
        for i, j in self.sizeTwoCycle:
            # handle i
            visited[j] = True
            iLongest = dfs(i)
            visited[j] = False  # backtracking
            # handle j
            visited[i] = True
            jLongest = dfs(j)
            visited[i] = False  # backtracking
            res += iLongest + jLongest  # 不会重复加上,因为每个人只喜欢一个人,所以不可能有同一个node出现在多个size=2 cycle中
        return res


Reference: