[总结]图

在图的基本算法中,最初需要接触的就是图的遍历算法,根据访问节点的顺序,可分为广度优先搜索(BFS)和深度优先搜索(DFS)。深度优先搜索,顾名思义即为一条道走到黑的搜索策略,行不通退回来换另外一条道再走到黑,依次直到搜索完成。其过程简要来说是对每一个可能的分支路径深入到不能再深入为止,而且每个节点只能访问一次。宽度优先搜索算法(又称广度优先搜索)是最简便的图的搜索算法之一,这一算法也是很多重要的图的算法的原型。Dijkstra单源最短路径算法和Prim最小生成树算法都采用了和宽度优先搜索类似的思想。其别名又叫BFS,属于一种盲目搜寻法,目的是系统地展开并检查图中的所有节点,以找寻结果。换句话说,它并不考虑结果的可能位置,彻底地搜索整张图,直到找到结果为止。
另外,拓扑图也是常见的题型。拓扑排序,是对有向无回路图进行排序,以期找到一个线性序列,这个线性序列在生活正可以表示某些事情完成的相应顺序。如果说所求的图有回路的话,则不可能找到这个序列。
特别的,并查集作为一种树型的数据结构,用于处理一些不交集(Disjoint Sets)的合并及查询问题。有一个联合-查找算法(union-find algorithm)定义了两个用于此数据结构的操作。

Dfs

[leetcode]22.Generate Parentheses

给你一个数n 写出来所有中可能的括号集合。本题采用的回溯的解题思想。具有限界条件的DFS算法称为回溯算法。所谓(回溯)Backtracking都是这样的思路:在当前局面下,你有若干种选择。那么尝试每一种选择。如果已经发现某种选择肯定不行(因为违反了某些限定条件),就返回;如果某种选择试到最后发现是正确解,就将其加入解集
对于这道题,在任何时刻,你都有两种选择:
1.加左括号。
2.加右括号。

同时有以下限制:
1.如果左括号已经用完了,则不能再加左括号了。
2.如果已经出现的右括号和左括号一样多,则不能再加右括号了。因为那样的话新加入的右括号一定无法匹配。
结束条件是:
左右括号都已经用完。

结束后的正确性:
左右括号用完以后,一定是正确解。因为1. 左右括号一样多,2. 每个右括号都一定有与之配对的左括号。因此一旦结束就可以加入解集(有时也可能出现结束以后不一定是正确解的情况,这时要多一步判断)。

class Solution(object):
    def generateParenthesis(self, n):
        res = []
        self.dfs(n,n,'',res)
        return res

    def dfs(self,left,right,result,res):
        if not left and not right:
            res.append(result)
            return
        if left > right:
            return
        if right :
            self.dfs(left,right-1,result+')',res)

        if left:
            self.dfs(left-1,right,result+'(',res)
[leetcode]37.Sudoku Solver

用回溯法,递归实现(实现简单,不如直接用栈高效),基本思路,首先找到一个待填写的小方格,9个数字依次试探填入,并行下一个方格,如果当前方格9个数字都不满足上面说的三个规则,那么恢复当前环境,并且回溯到前一个方格,依次进行,直至结束。

class Solution:
    def solveSudoku(self, board: List[List[str]]) -> None:
        """
        Do not return anything, modify board in-place instead.
        """
        self.dfs(board)

    def dfs(self, board):
        for row in range(9):
            for col in range(9):
                if board[row][col] == '.':
                    for char in '123456789':
                        board[row][col] = char
                        if self.check(board,row, col) and self.dfs(board):
                            return True
                        board[row][col] = '.'
                    return False
        return True

    def check(self, board, x, y):
        tmp = board[x][y]
        board[x][y] = '.'
        for row in range(9):
            if board[row][y] == tmp:
                return False
        for col in range(9):
            if board[x][col] == tmp:
                return False
        for row in range(3):
            for col in range(3):
                if board[(x // 3) * 3 + row][(y // 3) * 3 + col] == tmp:
                    return False
        board[x][y] = tmp
        return True
[leetcode]51.N-Queens

递归回溯问题,也可以叫做对决策树的深度优先搜索(dfs)。N皇后问题有个技巧的关键在于棋盘的表示方法,这里使用一个数组就可以表达了。

class Solution(object):
    def solveNQueens(self, n):
        board=[-1 for i in range(n)]
        res=[]
        self.dfs(0,board,[],res)
        return res

    def check(self,k, j,board):  # check if the kth queen can be put in column j!
        for i in range(k):
            if board[i]==j or abs(k-i)==abs(board[i]-j):
                return False
        return True
    def dfs(self,depth,board, result,res):
        if depth==len(board): res.append(result); return
        for i in range(len(board)):
            if self.check(depth,i,board):
                board[depth]=i
                s='.'*len(board)
                self.dfs(depth+1, board,result+[s[:i]+'Q'+s[i+1:]],res)

回溯法。深度遍历寻找单词。

class Solution:
    def exist(self, board, word):
        if not board:
            return False
        for i in range(len(board)):
            for j in range(len(board[0])):
                if self.dfs(board, i, j, word):
                    return True
        return False

    # check whether can find word, start at (i,j) position
    def dfs(self, board, i, j, word):
        if len(word) == 0: # all the characters are checked
            return True
        if i<0 or i>=len(board) or j<0 or j>=len(board[0]) or word[0]!=board[i][j]:
            return False
        tmp = board[i][j]  # first character is found, check the remaining part
        board[i][j] = "#"  # avoid visit agian
        # check whether can find "word" along one direction
        res = self.dfs(board, i+1, j, word[1:]) or self.dfs(board, i-1, j, word[1:]) \
        or self.dfs(board, i, j+1, word[1:]) or self.dfs(board, i, j-1, word[1:])
        board[i][j] = tmp
        return res
[leetcode]131.Palindrome Partitioning

DFS,每一步都需要判断是否回文。

class Solution:
    def partition(self, s: str) -> List[List[str]]:
        if not s:return []
        res = []
        self.dfs(s,[],res)
        return res

    def dfs(self,s,result,res):
        if not s:
            res.append(result)
            return
        for i in range(len(s)):
            if not self.ispalidrome(s[:i+1]):continue

            self.dfs(s[i+1:],result+[s[:i+1]],res)

    def ispalidrome(self,s):
        start,end = 0,len(s)-1
        while start<end:
            if s[start]!=s[end]:
                return False
            start += 1
            end -= 1
        return True
[leetcode]282.Expression Add Operators #前一状态

DFS.需要两个变量diff和curNum,一个用来记录将要变化的值,另一个是当前运算后的值,而且它们都需要用 long 型的,因为字符串转为int型很容易溢出,所以我们用长整型。对于加和减,diff就是即将要加上的数和即将要减去的数的负值,而对于乘来说稍有些复杂,此时的diff应该是上一次的变化的diff乘以即将要乘上的数,有点不好理解,那我们来举个例子,比如 \(2+3*2\),即将要运算到乘以2的时候,上次循环的 \(curNum = 5\), \(diff = 3\), 而如果我们要算这个乘2的时候,新的变化值diff应为 \(3*2=6\),而我们要把之前+3操作的结果去掉,再加上新的diff,即 \((5-3)+6=8\),即为新表达式 \(2+3*2\) 的值

class Solution:
    def addOperators(self, num: str, target: int) -> List[str]:
        res = []
        self.dfs(num,target,0,0,'',res)
        return res

    def dfs(self,num,target,result,pre_diff,tmp,res):
        if not num and result == target:
            res.append(tmp)
            return
        for i in range(len(num)):
            cur = num[:i+1]
            if len(cur)>1 and cu [0]=='0':
                continue
            rest_num = num[i+1:]
            if tmp:
                self.dfs(rest_num,target,result+int(cur),int(cur),tmp+'+'+cur,res)
                self.dfs(rest_num,target,result-int(cur),-int(cur),tmp+'-'+cur,res)
                self.dfs(rest_num,target,(result-pre_diff)+(pre_diff*int(cur)),pre_diff*int(cur),tmp+'*'+cur,res)
            else:
                self.dfs(rest_num,target,result+int(cur),int(cur),tmp+cur,res)
[leetcode]301.Remove Invalid Parentheses #visited

DFS,每个括号都可以选择去掉,或者不去掉来形成最后的结果。
比较难想到的一个地方是删和不删的标准。首先遍历记录下需要删除的左括号和右括号的数量.(最终解这2个都要是0)。DFS的时候,还要计算没闭合的括号的数量,因为只算上面的话,比如()())(),多一个右,最终结果可以是()())(,错误地去掉了1个右。

class Solution:
    def removeInvalidParentheses(self, s: str) -> List[str]:
        res = []
        visited = set()
        self.dfs(s, visited,res)
        return res


    def dfs(self,s, visited,res):
        m = self.calc(s)
        if m == 0:
            res.append(s)
            return

        for i in range(len(s)):
            new_s = s[:i] + s[i+1:]
            if new_s not in visited and self.calc(new_s) < m:
                visited.add(new_s)
                self.dfs(new_s, visited,res)#不能在这写  visited+[new_s]   原因是visited是作为全局共享的变量,需要上一层遍历时的值

    # Invalid Parenthes 的数目
    def calc(self,s):
        stack = []
        count = 0
        for ch in s:
            if ch=='(':
                stack.append(ch)
            if ch==')':
                if stack!=[]:
                    stack.pop()
                else:
                    count+=1
        return count+len(stack)
[leetcode]306.Additive Number

使用回溯法+合理剪枝。
因为只要判断能否构成即可,所以不需要res数组保存结果。回溯法仍然是对剩余的数字进行切片,看该部分切片能否满足条件。剪枝的方法是判断数组是否长度超过3,如果超过那么判断是否满足费布拉奇数列的规则。不超过3或者已经满足的条件下继续进行回溯切片。最后当所有的字符串被切片完毕,要判断下数组长度是否大于等于3,这是题目要求。

class Solution:
    def isAdditiveNumber(self, num: str) -> bool:
        return self.dfs(num, [])

    def dfs(self, num_str, result):
        if len(result) >= 3 and  result[-1] != result[-2] + result[-3]:
            return False
        if not num_str and len(result) >= 3:
            return True
        for i in range(len(num_str)):
            curr = num_str[:i+1]
            if (curr[0] == '0' and len(curr) != 1):
                continue
            if self.dfs(num_str[i+1:], result + [int(curr)]):
                return True
        return False
[leetcode]329.Longest Increasing Path in a Matrix #前一状态

从一个点开始,检索上下左右各个方向,如果找到有比这个数小的,那么就以这个小的数为起点,继续用相同的方法检索,直到这个数的周围没有比它更小的数了,那么从这个局部最小数开始到我们最开始的起点就构成了一个递增序列,使用完深度优先搜索我们就可以得到从这个点开始的最长序列长度。使用记忆化搜索可以节省一定的时间开销。

class Solution:
    def longestIncreasingPath(self, matrix: List[List[int]]) -> int:
        if not matrix:
            return 0
        m,n = len(matrix),len(matrix[0])
        recode = [[0 for j in range(n)] for i in range(m)]
        res = 0
        for i in range(m):
            for j in range(n):
                res = max(res,self.dfs(matrix,recode,i,j,-1))

        return res

    def dfs(self,matrix,recode,i,j,lastnum):
        m,n = len(matrix),len(matrix[0])
        if i<0 or i>len(matrix)-1 or j<0 or j>len(matrix[0])-1 or  matrix[i][j]<=lastnum:
            return 0
        if recode[i][j]!=0:
            return recode[i][j]
        left = 1+self.dfs(matrix,recode,i,j-1,matrix[i][j])
        right = 1+self.dfs(matrix,recode,i,j+1,matrix[i][j])
        top = 1+self.dfs(matrix,recode,i+1,j,matrix[i][j])
        down = 1+self.dfs(matrix,recode,i-1,j,matrix[i][j])
        recode[i][j] = max([left,right,top,down])
        return recode[i][j]

关键在于在dfs中加入上一轮搜索到的值,与这次搜索的值比较。

[leetcode]473.Matchsticks to Square

回溯法。在使用记录四组的和的方式,进行遍历的时候保存各个组的和,如果不能满足就把这个数字再加上,相当于跳过这个数字的方式。最后结束的条件就是所有的数字全部都用完了,因为如果用完了,说明我们把所有的火柴都放到了4条边中的一个,所以得到每组都满足我们条件的结论。

class Solution:
    def makesquare(self, nums: List[int]) -> bool:
        if not nums or len(nums) < 4: return False
        _sum = sum(nums)
        if _sum%4 != 0 or max(nums) > _sum / 4: return False

        div = _sum//4

        nums.sort(reverse = True)
        target = [div] * 4
        return self.dfs(nums, 0, target)

    def dfs(self, nums, index, target):
        if index == len(nums): return True
        num = nums[index]
        for i in range(4):
            if target[i] >= num:
                target[i] -= num
                if self.dfs(nums, index + 1, target): return True
                target[i] += num
        return False

Dfs+DP

[leetcode]140.Word Break II

使用word break题中的动态规划的结果,在dfs之前,先判定字符串是否可以被分割,如果不能被分割,直接跳过这一枝。实际上这道题是Dfs+DP。

class Solution:
    def wordBreak(self, s: str, wordDict: List[str]) -> List[str]:
        res = []
        self.dfs(s, wordDict, [],res)
        return res

    def check(self, s, dic):
        dp = [False for i in range(len(s)+1)]
        dp[0] = True
        for i in range(1, len(s)+1):
            for k in range(i):
                if dp[k] and s[k:i] in dic:
                    dp[i] = True
        return dp[len(s)]

    def dfs(self, s, dic, result,res):
        if not s:
            res.append(' '.join(result))
            return
        if not self.check(s,dic):
            return
        for i in range(1, len(s)+1):
            if s[:i] in dic and self.check(s,dic):
                self.dfs(s[i:], dic, result+[s[:i]],res)

Dfs+hash

[leetcode]464.Can I Win

两个人玩游戏,从1~maxChoosableInteger中任选一个数字,第一个人先选,第二个人后选,每个人选过的数字就不能再选了~两个人谁先加起来总和超过desiredTotal谁就赢,问给出数字,第一个人能否赢得比赛。传入两个值,一个是想要达到(或者说超过,也就是大于等于啦)的目标数字target,另一个是visited数字标记哪些数字被选过了。
用map标记当前情况在map表中是否存在,存在的话结果保存在map里面~如果我们发现这个visited也就是这个数字选择的情况已经被保存过了,就直接返回在map里面保存的结果。

class Solution:
    def canIWin(self, maxChoosableInteger: int, desiredTotal: int) -> bool:
        if maxChoosableInteger>=desiredTotal:
            return True
        if (1 + maxChoosableInteger) * maxChoosableInteger/2 < desiredTotal:
            return False
        memo = {}
        nums = [i+1 for i in range(maxChoosableInteger)] #list(range(1, maxChoosableInteger + 1))
        return self.dfs(nums,memo, desiredTotal)


    def dfs(self, nums,memo, desiredTotal):
        if not nums:
            return  False
        hash = str(nums)
        if hash in memo:
            return memo[hash]

        if nums[-1] >= desiredTotal:
            memo[hash]= True
            return True

        for i in range(len(nums)):
            if not self.dfs(nums[:i] + nums[i+1:],memo, desiredTotal - nums[i]):
                memo[hash]= True
                return True
        memo[hash] = False
        return False
[leetcode]508.Most Frequent Subtree Sum

给出一个树的根节点,要求你找到出现最频繁的子树和。一个节点的子树和是指其所有子节点以及子节点的子节点的值之和(包含节点本身)。怎么记录所有子树和呢?这道题既然是要求找出出现频率最高的子树和值,那肯定要记录各个值出现的次数,方法也就呼之欲出了,用HashMap,以子树和为key,以出现次数为value,对于已经出现过的子树和,就将其value+1,没出现过的就添加到HashMap中去,其value设为1。这样就可以边计算所有子树和,边记录各个和出现的次数了。

# Definition for a binary tree node.
# class TreeNode:
#     def __init__(self, x):
#         self.val = x
#         self.left = None
#         self.right = None

import collections
class Solution:
    def findFrequentTreeSum(self, root: TreeNode) -> List[int]:
        if not root:return []
        hashmap = collections.defaultdict(int)
        self.dfs(root,hashmap)
        res = []
        max_fre = 0
        for key in hashmap:
            if hashmap[key] > max_fre:
                max_fre = hashmap[key]
                res = [key]
            elif hashmap[key] == max_fre:
                res.append(key)
        return res


    def dfs(self,root,hashmap):
        res = root.val
        if root.left:
            res += self.dfs(root.left,hashmap)
        if root.right:
            res += self.dfs(root.right,hashmap)
        hashmap[res] += 1
        return res

Dfs+set

[leetcode]491.Increasing Subsequences

Dfs。为了避免重复值的出现,这里用于记录的list改为了set。

class Solution:
    def findSubsequences(self, nums: List[int]) -> List[List[int]]:
        res = set()
        self.dfs(nums, [],res)
        return list(map(list, res))

    def dfs(self, nums,  result,res):
        if len(result) >= 2:
            res.add(tuple(result))
        for i in range(len(nums)):
            if not result or nums[i] >= result[-1]:
                self.dfs(nums[i+1:],  result + [nums[i]] ,res)

BFS

[leetcode]127.Word Ladder

使用bfs来进行搜索,寻找到出发点到目的地的最小路径

class Solution:
    def ladderLength(self, beginWord: str, endWord: str, wordList: List[str]) -> int:
        wordset = set(wordList)
        # if endWord not in wordset:return 0
        bfs = collections.deque()
        bfs.append((beginWord, 1))
        while bfs:
            word, length = bfs.popleft()
            if word == endWord:
                return length
            for i in range(len(word)):
                for c in "abcdefghijklmnopqrstuvwxyz":
                    newWord = word[:i] + c + word[i + 1:]
                    if newWord in wordset: #and newWord != word:
                        wordset.remove(newWord)
                        bfs.append((newWord, length + 1))
        return 0
[leetcode]200.Number of Islands

这就是很典型的深度优先广度优先了,可以看作简易版的求连通分量个数。

class Solution:
    # O(m*n)
    def numIslands(self, grid: List[List[str]]) -> int:
        if not grid:return 0
        m,n = len(grid),len(grid[0])
        res = 0
        for i in range(m):
            for j in range(n):
                if grid[i][j] == '1':
                    self.bfs(grid,i,j)
                    res += 1
        return res

    def bfs(self,grid,i,j):
        if i>len(grid)-1 or i<0 or j>len(grid[0])-1 or j<0 or grid[i][j] == '0':
            return
        grid[i][j] = '0'
        self.bfs(grid,i+1,j)
        self.bfs(grid,i-1,j)
        self.bfs(grid,i,j+1)
        self.bfs(grid,i,j-1)
[leetcode]542.01 Matrix

给定一个只含0和1的矩阵,找到每个1到0的最短距离。 两个相邻单元格的距离是1.
思路:每一轮以影响值来影响四周的目标值,如果目标值大于影响值+1,则目标值等于影响值+1。用形象的比喻来描述这道题的话,就是一颗石头扔入湖中时,它的影响波及周围,且向四周不断传播它的影响。

class Solution(object):
    def updateMatrix(self, matrix):
        """
        :type matrix: List[List[int]]
        :rtype: List[List[int]]
        """

        if not matrix or not matrix[0]:
            return matrix

        queue = []
        for i in range(len(matrix)):
            for j in range(len(matrix[0])):
                if matrix[i][j] == 0:
                    queue.append((i, j))
                else:
                    matrix[i][j] = float('inf')
        direction = [(-1, 0), (1, 0), (0, -1), (0, 1)]
        while queue:
            i, j = queue.pop(0)
            for i_delta, j_delta in direction:
                i_new = i + i_delta
                j_new = j + j_delta
                if i_new >= 0 and i_new <= len(matrix) - 1 and j_new >= 0 and j_new <= len(matrix[0]) - 1 and matrix[i_new][j_new] > matrix[i][j] + 1:
                    matrix[i_new][j_new] = matrix[i][j] + 1
                    queue.append((i_new, j_new))
        return matrix

拓扑图

[leetcode]133.Clone Graph

先克隆每个节点的label,同时建立克隆后的老节点与新节点的对应关系。在所有节点克隆完成后遍历所有新节点,依次按照对应关系给节点的neighbors赋值。

"""
# Definition for a Node.
class Node:
    def __init__(self, val, neighbors):
        self.val = val
        self.neighbors = neighbors
"""
class Solution:

    # def __init__(self):
    #     self.dict={None:None}
    def cloneGraph(self, node: 'Node') -> 'Node':
        cp = collections.defaultdict(lambda: Node(0, []))
        nodes = [node]
        seen = set()
        while nodes:
            n = nodes.pop(0)
            cp[n].val = n.val
            cp[n].neighbors = [cp[x] for x in n.neighbors]
            nodes += [x for x in n.neighbors if x not in seen]
            seen.add(n)
        return cp[node]
[leetcode]207.Course Schedule

这道题是拓扑排序的典型应用。
如果一个有向图的任意顶点都无法通过一些有向边回到自身,那么称这个有向图为有向无环图(DAG)。
拓扑排序是将有向无环图G的所有顶点排成一个线性序列,使得对图G中的任意两个顶点u、v,如果存在边u->v,那么在序列中u一定在v前面。这个序列又被称为拓扑序列。判断一个有向图是否有环,有两个算法:

  1. 拓扑排序
    即找出该图的一个线性序列,使得需要事先完成的事件总排在之后才能完成的事件之前。如果能找到这样一个线性序列,则该图是一个有向无环图
    时间复杂度是O(N ^ 2),空间复杂度是O(N)

  2. DFS
    遍历图中的所有点,从i点出发开始DFS,如果在DFS的不断深入过程中又回到了该点,则说明图中存在回路。
    拓扑排序算法的实现: 每次从图中去掉一个入度为0的点,去掉该点后,该点指向的点的入度-1。如果最后所有的点都被去掉了,则说明该图是dag
    时间复杂度是O(N),空间复杂度是O(N)。

class Solution:
    def canFinish(self, numCourses: int, prerequisites: List[List[int]]) -> bool:
        graph = collections.defaultdict(list)
        for u, v in prerequisites:
            graph[v].append(u)
        # 0 = Unknown, -1 = visiting, 1 = visited
        visited = [0] * numCourses
        for i in range(numCourses):
            if not self.dfs(graph, visited, i):
                return False
        return True

    # Can we add node i to visited successfully?
    def dfs(self, graph, visited, i):
        if visited[i] == -1: return False
        if visited[i] == 1: return True
        visited[i] = -1
        for j in graph[i]:
            if not self.dfs(graph, visited, j):
                return False
        visited[i] = 1
        return True


#         graph = collections.defaultdict(list)
#         indegrees = collections.defaultdict(int)
#         for u, v in prerequisites:
#             graph[v].append(u)
#             indegrees[u] += 1
#         for i in range(numCourses):
#             zeroDegree = False
#             for j in range(numCourses):
#                 if indegrees[j] == 0:
#                     zeroDegree = True
#                     break
#             if not zeroDegree: return False
#             indegrees[j] = -1
#             for node in graph[j]:
#                 indegrees[node] -= 1
#         return True
[leetcode]210.Course Schedule II

拓扑排序,BFS 时间复杂度是O(N ^ 2),空间复杂度是O(N)
DFS 时间复杂度是O(N),空间复杂度是O(N)。

class Solution:
    def findOrder(self, numCourses: int, prerequisites: List[List[int]]) -> List[int]:
        graph = collections.defaultdict(list)
        indegrees = collections.defaultdict(int)
        for u, v in prerequisites:
            graph[v].append(u)
            indegrees[u] += 1
        res = []
        for i in range(numCourses):
            zeroDegree = False
            for j in range(numCourses):
                if indegrees[j] == 0:
                    zeroDegree = True
                    break
            if not zeroDegree:
                return []
            indegrees[j] -= 1
            res.append(j)
            for node in graph[j]:
                indegrees[node] -= 1
        return res


#         graph = collections.defaultdict(list)
#         for u, v in prerequisites:
#             graph[u].append(v)
#         # 0 = Unknown, -1 = visiting, 1 = visited
#         visited = [0] * numCourses
#         res = []
#         for i in range(numCourses):
#             if not self.dfs(graph, visited, i, res):
#                 return []
#         return res

#     def dfs(self, graph, visited, i, res):
#         if visited[i] == -1: return False
#         if visited[i] == 1: return True
#         visited[i] = -1
#         for j in graph[i]:
#             if not self.dfs(graph, visited, j, res):
#                 return False
#         visited[i] = 1
#         res.append(i)
#         return True

[leetcode]310.Minimum Height Trees

使用类似与拓扑排序的BFS进行解决。
在这个题中,因为我们要找到无向图最靠近中间的节点,所以,我们先使用一个字典保存每个节点的所有相邻节点set。每次把所有只有一个邻接的节点(叶子节点,类似于入度为0,但是这是个无向图,入度等于出度)都放入队列,然后遍历队列中的节点u,把和每个节点u相邻的节点v的set删去u,所以这一步操作得到的是去除了叶子节点的新一轮的图。所以我们需要再次进行选择只有一个邻接节点的叶子节点,然后放入队列中,再次操作。最后结束的标准是,整个图只留下了1个或者两个元素。为什么不能是3个呢?因为题目第一句话说了给出的图是具有树的特性的,所以一定没有环存在。
这个题整体的思路就是把所有的叶子节点放入队列中,然后同时向中间遍历,这样最后剩下来的就是整棵树中间的元素。
时间复杂度是O(V),空间复杂度是O(E + V).

class Solution:
    def findMinHeightTrees(self, n: int, edges: List[List[int]]) -> List[int]:
        if n == 1: return [0]
        leaves = collections.defaultdict(set)
        for u, v in edges:
            leaves[u].add(v)
            leaves[v].add(u)
        queue = collections.deque()
        for u, vs in leaves.items():
            if len(vs) == 1:
                queue.append(u)
        while n > 2:
            n -= len(queue)
            for _ in range(len(queue)):
                u = queue.popleft()
                for v in leaves[u]:
                    leaves[v].remove(u)
                    if len(leaves[v]) == 1:
                        queue.append(v)
        return list(queue)

并查集

并查集是一种树型的数据结构,用于处理一些不交集(Disjoint Sets)的合并及查询问题。有一个联合-查找算法(union-find algorithm)定义了两个用于此数据结构的操作:

  • Find:确定元素属于哪一个子集。这个确定方法就是不断向上查找找到它的根节点,它可以被用来确定两个元素是否属于同一子集。
  • Union:将两个子集合并成同一个集合。

由于支持这两种操作,一个不相交集也常被称为联合-查找数据结构(union-find data structure)或合并-查找集合(merge-find set)。其他的重要方法,MakeSet,用于建立单元素集合。有了这些方法,许多经典的划分问题可以被解决。
为了更加精确的定义这些方法,需要定义如何表示集合。一种常用的策略是为每个集合选定一个固定的元素,称为代表,以表示整个集合。接着,Find(x) 返回 x 所属集合的代表,而 Union 使用两个集合的代表作为参数。

[leetcode]399.Evaluate Division

典型的并查集应用。注意每个节点需要存储节点表示与节点的值。

class Solution(object):
    def calcEquation(self, equations, values, queries):
        """
        :type equations: List[List[str]]
        :type values: List[float]
        :type queries: List[List[str]]
        :rtype: List[float]
        """

        dic = {}
        result = []

        #UF的init初始化,每个节点的根节点为自身,根节点就是组号
        for node1,node2 in equations:
            dic[node1] = [node1,1]
            dic[node2] = [node2,1]

        def find(p):
            value = 1
            while p!=dic[p][0]:
                value*=dic[p][1]
                p = dic[p][0]
            return p,value
        def union(p,q,value):
            root1,value1 = find(p)
            root2,value2 = find(q)
            dic[root1] = [root2,value*value2/value1]
        def query(p,q):
            if p not in dic or q not in dic:
                return -1.0
            root1,value1 = find(p)
            root2,value2 = find(q)
            if root1!=root2:
                return -1.0
            else:
                return value1/value2
        #构建图
        for i in range(len(values)):
            union(equations[i][0],equations[i][1],values[i])
        for querie in queries:
            result.append(query(querie[0],querie[1]))
        return result
[leetcode]547.Friend Circles

给出一个N*N的矩阵M,如果M[i][j]1,说明i和j有直接朋友关系,如果M[i][j]1并且M[j][k]==1那么i和k就有间接朋友关系。求所有人之间的朋友圈子有几个。
可以利用并查集,把有有关系的人放到一个集合里,然后计算集合的个数。

class Solution(object):
    def findCircleNum(self, M):
        """
        :type M: List[List[int]]
        :rtype: int
        """
        dic = {}
        n = len(M)

        def find(p):
            while p != dic[p]:
                p = dic[p]
            return p

        def union(p,q):
            root1,root2 = find(p),find(q)
            dic[root1] = root2

        for i in range(n):
            dic[i] = i
        for i in range(n):
            for j in range(i+1,n):
                if M[i][j]:
                    union(i,j)
        res = 0
        for i in range(n):
            if dic[i] == i:
                res += 1
        return res

集合的个数就是指向非本节点的节点个数。

参考:
算法:并查集
超有爱的并查集~

posted @ 2019-10-19 11:19  Jamest  阅读(213)  评论(0编辑  收藏  举报