算法刷题总结 (二) 回溯与深广搜与并查集算法

算法总结2 回溯与深广搜算法

在这里插入图片描述

一、理解回溯算法

回溯与深广搜有相似的做法和理解,所以把他们放在同一个文章之中,文章看似篇幅很长,实际上,题目都是相似的,顺着章节来可以很快的掌握这个算法内容,以后碰到这样的相似题目,会很快想出思路。

1.1、回溯的概念

回溯是递归的纵横拓展,主要是递归(纵)+局部暴力枚举(横)。所以可以从递归和暴力两个方面来拆解回溯问题。
在这里插入图片描述
由上图可知,回溯法的所有问题都可以抽象为树形结构,集合的大小构成了树的宽度,递归深度构成了树的深度(N叉树)。因为递归有终止条件,所以树的高度会有限,同时递归的最终结果会呈现在叶子节点上。


1.2、回溯法的效率

从上一节的概念可知,回溯=递归+暴力搜索,所以它并不是一个特别高效的算法,即便再做一些剪枝优化下,依旧改变不了它的整体就是穷举的特性。

但即便这个算法效率不高,它的使用依旧很广泛,因为很多问题除了穷举,实在就是没有别的解决办法了,具体可以看看下一章的例题感受下。

在比赛中,有时使用dfs会超时,这时除了换用另一种解法比如dp外,还可以在dfs函数前加上语法糖@lru_cache(None),它可以去掉太长时间没有使用的内存块。

LRU (Least Recently Used,最近最少使用) 算法是一种缓存淘汰策略。其根据数据的历史访问记录来进行淘汰,核心思想是,“如果数据最近被访问过,那么将来被访问的几率也更高”。该算法最初为操作系统中一种内存管理的页面置换算法,主要用于找出内存中较久时间没有使用的内存块,将其移出内存从而为新数据提供空间。

比如leetcode比赛中的这道题,如果加上@lru_cache(None)才能通过,否则会超时。
6433. 矩阵中移动的最大次数
当时在做的时候,想着是不是某些关键部分需要剪枝,但是想了想并没有要剪枝的部分,又换成dp去做的,如果早点知道这个小技巧,会省下不少时间。


1.3、回溯法问题分类

回溯法主要解决以下五种问题:

问题描述
组合问题N个数里面按一定规则找出k个数的集合
切割问题一个字符串按一定规则有几种切割方式
子集问题一个N个数的集合里有多少符合条件的子集
排列问题N个数按一定规则全排列,有几种排列方式
棋盘问题N皇后,解数独,迷宫等等

1.4、回溯法的做题步骤

回溯法三部曲:

  1. 回溯函数的模板,返回值以及参数
    这里回溯函数起名为backtracking,回溯算法中函数返回值一般为空。
    对于参数的话,很难提前确定下来,一般是先写逻辑,然后需要什么参数,就填什么参数。
def backtracking(参数):
  1. 回溯函数终止条件
    什么时候达到了终止条件,树中就可以看出,一般来说搜到叶子节点了,也就找到了满足条件的一条答案,把这个答案存放起来,并结束本层递归。
if 终止条件:
	保存结果
	return
  1. 回溯搜索的遍历过程
# 横向遍历。一个节点有多少个孩子就执行多少次
for i in [本层集合中的元素(树中及节点孩子的数量就是集合的大小)]:
	处理节点
	# 纵向遍历,自己调用自己实现递归
	backtracking(路径,选择列表)
	回溯,撤销处理结果

总结:

def backtracking(参数):
	if 终止条件:
		存放结果
		return
		
	for i in [本层集合中的元素(树中及节点孩子的数量就是集合的大小)]:
		处理节点
		# 纵向遍历,自己调用自己实现递归
		backtracking(路径,选择列表)
		回溯,撤销处理结果


二、经典问题

在这里插入图片描述

2.1、组合问题

在这里插入图片描述

2.1.1、77. 组合 - 值不重复

力扣题目链接

1.使用函数:
itertools库下的combinations组合函数,与之对应的是permutations排列函数。

from itertools import combinations
class Solution:
    def combine(self, n: int, k: int) -> List[List[int]]:
        l = [i+1 for i in range(n)]
        res = combinations(l, k)
        return list(res)

2.回溯法:
从题目可以知道,k=2两层循环解决,k=3三层循环,但是题目的k是变动的,也就是for循环的层数是需要是变动的,那么回溯法就用递归来解决层数嵌套,每递归一次就是一层for循环,一个简单的过程如下:
在这里插入图片描述
n相当于树的宽度,k相当于树深度的

class Solution:
    def combine(self, n: int, k: int) -> List[List[int]]:
        # 存放符合条件结果的集合
        result = []
        # 存放符合条件单一结果
        path = []
        # startIndex来记录下一层递归,搜索的起始位置
        def backtracking(n, k, startIndex):
            if len(path)==k:
            	# 这里注意要添加一个path的拷贝,直接append为原址
            	# path最后会被pop,结果为空值
                result.append(path[:])
                return
            for i in range(startIndex, n+1):
                path.append(i)
                backtracking(n, k, i+1)
                path.pop()
        backtracking(n, k, 1)
        return result
            

加上剪枝操作(当path为固定长度时):
如果for循环选择的起始位置之后的元素个数 已经不足 我们需要的元素个数了,那么就没有必要搜索了。那么后面这些部分可以剪掉,如下,只用更改循环的终止条件改变范围就行。

# 这里
# n - (k-len(path))+1恰好满足最后一个长度到结尾
# 再往右就不满足了
# n = 4, k = 2, 当len(path) = 1 时
# 4 - (2 - 1) +1 = 4,从startindex到4都是可以取的
endIndex = n - (k-len(path))+1
for i in range(startIndex, endIndex+1):
    path.append(i)
    backtracking(n, k, i+1)
    path.pop()

当然,这个剪枝是针对,有固定长度k才可取。

综上为:

class Solution:
    def combine(self, n: int, k: int) -> List[List[int]]:
        # 存放符合条件结果的集合
        result = []
        # 存放符合条件单一结果
        path = []
        # startIndex来记录下一层递归,搜索的起始位置
        def backtracking(n, k, startIndex):
            if len(path)==k:
                result.append(path[:])
                return
            # n = 4, k = 2, 当len(path) = 1 时
            # 4 - (2 - 1) +1 = 4,从startindex到4都是可以取的
            endIndex = n - (k-len(path))+1
            for i in range(startIndex, endIndex+1):
                path.append(i)
                backtracking(n, k, i+1)
                path.pop()
        backtracking(n, k, 1)
        return result
            

2.1.2、216.组合总和III - 值不重复且等于目标值

力扣题目链接

1.使用函数:

from itertools import combinations
class Solution:
    def combinationSum3(self, k: int, n: int) -> List[List[int]]:
        res = []
        l = combinations([i+1 for i in range(9)], k)
        for i in list(l):
            if sum(i)==n:
                res.append(i)
        return res

2.回溯搜索:

class Solution:
    def combinationSum3(self, k: int, n: int) -> List[List[int]]:
        res = []
        path = []

        def backtracking(ind):
            if len(path)==k:
                if sum(path)==n:
                    res.append(path[:])
                return
            # 剪枝1,同前面的原理
            endindex = 9-(k-len(path))+1
            for i in range(ind, endindex+1):
                path.append(i)
                # 剪枝2,和大了就不继续回溯,这个放前面,backtracking下第一行也行
                if sum(path)<=n:
                    backtracking(i+1)
                path.pop()
        
        backtracking(1)
        return res

加上两次剪枝:

class Solution:
    def combinationSum3(self, k: int, n: int) -> List[List[int]]:
        path = []
        res = []
        def backtracking(k, n, startIndex):
            # 剪枝1,大于目标值,再遍历也没意义,直接去掉
            if sum(path)>n:
                return
            if len(path)==k and sum(path)==n:
                res.append(path[:])
                return
            # 剪枝2,需要搭配剪枝1,否则k-len(path)会为负数,循环次数反而会增加
            endIndex = 9 - (k-len(path))+1
            for i in range(startIndex, endIndex+1):
                path.append(i)
                backtracking(k, n, i+1)
                path.pop()
        backtracking(k, n, 1)
        return res

本题与77.组合 加了元素总和的限制,同样的,把问题抽象为树形结构,按照回溯算法的步骤走,最后给出剪枝的优化。


在这里插入图片描述

2.1.3、17. 电话号码的字母组合 - 双层回溯

力扣题目链接

回溯法1,从digits的角度:

class Solution:
    def letterCombinations(self, digits: str) -> List[str]:
        d = {'2':'abc', '3':'def', '4':'ghi', '5':'jkl', '6':'mno', '7':'pqrs','8':'tuv','9':'wxyz'}
        # 存储根节点
        path = []
        # 存储最终结果
        res = []

        # 回溯数字
        def backtracking(digits):
            if len(digits)==0:
                # 去除空值,非空则添加字符串
                if path:
                    res.append(''.join(path[:]))
                return
            # 循环取一个数字对应的字母
            for i in d[digits[0]]:
                # 存一个该数字的字母
                path.append(i)
                # 去除第一个数字,后面的数字串
                backtracking(digits[1:])
                path.pop()
        
        backtracking(digits)
        return res

回溯法2:从index的角度:

class Solution:
    def letterCombinations(self, digits: str) -> List[str]:
        alphas = {"2":"abc","3":"def","4":"ghi","5":"jkl","6":"mno","7":"pqrs",
        "8":"tuv","9":"wxyz"}

        path = []
        res = []

        def backtracking(ind):
            if len(path)==len(digits):
                if path:
                    res.append(''.join(path[:]))
                return
            for a in alphas[digits[ind]]:
                path.append(a)
                backtracking(ind+1)
                path.pop()

        backtracking(0)
        return res

在这里插入图片描述
注意:输入1 * #按键等等异常情况。代码中最好考虑这些异常情况,但题目的测试数据中应该没有异常情况的数据,但是要知道会有这些异常,如果是现场面试中,就一定要考虑到。


2.1.4、39. 组合总和 - 可复选同一值

力扣题目链接

回溯法1,candidates角度:

class Solution:
    def combinationSum(self, candidates: List[int], target: int) -> List[List[int]]:
        path = []
        res = []

        def backtracking(candidates):
        	# 大于目标值直接返回空
            if sum(path)>=target:
                if sum(path)==target and sorted(path) not in res:
                	# 值存进去之前,提前排序,便于去重
                    res.append(sorted(path[:]))
                return
			# 循环整个列表(注意这里会产生重复结果,只是顺序不同)
            for i in candidates:
                path.append(i)
                backtracking(candidates)
                path.pop()
        
        backtracking(candidates)
        return res

回溯法2,index角度:
这里加上循环的起始索引,并引入一个su_m变量去存储每一步的sum。

class Solution:
    def combinationSum(self, candidates: List[int], target: int) -> List[List[int]]:
        path = []
        res = []

        def backtracking(candidates, su_m, startIndex):
            # 因为循环进行了优化,所以这里判断会简单许多
            if su_m>=target:
                if su_m==target:
                    res.append(path[:])
                return
            # [2,3,5,7], 7
            # 这种循环索引而不是列表值,会优先考虑重复
            # 所以不会出现[2,3,2]和[3,2,2]这种重复的排列
            # 优先[2,2,2,2]得不到target开始pop
            for i in range(startIndex, len(candidates)):
                su_m += candidates[i]
                path.append(candidates[i])
                # 这里起始索引为i,因为可以重复使用
                backtracking(candidates, su_m, i)
                # 回溯要减去值
                su_m -= candidates[i]
                path.pop()
        
        backtracking(candidates, 0, 0)
        return res

这里还可以简化下:

class Solution:
    def combinationSum(self, candidates: List[int], target: int) -> List[List[int]]:
        res = []
        path = []

        def backtracking(ind):
            if sum(path)==target:
                res.append(path[:])
                return
            
            for i in range(ind, len(candidates)):
                path.append(candidates[i])
                if sum(path)<=target:
                    backtracking(i)
                path.pop()
        # 这里直接一个index即可
        backtracking(0)
        return res

回溯1会产生排列,要去重,而回溯2只产生组合。
在这里插入图片描述
剪枝优化:
当判断sum > target后,就没有必要进入下一层递归。

对总集合排序之后,如果下一层的sum(就是本层的 sum + candidates[i])已经大于target,就可以结束本轮for循环的遍历。
在这里插入图片描述

class Solution:
    def combinationSum(self, candidates: List[int], target: int) -> List[List[int]]:
        path = []
        res = []

        def backtracking(candidates, su_m, startIndex):
            if su_m==target:
                res.append(path[:])
                return

            for i in range(startIndex, len(candidates)):
                # 排序后,小的值的和都大于target,后续大的无序继续
                # 不排序,结果会出问题
                if su_m + candidates[i]> target:
                    return
                su_m += candidates[i]
                path.append(candidates[i])
                backtracking(candidates, su_m, i)
                su_m -= candidates[i]
                path.pop()
        # 注意要排序
        candidates.sort()
        backtracking(candidates, 0, 0)
        return res

2.1.5、40.组合总和II - 不可复选同一值,但有重复元素

力扣题目链接
本题的难点在于:集合(数组candidates)有重复元素,但还不能有重复的组合。

回溯法1(超时):
把所有组合求出来,去除重复的。

class Solution:
    def combinationSum2(self, candidates: List[int], target: int) -> List[List[int]]:
        path = []
        res = []
        
        def backtracking(candidates, su_m, startIndex):
            # 这里会有重复的,如[1,1,2,5,6,7,10],8
            # 第一个1与第二个1意义不同,[1,2,5] 和 [1,2,6], [1,7]和[1,7]
            # 但对结果来说相同。
            if su_m == target and path[:] not in res:
                res.append(path[:])
                return 
            for i in range(startIndex, len(candidates)):
                if su_m + candidates[i]>target:
                    return
                su_m += candidates[i]
                path.append(candidates[i])
                backtracking(candidates, su_m, i+1)       
                su_m -= candidates[i]
                path.pop()
        candidates.sort()
        backtracking(candidates, 0, 0)
        return res

这里会有重复的,如[1,1,2,5,6,7,10],8。

第一个1与第二个1意义不同,[1,2,5] 和 [1,2,6], [1,7]和[1,7],但对结果来说相同。

所以,只需要第一次的1就可以,同一树层,第一次出现的重复元素会遍历到后面的重复元素,记一次就行,树层的其他相同的直接去掉。

把所有组合求出来,再去重,这么做很容易超时,所以要在搜索的过程中就去掉重复组合。

回溯法2(使用startIndex去重):
既然求结果去重会超时,那么就需要在求的过程中提前去重。

这里直接使用startIndex进行判断去重,同一个树层(for循环内)和上一个元素相等则continue跳过。

class Solution:
    def combinationSum2(self, candidates: List[int], target: int) -> List[List[int]]:
        path = []
        res = []
        
        def backtracking(startIndex):
            # 这里会有重复的,如[1,1,2,5,6,7,10]
            # 第一个1与第二个1意义不同,但对结果来说相同
            if sum(path) == target:
                res.append(path[:])
                return 
            for i in range(startIndex, len(candidates)):
                # 只是用索引来判断重复。这里注意不是i>0
                # 因为每一层的元素有更新,得从startIndex开始
                # 否则i>0则不存在重复元素了
                if i>startIndex and candidates[i] == candidates[i-1]:
                    continue
                su_m += candidates[i]
                path.append(candidates[i])
                if sum(path)<=target:
                	backtracking(i+1)       
                su_m -= candidates[i]
                path.pop()
        candidates.sort()
        backtracking(0)
        return res

回溯法3(新建used数组去重):
used[i - 1] == true,说明同一树枝candidates[i - 1]使用过,纵向
used[i - 1] == false,说明同一树层candidates[i - 1]使用过,横向

class Solution:
    def combinationSum2(self, candidates: List[int], target: int) -> List[List[int]]:
        path = []
        res = []
        used = [False] * len(candidates)
        
        def backtracking(startIndex):
            # 这里会有重复的,如[1,1,2,5,6,7,10]
            # 第一个1与第二个1意义不同,但对结果来说相同
            if sum(path) == target:
                res.append(path[:])
                return 
            for i in range(startIndex, len(candidates)):
                # 上个相同节点没使用过,为False,可以判断为同一树层,则跳过
                # 上个相同节点使用过,为True,可以判断为同一树枝,不跳
                
                # 这里注意不同于前面i>startIndex
                # 本来i>0不允许任何重复,但used[i-1]==False,表示同一层,从这个相同点开始,(前面一个相同点已经回溯成False了)
                if i>0 and candidates[i] == candidates[i-1] and used[i-1]==False:
                    continue
                    
                used[i]=True
                path.append(candidates[i])
                if sum(path)<=target:
                	backtracking(i+1)    
                # 回溯,为了下一轮for loop   
                used[i]=False
                su_m -= candidates[i]
                path.pop()
        candidates.sort()
        backtracking(0)
        return res

在这里插入图片描述

2.1.6、练习
22. 括号生成

22. 括号生成

class Solution:
    def generateParenthesis(self, n: int) -> List[str]:
        res = []
        path = []
        
        def backtracking(l, r):
            if l==n and r==n:
                res.append(''.join(path))
            
            if l<n:
                path.append('(')
                backtracking(l+1,r)
                path.pop()
            if r<l:
                path.append(')')
                backtracking(l,r+1)
                path.pop()

        backtracking(0,0)
        return res


在这里插入图片描述

2.2、分割问题

2.2.1、131. 分割回文串 - 回溯附加条件

力扣题目链接

回溯+双指针:

class Solution:
    def partition(self, s: str) -> List[List[str]]:
        res = []
        path = []
        # 判断回文的函数
        # 正反序
#        def isPalindrome(s, start, end):
#            if s[start:end+1] == s[start:end+1][::-1]:
#                return True
#            return False
        # 双指针
        def isPalindrome(s, start, end):
            while start<end:
                if s[start] != s[end]:
                    return False
                start+=1
                end-=1
            return True
        def backtracking(s, startIndex):
            if startIndex>= len(s):
                res.append(path[:])
                return
            for i in range(startIndex, len(s)):
                # 判断是否为回文,是再添加到path
                # 不是回文,则切割失败,后续操作跳过
                if isPalindrome(s, startIndex, i):
                    path.append(s[startIndex:i+1])
                else:
                    continue
                backtracking(s, i+1)
                path.pop()
        
        backtracking(s, 0)
        return res

在这里插入图片描述
回溯+正反序:

class Solution:
    def partition(self, s: str) -> List[List[str]]:
        res = []
        path = []

        def backtracking(ind):
            if ind==len(s):
                res.append(path[:])
            
            for i in range(ind, len(s)):
                if s[ind:i+1]!=s[ind:i+1][::-1]:
                    continue
                path.append(s[ind:i+1])
                backtracking(i+1)
                path.pop()
        backtracking(0)
        return res

在这里插入图片描述

2.2.2、93.复原IP地址 - 回溯附加条件

力扣题目链接

回溯法1, 使用index:
思路同上一题,切割成4份为终止标准,结合startIndex判断是否遍历完整字符串,将合法的结果用’.'拼接即可。

class Solution:
    def restoreIpAddresses(self, s: str) -> List[str]:
        res = []
        path = []
        # 判断是否合法
        # 1. 0-255 之间,闭区间
        # 2. 当长度大于1时(或不为0),首位不能为0
        def isValid(s, start, end):
            if 0<=int(s[start:end+1])<=255 and \
            (s[start:end+1]=='0' or s[start:end+1].lstrip('0')==s[start:end+1]):
                # 首位不能为0,则去除首位的0后还是原来的字符串则首位无0
                return True
            return False

        def backtracking(startIndex):
            # 终止条件为截取了4段
            if len(path)==4:
                # 截取4段,并且索引到了尾部,即取了所有字符
                if startIndex == len(s):
                    # 将结果用.拼接起来
                    res.append('.'.join(path[:]))
                return 
            # 遍历字符串
            for i in range(startIndex, len(s)):
                if isValid(s, startIndex, i) and len(path)<=4:
                    path.append(s[startIndex:i+1])
                	backtracking(i+1)
                	path.pop()
        
        backtracking(0)
        return res

在这里插入图片描述
回溯法2,使用string:

class Solution:
    def restoreIpAddresses(self, s: str) -> List[str]:
        res=[]
        path=[]
        def isvalid(string):
            if len(string)==len(str(int(string))) and 0<=int(string)<=255:
                return True
            return False

        def dfs(s):
            if len(path)==4 and s:
                return False
            if len(path)==4 and not s:
                res.append('.'.join(path[:]))
                return
            
            for i in range(len(s)):
                if isvalid(s[:i+1]):
                    path.append(s[:i+1])
                    dfs(s[i+1:])
                    path.pop()

        dfs(s)
        return res

在这里插入图片描述

2.3、子集问题

2.3.1、78. 子集 - 整棵遍历数

力扣题目链接
注:幂集就是集合的所有子集。

回溯法:

class Solution:
    def subsets(self, nums: List[int]) -> List[List[int]]:
        res = []
        path = []

        def backtracking(nums, startIndex):
            # 子集就是遍历整个树
            res.append(path[:])
            if startIndex == len(nums):
                return

            for i in range(startIndex, len(nums)):
                path.append(nums[i])
                backtracking(nums, i+1)
                path.pop()
        
        backtracking(nums, 0)
        return res

在这里插入图片描述
从上图可知,求取子集问题,不需要任何剪枝,因为子集就是要遍历整棵树。


2.3.2、90. 子集 II - 乱序+重复元素+去重复值

力扣题目链接
思路同 40. 数组总和 II,即去重,去掉同一树层的重复节点。

回溯法:

class Solution:
    def subsetsWithDup(self, nums: List[int]) -> List[List[int]]:
        res = []
        path = []

        def backtracking(startIndex):
            res.append(path[:])
            if startIndex == len(nums):
                return
            
            for i in range(startIndex, len(nums)):
                # 同一树层,相似的则先去掉
                if i>startIndex and nums[i]==nums[i-1]:
                    continue
                path.append(nums[i])
                backtracking(i+1)
                path.pop()
        # 需要排序,便于把相似的排到一起
        nums.sort()
        backtracking(0)
        return res

在这里插入图片描述
注意: 一定要对元素进行排序,这样我们才方便通过相邻的节点来判断是否重复使用了。

子集题做完建议试试 2.6.1、491.递增子序列

class Solution:
    def subsetsWithDup(self, nums: List[int]) -> List[List[int]]:
        res = []
        path = []

        def backtracking(ind):
            res.append(path[:])

            if ind==len(nums):
                return
            # 每一层标记重复
            used = set()
            for i in range(ind, len(nums)):
            	# 有重复的跳过,前面已经遍历过
                if nums[i] in used:
                    continue
                # 按层添加
                used.add(nums[i])
                path.append(nums[i])
                backtracking(i+1)
                path.pop()
        # nums.sort()
        backtracking(0)
        return res

在这里插入图片描述

2.4、排列问题

排列是有序的,也就是说 [1,2] 和 [2,1] 是两个集合,这和之前分析的子集以及组合所不同的地方。

2.4.1、46.全排列

力扣题目链接

回溯法1,数组截断传递:

class Solution:
    def permute(self, nums: List[int]) -> List[List[int]]:
        res = []
        path = []

        def backtracking(nums):
            # 每次传入的nums是变动的,逐渐变为空,所以不能len(path)==len(nums)
            if not nums:
                res.append(path[:])
                return
            # 从索引0开始,因为每次都要取到所有数
            for i in range(len(nums)):
                path.append(nums[i])
                # 去掉被选中的nums[i],选取其他段拼在一起
                backtracking(nums[:i]+nums[i+1:])
                path.pop()
        backtracking(nums)
        return res

回溯法2,used数组:

class Solution:
    def permute(self, nums: List[int]) -> List[List[int]]:
        res = []
        path = []
		# 创建一个flag数组,保存结果,是否使用过
        used_list = [False]*len(nums)

        def backtracking(nums, used_list):
            # 每次传入的nums是固定的,则len(path)==len(nums)
            if len(path) == len(nums):
                res.append(path[:])
                return
            for i in range(len(nums)):
                # 已经使用过,则去重
                if used_list[i]==True:
                    continue
                used_list[i] = True
                path.append(nums[i])
                
                backtracking(nums, used_list)

                used_list[i] = False
                path.pop()
        backtracking(nums, used_list)
        return res

回溯法3,去掉used:
最简单的写法,因为无重复元素,所以path可以代替used_list。

class Solution:
    def permute(self, nums: List[int]) -> List[List[int]]:
        res = []
        path = []

        def backtracking(nums):
            # 每次传入的nums是固定的,则len(path)==len(nums)
            if len(path) == len(nums):
                res.append(path[:])
                return
            for i in range(len(nums)):
                # 因为本题为无重复元素,所以path内的元素是唯一的
                if nums[i] in path:
                    continue
                
                path.append(nums[i])
                
                backtracking(nums)

                path.pop()
        backtracking(nums)
        return res

在这里插入图片描述
可以看出元素1在[1,2]中已经使用过了,但是在[2,1]中还要在使用一次1,所以处理排列问题就不用使用startIndex了。

排列问题的不同:

每层都是从0开始搜索而不是startIndex
需要used数组记录path里都放了哪些元素了


2.4.2、47.全排列 II

力扣题目链接

这里必须使用used_list布尔列表,一个是原来的作用,即判断使用使用过;另一个是判断,在同一层,和此元素相同的值之前是否使用过。使用过直接跳过,否则是相同的操作结果。

class Solution:
    def permuteUnique(self, nums: List[int]) -> List[List[int]]:
        res = []
        path = []
        used_list = [False]*len(nums)
        
        def backtracking(nums, used_list):
            if len(nums) == len(path):
                res.append(path[:])
                return
    
            for i in range(len(nums)):
                # 每次递归,都是所有元素
                # 1.该元素自身被使用过,continue
                if used_list[i] == True:
                    continue
                # 2.该元素同一层有之前有重复,则只算一次,则这一次continue
                # used_list[i-1]==True表示同一层之前使用过
                # 同一层去重效率高,即取False效率比True高
                if (i>0 and nums[i]==nums[i-1]) and used_list[i-1]==False:
                    continue
                used_list[i] = True
                path.append(nums[i])
                backtracking(nums, used_list)
                used_list[i] = False
                path.pop()
        nums.sort()
        backtracking(nums, used_list)
        return res

树层上去重,即used[i-1]=False:
在这里插入图片描述
树枝上去重,即used[i-1]=true:
在这里插入图片描述
应该很清晰的看到,树层上对前一位去重非常彻底,效率很高,树枝上对前一位去重虽然最后可以得到答案,但是做了很多无用搜索。


在这里插入图片描述

2.5、应用型类似问题

2.5.1、491.递增子序列 (和子集问题很像)- 非排序+重复元素+去重

力扣题目链接

本题求自增子序列,是不能对原数组经行排序的,排完序的数组都是自增子序列了,所以不能使用之前的去重逻辑!

回溯法+集合去重:
#深度遍历中每一层都会有一个全新的usage_list用于记录本层元素是否重复使用

class Solution:
    def findSubsequences(self, nums: List[int]) -> List[List[int]]:
        res = []
        path = []

        def backtracking(startIndex):
            if len(path)>=2:
                res.append(path[:])
            if startIndex==len(nums):
                return
            # 这里要放在内部,因为每一层子树都要去重
            # 用来记录非重复的元素,用来去重判断
            used_list = set()
            # 单层递归逻辑
            # 深度遍历中每一层都会有一个全新的usage_list用于记录本层元素是否重复使用
            for i in range(startIndex, len(nums)):
                # 这个去重只能判断排序后相邻的数组
                # 所以像[4,7,6,7]在这里不可用
                #if i>startIndex and if nums[i] == nums[i-1]:
                #        continue

                # path非空,且后一个小于前一个,即小于path的最后一个元素 
                # 或者 同一树层后面重复出现,不同于前面的相邻重复出现
                if (path and nums[i]<path[-1]) or nums[i] in used_list:
                    continue
                used_list.add(nums[i])
                path.append(nums[i])
                backtracking(i+1)
                path.pop()
        
        backtracking(0)
        return res

回溯法+哈希表去重:

class Solution:
    def findSubsequences(self, nums: List[int]) -> List[List[int]]:
        res = []
        path = []

        def backtracking(nums, startIndex):
            if len(path)>=2:
                res.append(path[:])
            if startIndex==len(nums):
                return
            # 使用哈希表去重,也就是数组或列表
            used_list = [False] * 201  # 使用列表去重,题中取值范围[-100, 100]
            
            for i in range(startIndex, len(nums)):
                if (path and nums[i]<path[-1]) or used_list[nums[i]+100]==True:
                    continue
                # +100,将-100 - 100变为0 - 200
                used_list[nums[i]+100] = True
                path.append(nums[i])
                backtracking(nums, i+1)
                path.pop()
        
        backtracking(nums, 0)
        return res

在这里插入图片描述
在图中可以看出,同一父节点下的同层上使用过的元素就不能在使用了。与前面的同一层相邻重复的不同。


在这里插入图片描述
在这里插入图片描述

2.5.2、332.重新安排行程(和2.1.3、17. 电话号码的字母组合 很像)

力扣题目链接

这一题可参考 2.1.3、17. 电话号码的字母组合 - 双层回溯,自己创建映射进行遍历回溯,只不过,电话号码里面,是寻找所有组合,每个数字对于字母的选择是所有,而且能重复选择;而行程里面,每个地点对应的地点列表里,有效的只有部分,并且在选择了某地点后,下一次的地点列表需要将其去除。如何将其去除这是一个难点。

回溯法1(普通解法,超时):

class Solution:
    def findItinerary(self, tickets: List[List[str]]) -> List[str]:
        res = []
        path = []
        used_list = [False]*len(tickets)

        def backtracking(tickets):
            if len(path) == len(tickets):
                res.append(path[:])
                return
            # 每次遍历整个list,前面的通用解法,但对于这一题,这样会超时
            for i in range(len(tickets)):
                if used_list[i] == True:
                    continue
                if i>0 and tickets[i] == tickets[i-1] and used_list[i-1]==True:
                    continue
                
                if (path and (path[-1][-1]==tickets[i][0])) or (not path and tickets[i][0] == 'JFK'):
                    used_list[i] = True
                    path.append(tickets[i])
                    backtracking(tickets)
                    used_list[i] = False
                    path.pop()

        backtracking(tickets)
        
		# 将结果按字典排序
        final = sorted(res)[0]
        r = []
        # 所有list只取第一个值,加上最后一个list取最后一个值
        for i in range(len(final)):
            r.append(final[i][0])

            if i == len(final)-1:
                r.append(final[i][1])
        return r

回溯法2:
重新设置tickets为一个新的对象进行简化处理,这样就无需遍历整个tickets。

class Solution:
    def findItinerary(self, tickets: List[List[str]]) -> List[str]:
        res = []
        path = ['JFK']
        # 创建一个空字典,值为空list
        ticket_dict = defaultdict(list)
        for item in tickets:
            ticket_dict[item[0]].append(item[1])
        # 排序1 提前排序 或者在内部排序
        for t in ticket_dict:
        	ticket_dict[t].sort()
        '''
        tickets_dict里面的内容是这样的
         {'JFK': ['SFO', 'ATL'], 'SFO': ['ATL'], 'ATL': ['JFK', 'SFO']})
        '''
        # 根据字典的key去延伸到value再对应到下一个key......以此类推
        def backtracking(start_point):
            if len(path) == len(tickets)+1:
                return True
            # 内部排序,咱们使用外面的排序方法,只用遍历一次
            # ticket_dict[start_point].sort()
            for _ in ticket_dict[start_point]:
                # 取出一个值,选择一个落地点
                end_point = ticket_dict[start_point].pop(0)
                path.append(end_point)

                # 只要找到一个就可以直接返回了,这里设置成bool
                # 并且path直接为正确结果,跳过path.pop()还原
                if backtracking(end_point):
                    return True
                path.pop()
                ticket_dict[start_point].append(end_point)

        backtracking('JFK')
        return path

在这里插入图片描述


做回溯相关问题时,先思考以下几点:
排列或组合?有无序?是否有重复元素?有什么不同,需要什么或不需要什么?

总结如下:

回溯问题组合排列幂集(子集)
回溯函数参数使用startIndex,每进一层(startIndex, len(nums)),即找startIndex后面的元素进行组合,防止重复值。任意参数。有重复组合值,只是排列不同,startIndex无意义,使用used布尔列表,为True则表示访问过,去掉。同组合
额外变量只需startIndex即可 / used布尔list / 同一层的used集合used布尔list同组合
遍历顺序从startInde开始搜索:(startIndex, len(nums))从头开始搜索(len(nums)同组合
有重复元素+无序/有序(无序则先进行排序)下面三种不同方法排除重复组合,直接continue: [1] i>startIndex and nums[i]==nums[i-1] ;[2] i>0 and nums[i]==nums[i-1] and nums[i-1]=False ;[3] 同一层 used=set() ,nums[i] in used,used只有append无pop。(无序则先进行排序)除了自身是否使用过进行判断,其余判断同前面组合,对同一层是否重复使用进行判断。同组合
无重复元素+无序/有序(无需排序)常规回溯写法(无需排序)常规回溯写法+used判断/path代替used同组合
结果指定的长度组合同长度排列(不同长度)节点上所有元素。当数组无重复元素,幂集的结果大小为 2 数组长度 2^{数组长度} 2数组长度

注意: 这是常规解法,对本文章以及很多特定其他解法没有包含进去,等算法达到了一定的水平可以自行创造。


在这里插入图片描述

2.6、棋盘问题

在这里插入图片描述

2.6.1、HJ43 迷宫问题

华为机试牛客网链接
回溯法:

# 字符串转数字和迷宫
h, w = map(int, input().split())
maze = []
for i in range(h):
    maze.append(list(map(int, input().split())))

# 初始化
# 起点
result = [(0, 0)]
# 走过的路径进行标记,防止回溯死循环
maze[0][0] = 3

# 判断该点是否合法
# 1.范围,2.路或墙
def isVaild(x, y):
    if 0 <= x < h and 0 <= y < w and maze[x][y] == 0:
        return True
    return False


# 回溯
def backtracking(x, y):
    # 打印每一步的坐标点
    # print(x,y)
    # 终止条件,到达右下角终点
    if x == h - 1 and y == w - 1:
        # result.append((x, y))
        return result

    """
    回溯前,从第一个方向开始
    第一个条件先判断下一步是否合法
    合法再进行深度优先搜索
    1.同时对走过的路径进行标记为3
    2.将坐标加入到result结果中
    搜索成功,1和2成立
    搜索失败,进行回溯:
    1. 将走过的路径进行回溯,但不能改为原值0,可改为2,另一个值,或者不改也可以
    2. 将result进行回溯,pop退出结果
    选择下一个方向
    如果四个方向都失败,则返回False,说明该方向的深度优先无法走到终点
    """
    # 下
    if isVaild(x + 1, y):
        result.append((x + 1, y))
        maze[x + 1][y] = 3
        if backtracking(x + 1, y):
            return True
        # 回溯
        result.pop()
        maze[x + 1][y] = 2
    # 上
    if isVaild(x - 1, y):
        result.append((x - 1, y))
        maze[x - 1][y] = 3
        if backtracking(x - 1, y):
            return True
        # 回溯
        result.pop()
        maze[x - 1][y] = 2
    # 左
    if isVaild(x, y - 1):
        result.append((x, y - 1))
        maze[x][y - 1] = 3
        if backtracking(x, y - 1):
            return True
        # 回溯
        result.pop()
        maze[x][y - 1] = 2
    # 右
    if isVaild(x, y + 1):
        result.append((x, y + 1))
        maze[x][y + 1] = 3
        if backtracking(x, y + 1):
            return True
        # 回溯
        result.pop()
        maze[x][y + 1] = 2
    # 上下左右都不行,则返回False
    return False
backtracking(0, 0)
print(result)
print(maze)

测试输入:

h,w = map(int, '5 5'.split())
maze = []
deal = ['0 1 0 0 0', '0 1 1 1 0', '0 0 0 0 0','0 1 1 1 0','0 0 0 1 0']
for i in deal:
    maze.append(list(map(int, i.split())))
"""
[[0, 1, 0, 0, 0],
 [0, 1, 1, 1, 0],
 [0, 0, 0, 0, 0],
 [0, 1, 1, 1, 0],
 [0, 0, 0, 1, 0]]
"""

结果:

0 0
1 0
2 0
3 0
4 0
4 1
4 2
2 1
2 2
2 3
2 4
3 4
4 4
[(0, 0), (1, 0), (2, 0), (2, 1), (2, 2), (2, 3), (2, 4), (3, 4), (4, 4)]
[[3, 1, 0, 0, 0],
 [3, 1, 1, 1, 0],
 [3, 3, 3, 3, 3],
 [2, 1, 1, 1, 3],
 [2, 2, 2, 1, 3]]

在这里插入图片描述
在这里插入图片描述

2.6.2、51. N 皇后

力扣题目链接

回溯法:

class Solution:
    def solveNQueens(self, n: int) -> List[List[str]]:
        res = []
        board = [['.']*n for _ in range(n)]

        # 判断是否合法
        def isVaild(board, row, col):
            # 1. 判断同一列是否冲突
            for i in range(row+1):
                if board[i][col] == 'Q':
                    return False
            # 2.判断左上是否冲突
            tmp_row = row
            tmp_col = col
            while 0<=tmp_row<n and 0<=tmp_col<n:
                
                if board[tmp_row][tmp_col]== 'Q':
                    return False
                tmp_row-=1
                tmp_col-=1

            # 3.判断右上是否冲突
            tmp_row = row
            tmp_col = col
            while 0<=tmp_row<n and 0<=tmp_col<n:
                if board[tmp_row][tmp_col]== 'Q':
                    return False
                tmp_row-=1
                tmp_col+=1
            
            return True

        # 每一行进行遍历
        def backtracking(row, board):
            if row == n:
                tmp = []
                for i in board:
                    tmp.append(''.join(i))
                res.append(tmp)
                return
            # 每一行的每一列进行选取
            for col in range(n):
                if not isVaild(board, row, col):
                    continue
                board[row][col] = 'Q'
                # 进入下一行
                backtracking(row+1, board)
                board[row][col] = '.'
                
        
        backtracking(0, board)
        
        return res

在这里插入图片描述
皇后们的约束条件:

  1. 不能同行
  2. 不能同列
  3. 不能同斜线

符合条件则继续下一行,否则同行的下一列。

这题是hard题其实难度并不大,当做 2.1.3、17. 电话号码的字母组合332.重新安排行程 来做,每一行为索引进行遍历,内部每一列进行遍历回溯。

难点在于判断条件,即约束条件上是否有Q。我们一般拆成4个部分,横,竖,正斜,反斜,但实际上,横无需判断,因为每一行的行为树根,这个只会按顺序递增,而由此引出子树列才需要回溯,只会产生一个Q。并且斜左下与斜右下也无需遍历,因为一定为空,一定没有Q,因为还没遍历到那个位置,所以咱们的判断以该点为水平线的上面部分为区间即可,这样可以降低复杂度。


在这里插入图片描述

2.6.3、37. 解数独

力扣题目链接

回溯法:

class Solution:
    def solveSudoku(self, board: List[List[str]]) -> None:
        """
        Do not return anything, modify board in-place instead.
        """
        def isVaild(row, col, val, board):
            # 判断同一行是否冲突
            for i in range(9):
                if board[row][i] == str(val):
                    return False
            # 判断同一列是否冲突
            for i in range(9):
                if board[i][col] == str(val):
                    return False
            # 判断同一宫是否冲突
            start_row = (row//3)*3
            start_col = (col//3)*3
            for i in range(start_row, start_row+3):
                for j in range(start_col, start_col+3):
                    if board[i][j] == str(val):
                        return False
            return True


        # 若有解返回True,无解返回False
        def backtracking(board):
        	# 这里遍历完即是终止条件
            # 遍历整个表,行和列
            for i in range(len(board)):
                for j in range(len(board[0])):
                    # 若空格内已经有数字,则跳过
                    if board[i][j] != '.':
                        continue
                    # 逐渐填入1-10
                    for k in range(1, 10):
                        # 有效则继续递归
                        if isVaild(i, j, k, board):
                            board[i][j] = str(k)
                            if backtracking(board):
                                return True
                            # 回溯
                            board[i][j] = '.'
                    # 若数字1-9都不能成功填入空格,返回False无解
                    return False
            # 有解,说明已经填满了,到了最后一步,所有的board[i][j] != '.'
            return True
        backtracking(board)

在这里插入图片描述
N皇后问题是因为每一行每一列只放一个皇后,只需要一层for循环遍历一行,递归来来遍历列,然后一行一列确定皇后的唯一位置。

本题就不一样了,本题中棋盘的每一个位置都要放一个数字,并检查数字是否合法,解数独的树形结构要比N皇后更宽更深。

这里需要的是一个二维的递归(也就是两个for循环嵌套着递归),这个二维的递归与下一章中的深搜与广搜有很深的联系,如果你能理解其一,对这题或者下一章的题目的理解都非常的简单。

一个for循环遍历棋盘的行,一个for循环遍历棋盘的列,一行一列确定下来之后,递归遍历这个位置放9个数字的可能性。如果一行一列确定下来了,这里尝试了9个数都不行,说明这个棋盘找不到解决数独问题的解。

判断棋盘是否合法有如下三个维度:

  1. 同行是否重复
  2. 同列是否重复
  3. 9宫格里是否重复

同时,一定要注意,最后的两层循环之外,需要return True,因为棋盘都填满了,这是满足结果的,需要传回布尔True给if backtracking()去判断,从而继续return True,防止后续的回溯而还原结果,导致即便搜到了结果,因为回溯还原了,最后board还是原来的空的。

同时,题目要求只有一个结果时,我们把backtracking当做布尔if去判断,通过return True防止回溯。
当题目要求有多个结果时,我们就当普通的backtracking为一行,回溯前将结果通过path存到res中。



三、深搜DFS与广搜BFS

深广搜问题相比回溯问题,少了退回的操作,但思路大致相似,所以该类型题目会更加容易。
在这里插入图片描述

3.1、岛屿问题

基本上,能用dfs做的题目也能用bfs来做,所以可以尝试用这两种方法去解题。这样对该类型算法的理解会更加深刻。
在这里插入图片描述


3.1.1、深搜 or 广搜

200. 岛屿数量

200. 岛屿数量
非常常规和经典的DFS和BFS题目。


1. 深度优先搜索DFS

dfs题目的模板与回溯法类似,但是没有退回操作,因为都是有效操作。

class Solution:
    def numIslands(self, grid: List[List[str]]) -> int:
        # 避免重复,将结果存进来,或者grid[i][j]=0遍历过变成湖泊避免死循环
        grid_set = set()
        res = 0

        def dfs(i, j):
            for x, y in (i+1, j), (i-1, j), (i, j+1), (i, j-1):
                if 0<=x<len(grid) and 0<=y<len(grid[0]) and grid[x][y]=='1' and (x,y) not in grid_set:

                    # 遍历过的结果存到grid_set
                    grid_set.add((x,y))
                    # 将符合条件的(x,y)继续迭代
                    dfs(x,y)
            
        for i in range(len(grid)):
            for j in range(len(grid[0])):
                # 为陆地,并且没被遍历过
                if grid[i][j]=='1' and (i,j) not in grid_set:
                    res+=1
                    grid_set.add((i,j))
                    dfs(i,j)
        return res

因为整个地图给到我们,我们不用将结果存起来去判断重复,直接遍历过的变成 0 湖泊就行了,那么少一个grid_set的空间以及搜索时间,效率会提高:

class Solution:
    def numIslands(self, grid: List[List[str]]) -> int:
        res = 0

        def dfs(i, j):
            grid[i][j]='0' # 这里改成非'1'的任何值都可以
            for x, y in (i+1, j), (i-1, j), (i, j+1), (i, j-1):
                if 0<=x<len(grid) and 0<=y<len(grid[0]) and grid[x][y]=='1':
                    # 将符合条件的(x,y)继续迭代
                    dfs(x,y)
            
        for i in range(len(grid)):
            for j in range(len(grid[0])):
                # 为陆地,并且没被遍历过
                if grid[i][j]=='1':
                    res+=1
                    dfs(i,j)
        return res

2. 广度优先搜索 BFS
from collections import deque
class Solution:
    def numIslands(self, grid: List[List[str]]) -> int:
        res = 0
            
        for i in range(len(grid)):
            for j in range(len(grid[0])):
                # 为陆地,并且没被遍历过
                if grid[i][j]=='1':
                    res+=1
                    grid[i][j]=='0'
                    # 用队列代替列表存储,加快速度
                    neighbors = deque([(i,j)])
                    # 从一个点开始向周围扩散,广度优先,扩散到完成,退出,找下一个点
                    while neighbors:
                    	# 从左起点开始取所有点
                    	# 为什么从左开始?先搜索完该点的第一外层
                    	# 随后第二外层等等依次继续
                        xi, yj = neighbors.popleft()
                        # 后面的代码同dfs,但是内部没有嵌套dfs,也就不会无限的深度找,而是找一次,即外层一圈即可。
                        for x, y in (xi+1, yj), (xi-1, yj), (xi, yj+1), (xi, yj-1):
                            if 0<=x<len(grid) and 0<=y<len(grid[0]) and grid[x][y]=='1':
                                neighbors.append((x, y))
                                grid[x][y]='0'
        return res

1254. 统计封闭岛屿的数目

1254. 统计封闭岛屿的数目
求封闭的个数,那么分为非封闭与封闭岛屿,需要在搜索的过程中对不符合条件的进行标记,被标记上的岛屿总数不会+1。

1.深度优先搜索DFS

遍历所有面积,去除与边界连通的面积,即边界位置有1岛屿的。

class Solution:
    def closedIsland(self, grid: List[List[int]]) -> int:
        row = len(grid)
        col = len(grid[0])
        res = 0

        def dfs(i, j):
            # 同样,遍历过的点进行标注,防止死循环或重复遍历
            grid[i][j]=1
            for x, y in (i+1, j), (i-1, j), (i, j+1), (i, j-1):
                if 0<=x<row and 0<=y<col and grid[x][y]==0:
                    # 有不符合条件的点,该面积标注为0,即不合理
                    if x==0 or x==row-1 or y==0 or y==col-1:
                        nonlocal flag
                        flag=0
                    dfs(x, y)
           
        for i in range(1, row-1):
            for j in range(1, col-1):
                flag = 1
                if grid[i][j]==0:
                    dfs(i, j)
                    # 符合条件,无边界为0或row、col长度的面积则合理
                    if flag:
                        res+=1
        return res

2.广度优先搜索
from collections import deque
class Solution:
    def closedIsland(self, grid: List[List[int]]) -> int:
        row = len(grid)
        col = len(grid[0])
        res = 0
           
        for i in range(1, row-1):
            for j in range(1, col-1):
                flag = 1
                if grid[i][j]==0:
                	grid[i][j]=1
                	# 同样使用队列代替列表加速
                    neighbors = deque([(i, j)])
                    while neighbors:
                        xi, yj = neighbors.popleft()
                        for x, y in (xi+1, yj),(xi-1, yj),(xi,yj+1),(xi,yj-1):
                            if 0<=x<row and 0<=y<col and grid[x][y]==0:
                                # 判断条件,不符合条件的标记为0
                                if x==0 or x==row-1 or y==0 or y==col-1:
                                    flag = 0
                                # 添加下一圈点以及避免重复进行相反值标记
                                neighbors.append((x,y))
                                grid[x][y]=1
                    # 符合条件,无边界为0或row、col长度的面积则合理
                    if flag:
                        res+=1
        return res

3.深度优先/广度优先 去除边界连通面积
class Solution:
    def countSubIslands(self, grid1: List[List[int]], grid2: List[List[int]]) -> int:
        row = len(grid1)
        col = len(grid1[0])
        res = 0
        # 常规dfs
        def dfs(i,j):
            grid2[i][j]=0
            for x, y in (i+1,j),(i-1,j),(i,j+1),(i,j-1):
                if 0<=x<row and 0<=y<col and grid2[x][y]==1:
                    dfs(x ,y)

        # 求B在A中的子集,B有A没有的点,延伸的面积全部变成0
        for i in range(row):
            for j in range(col):
                if grid1[i][j]==0 and grid2[i][j]==1:
                    dfs(i,j)
        # 最后剩下的grid2中的1的岛屿即A的子集
        for i in range(row):
            for j in range(col):
                if grid2[i][j]==1:
                    res+=1
                    dfs(i,j)
        return res

1020.飞地的数量

1020.飞地的数量

这题同上思路,不过不是统计面积,无需再dfs,而是统计为1的格子数量,那么直接遍历即可。

class Solution:
    def numEnclaves(self, grid: List[List[int]]) -> int:
        row = len(grid)
        col = len(grid[0])
        res = 0
		# 同上
        def dfs(i,j):
            grid[i][j]=0
            for x, y in (i+1,j),(i-1,j),(i,j+1),(i,j-1):
                if 0<=x<row and 0<=y<col and grid[x][y]==1:
                    dfs(x,y)
        for i in range(row):
            if grid[i][0]==1:
                dfs(i,0)
            if grid[i][col-1]==1:
                dfs(i,col-1)
        for j in range(col):
            if grid[0][j]==1:
                dfs(0,j)
            if grid[row-1][j]==1:
                dfs(row-1,j)
        # 统计图上都是1的格子数量
        for i in range(1, row-1):
            for j in range(1, col-1):
                if grid[i][j]==1:
                    res+=1
        return res

广度优先的写法也很简单,同上。


695. 岛屿的最大面积

695. 岛屿的最大面积
同前面,将所有面积计算出来,每次max最大的即可。

1.深度优先搜索DFS
class Solution:
    def maxAreaOfIsland(self, grid: List[List[int]]) -> int:
        row = len(grid)
        col = len(grid[0])
        # 保存最大的结果
        max_area = 0

        def dfs(i,j):
            grid[i][j]=0
            # 这里是关键,每遍历一个点,面积+1
            nonlocal area
            area+=1
            for x, y in (i+1, j),(i-1,j),(i,j+1),(i,j-1):
                if 0<=x<row and 0<=y<col and grid[x][y]==1:
                    dfs(x,y)
        
        for i in range(row):
            for j in range(col):
                if grid[i][j]==1:
                    area = 0
                    dfs(i,j)
                    # 取最大的面积
                    max_area = max(max_area, area)
        return max_area

2.广度优先搜索 BFS
from collections import deque
class Solution:
    def maxAreaOfIsland(self, grid: List[List[int]]) -> int:
        row = len(grid)
        col = len(grid[0])
        max_area = 0

        for i in range(row):
            for j in range(col):
                if grid[i][j]==1:
                    area = 0
                    grid[i][j]=0
                    neighbors = deque([(i,j)])
                    while neighbors:
                        xi, yj = neighbors.popleft()
                        # 面积+1
                        area+=1
                        for x, y in (xi+1, yj), (xi-1, yj), (xi, yj+1),(xi,yj-1):
                            if 0<=x<row and 0<=y<col and grid[x][y]==1:
                                neighbors.append((x,y))
                                grid[x][y]=0
                    max_area = max(area, max_area)

        return max_area

1905. 统计子岛屿

1905. 统计子岛屿

class Solution:
    def countSubIslands(self, grid1: List[List[int]], grid2: List[List[int]]) -> int:
        row = len(grid1)
        col = len(grid1[0])
        res = 0
        def dfs(i,j):
            grid2[i][j]=0
            for x, y in (i+1,j),(i-1,j),(i,j+1),(i,j-1):
                if 0<=x<row and 0<=y<col and grid2[x][y]==1:
                    dfs(x ,y)

        # 求B在A中的子集,B有A没有的点,延伸的面积全部变成0
        for i in range(row):
            for j in range(col):
                if grid1[i][j]==0 and grid2[i][j]==1:
                    dfs(i,j)
        # 最后剩下的grid2中的1的岛屿即A的子集
        for i in range(row):
            for j in range(col):
                if grid2[i][j]==1:
                    res+=1
                    dfs(i,j)
        return res

广度优先的写法也很简单,同上。


130. 被围绕的区域

130. 被围绕的区域

class Solution:
    def solve(self, board: List[List[str]]) -> None:
        row = len(board)
        col = len(board[0])

        def dfs(i,j):
            board[i][j]='Z'
            for x, y in (i+1, j),(i-1, j),(i, j+1),(i,j-1):
                if 0<=x<row and 0<=y<col and board[x][y]=='O':
                    dfs(x,y)
        # 两竖边界
        for i in range(row):
            if board[i][0]=='O':
                dfs(i,0)
            if board[i][col-1]=='O':
                dfs(i,col-1)
        # 两横边界
        for j in range(col):
            if board[0][j]=='O':
                dfs(0,j)
            if board[row-1][j]=='O':
                dfs(row-1,j)
        for i in range(row):
            for j in range(col):
                if board[i][j]=='O':
                    board[i][j]='X'
                if board[i][j]=='Z':
                    board[i][j]='O'

广度优先的写法也很简单,同上。


417. 太平洋大西洋水流问题

417. 太平洋大西洋水流问题

这题也是需要判断区域的四个边界,以此为判断能否流出两种河流的结果。
除此之外,该题的图内的值不可更改,所以需要另外创建visited去存储访问过的点,visited可以为图大小的二维数组,也可以为set集合或者list列表。

class Solution:
    def pacificAtlantic(self, heights: List[List[int]]) -> List[List[int]]:
        row = len(heights)
        col = len(heights[0])
        res = []

        def dfs(i,j):
            visited.add((i,j))
            if i==0 or j==0:
                nonlocal sign_pacific
                sign_pacific = 1
            if i==row-1 or j==col-1:
                nonlocal sign_atlantic
                sign_atlantic = 1
            if sign_pacific and sign_atlantic:
                return True

            for x,y in (i+1, j),(i-1,j),(i,j+1),(i,j-1):
                if 0<=x<row and 0<=y<col:
                    if heights[x][y]<=heights[i][j] and (x,y) not in visited:
                        dfs(x, y)

        for i in range(row):
            for j in range(col):
                sign_pacific = 0
                sign_atlantic = 0
                # 不能改变原图的值,所以避免重复值,则需要新创建一个图
                visited = set()
                dfs(i,j)
                if sign_pacific and sign_atlantic:
                    res.append([i,j])
        return res


3.1.2、广搜

1162. 地图分析 (特殊:只能广度优先搜索,且完全按层遍历)

1162. 地图分析
这题类似于河水污染题,那题的0表示河水,1表示污染区域,每天污染相邻区域,求几天整个河水都污染完。

在这里插入图片描述

这题类似,这里的广度优先与前面题目不同,每一层需要遍历完才能到下一层,下面代码用tmp临时存储下一层即将污染的水域,count保存遍历的树的层数,或者是这题中河与岸的距离或者上面污染题的天数。

from collections import deque
class Solution:
    def maxDistance(self, grid: List[List[int]]) -> int:
        n = len(grid)
        sea = deque([])
        # 遍历从所有的1开始BFS,一层一层的找0
        for i in range(n):
            for j in range(n):
                if grid[i][j]==1:
                    sea.append((i,j))
        # 每一层+1,距离或天数
        count = 0
        # 队列一直有值则一直遍历,直到空则退出while
        while True:
            tmp = deque([])
            # 每个队列遍历完,即每一层BFS遍历一层
            while sea:
                s = sea.popleft()
                for x, y in (s[0]+1, s[1]),(s[0]-1,s[1]),(s[0],s[1]+1),(s[0],s[1]-1):
                    if 0<=x<n and 0<=y<n and grid[x][y]==0:
                        grid[x][y]=1
                        tmp.append((x,y))
            # 如果还有空余海域,则继续污染,day+1
            if tmp:
                sea=tmp
                count+=1
            # 否则退出
            else:
                break
        return count if count else -1


3.1.3、先深搜,再广搜

934. 最短的桥

934. 最短的桥
相当于找两个岛屿间的最短距离。先深搜找到其中一个岛屿,再广搜,对该岛屿每个点每次全部进行扩散(理论上最外层的点才是有效扩散,但统一扩散,因为最外层的点不方便寻找,徒增复杂度),原岛屿遍历过都标记为-1,每次外层广搜,步数+1,一旦找到值为1的另外岛屿,则直接返回步数。

from collections import deque
class Solution:
    def shortestBridge(self, grid: List[List[int]]) -> int:
        row = len(grid)
        col = len(grid[0])
        count = 0
        first_island = deque([])
        # 先DFS找到一座岛
        def dfs(i,j):
            grid[i][j]=-1
            first_island.append((i,j))
            for x,y in (i+1, j),(i-1,j),(i,j+1),(i,j-1):
                if 0<=x<row and 0<=y<col and grid[x][y]==1:
                    dfs(x,y)
        
        for i in range(row):
            for j in range(col):
                if grid[i][j]==1:
                    dfs(i,j)
                    
                    # 前面的循环无需继续进行,后续直接开始,也可以退出前面这两层循环
                    # 但无必要,后续的操作直接返回
                    
                    # 再BFS,所有点一层层外扩,找到1则停止
                    while True:
                        tmp = deque([])
                        while first_island:
                            x,y = first_island.popleft()
                            for xi,yj in (x+1,y),(x-1,y),(x,y+1),(x,y-1):
                                if 0<=xi<row and 0<=yj<col and grid[xi][yj]!=-1:
                                    if grid[xi][yj]==1:
                                        return count
                                    else:
                                        grid[xi][yj]=-1
                                        tmp.append((xi,yj))
                        count+=1
                        first_island = tmp

3.2、连通图

面试题 04.01. 节点间通路 - 连通图

类似于 332.重新安排行程 和 207. 课程表
面试题 04.01. 节点间通路

from collections import defaultdict
class Solution:
    def findWhetherExistsPath(self, n: int, graph: List[List[int]], start: int, target: int) -> bool:
        # 把每个点的指向存储起来
        paths = defaultdict(list)
        for gra in graph:
            paths[gra[0]].append(gra[1])
        # 防止重复,减少复杂度
        visited = set()
        def dfs(point):
            visited.add(point)
            if point==target:
                return True
            for p in paths[point]:
                if p not in visited and dfs(p):
                    return True
            return False
        return dfs(start)

2685. 统计完全连通分量的数量 - 完全连通图

2685. 统计完全连通分量的数量
同上一题,只不过统计完全连通分量。

dfs每个连通块,统计当前连通块的点数 v v v和边数 e e e
中间过程:

  • 每访问一个点,就把 v v v加一
  • e e e加上点v的邻居个数。注意这样一条边会统计两次。

在完全图中,任意两点之间都有边,相当于从 v v v个点中选2个点的方案数。所以有
e = v ( v − 1 ) 2 \begin{aligned} e=\frac{v(v-1)}{2} \end{aligned} e=2v(v1)
由于上面统计的时候,一条边统计了两次,所以代码中的判断条件是:
e = v ( v − 1 ) \begin{aligned} e={v(v-1)} \end{aligned} e=v(v1)

详细代码如下:

class Solution:
    def countCompleteComponents(self, n: int, edges: List[List[int]]) -> int:
        g = [[] for _ in range(n)]
        for i, j in edges:
            g[i].append(j)
            g[j].append(i)
        visited = [False]*n
        def dfs(x):
            visited[x]=True
            nonlocal v, e
            # 节点向量数量+1
            v+=1
            # 出度
            e+=len(g[x])
            for y in g[x]:
                if not visited[y]:
                    dfs(y)
        ans = 0
        for i in range(n):
            if not visited[i]:
                v = e = 0
                dfs(i)
                ans += e == v*(v-1)
        return ans

参考


3.3、集合划分问题(等用于回溯的问题)

698. 划分为k个相等的子集

698. 划分为k个相等的子集
要划分相等子集,首先要判断能否划分,即能否被k整除,能的话再继续下一步判断。

k个子集相当于k个桶,将nums的物品依次丢入到这k个桶中去,每一个可能放可能不放复杂度为 O ( k n ) O(k^n) O(kn)相当于暴力搜索。

按序深搜每个物品放入每个桶中,能放入该桶,则深搜下一个物品,不能则回溯,放入下一个桶中。如果每个桶都不能放,则退出了k个桶的循环return False。

class Solution:
    def canPartitionKSubsets(self, nums: List[int], k: int) -> bool:
        target = sum(nums)
        if target%k:
            return False
        target //=k
        # 优化1,将nums物品按照从大道小排序,可减少搜搜复杂度。
        # 如果物品大小大于背包,4个桶装不下,可以直接False
        # 等于背包则直接放入而装满,进行下一个
        nums.sort(reverse=True)
        bucket = [0]*k
        def dfs(ind):
            if ind==len(nums):
                return True
            for i in range(k):
            	# 优化2,如果桶的大小相同,当放一个物品放不下时,放另一个相等大小的桶也一样放不下,则直接跳过不一样大小的桶进行dfs
                if i>0 and bucket[i]==bucket[i-1]:
                    continue
                # 优化3(这里没更改),if bucket[i]+nums[ind]<=target:再继续后面的
                # 把 if bucket[i]<=target and dfs(ind+1): 拆开
                bucket[i]+=nums[ind]
                if bucket[i]<=target and dfs(ind+1):
                    return True
                bucket[i]-=nums[ind]
            return False
        
        return dfs(0)

473. 火柴拼正方形

473. 火柴拼正方形

class Solution:
    def makesquare(self, matchsticks: List[int]) -> bool:
        target = sum(matchsticks)
        if target%4:
            return False
        target //=4
        bucket = [0]*4
        matchsticks.sort(reverse=True)
        def backtracking(ind):
            if ind==len(matchsticks):
                return True
            
            for i in range(4):
                if i>0 and bucket[i]==bucket[i-1]:
                    continue
                bucket[i]+=matchsticks[ind]
                if bucket[i]<=target and backtracking(ind+1):
                    return True
                bucket[i]-=matchsticks[ind]
            return False

        return backtracking(0)


2305. 公平分发饼干

2305. 公平分发饼干
以cookies =[8,15,10,20,8]和k=2为例

相当于吧所有的方法找出来:
[61, 0]
[53, 8]
[41, 20]
[33, 28]
[51, 10]
[43, 18]
[31, 30]
[23, 38]
[46, 15]
[38, 23]
[26, 35]
[18, 43]
[36, 25]
[28, 33]
[16, 45]
[8, 53]
去找bucket里的最大值,整体的最小值:
res = min(res, max(bucket))
因为复杂度很高,再进行剪枝

import math
class Solution:
    def distributeCookies(self, cookies: List[int], k: int) -> int:
        # 因为可能无法均分,所以这里不去设置target值,而是设置一个变动的res
        # res逐渐变小,但是是bucket里的最大值
        # target = sum(cookies)
        # target = math.ceil(target/k)
        
        # 从最大值开始,找最小值,因为小就接近于均分 
        res = float('inf')
        # 优化1
        cookies.sort(reverse=True)

        bucket = [0]*k
        def backtracking(ind):
            if ind==len(cookies):
                # print(bucket)
                nonlocal res
                res = min(res, max(bucket))
                return
            for i in range(k):
                # 优化2
                if i>0 and bucket[i]==bucket[i-1]:
                    continue
                # 优化3 剪枝,有桶大于res,直接去掉
                if bucket[i]+cookies[ind]<res:
                	bucket[i]+=cookies[ind]
                    backtracking(ind+1)
                	bucket[i]-=cookies[ind]
        
        backtracking(0)
        return res
        

在这里插入图片描述

1723. 完成所有工作的最短时间

1723. 完成所有工作的最短时间

class Solution:
    def minimumTimeRequired(self, jobs: List[int], k: int) -> int:
        res = float('inf')
        bucket = [0]*k
        jobs.sort(reverse=True)
        def backtracking(ind):
            if ind==len(jobs):
                nonlocal res
                res = min(res, max(bucket))
                return True
            
            for i in range(k):
                if i>0 and bucket[i]==bucket[i-1]:
                    continue
                if bucket[i]+jobs[ind]<res:
                	bucket[i]+=jobs[ind]
                    backtracking(ind+1)
                	bucket[i]-=jobs[ind]
            return False
        backtracking(0)
        return res
            

3.4、其他问题

547. 省份数量 - (并查集)

547. 省份数量
这题是以一个表来展现相邻,这与岛的相邻不同,不能直接判表中的1相邻,实际上咱们每一行站在列的角度,行的列不相交,则不会直接相邻,可能间接相邻,需要深度搜索,若相交则一定相邻。当然这只是个思考判断方法,具体做题是另一种思路。

这里行与列一定相等,把坐标表示为城市名字。
遍历每一行,即从第一个城市开始探索与其相邻的城市,第一行是与0节点相连的所有其他城市,值为1则对其显示的列坐标城市,转到对应的行再进行深度搜索,寻找与其相邻的城市,重复该步骤,并且用visited进行存储,已经存在则无需重复再搜。

class Solution:
    def findCircleNum(self, isConnected: List[List[int]]) -> int:
        row = len(isConnected)
        col = len(isConnected[0])
        res = 0

        def dfs(i):
            visited.add(i)
            for j in range(row):
                if j not in visited and isConnected[i][j]==1:
                    dfs(j)
            
        visited = set()
        for i in range(row):
            if i not in visited:
                dfs(i)
                res+=1
                # print(visited)
        return res

并查集:

class Solution:
    def findCircleNum(self, isConnected: List[List[int]]) -> int:
        n = len(isConnected)
        p = [i for i in range(n)]

        def find(x):
            if p[x]!=x:
                p[x] = find(p[x])
            return p[x]
        def union(x, y):
            p[find(x)]=find(y)
        for i in range(n):
            for j in range(i+1, n):
                if isConnected[i][j]==1:
                    union(i, j)
        return sum(1 for i in range(n) if i==p[i])

总结

去重,标记重复路径能,有不能,没有
能否改变原题的board,或者有无board无需额外visited需要visited

board是指,像棋盘问题那样的,能在原grid上进行更改且不影响搜索结果的问题。

是否需要回溯
求面积等等,需要一搜到底多个结果,需撤回操作去寻找其他答案
dfs是否作为布尔值
求单个值,正确直接return True,阻止后续回溯求多个值,无返回值,后续回溯进行其他答案的探寻
是否需要排序
去除重复结果,相邻相同元素直接跳过可能数据集中无重复元素
回溯问题组合排列幂集
回溯函数参数使用startIndex,每进一层(startIndex, len(nums)),即找startIndex后面的元素进行组合,防止重复值。任意参数。有重复组合值,只是排列不同,startIndex无意义,使用used布尔列表,为True则表示访问过,去掉。同组合
额外变量只需startIndex即可 / used布尔list / 同一层的used集合used布尔list同组合
遍历顺序从startInde开始搜索:(startIndex, len(nums))从头开始搜索(len(nums)同组合
有重复元素+无序/有序(无序则先进行排序)下面三种不同方法排除重复组合,直接continue: [1] i>startIndex and nums[i]==nums[i-1] ;[2] i>0 and nums[i]==nums[i-1] and nums[i-1]=False ;[3] 同一层 used=set() ,nums[i] in used,used只有append无pop。(无序则先进行排序)除了自身是否使用过进行判断,其余判断同前面组合,对同一层是否重复使用进行判断。同组合
无重复元素+无序/有序(无需排序)常规回溯写法(无需排序)常规回溯写法+used判断/path代替used同组合
结果同长度组合同长度排列(不同长度)节点上所有元素。当数组无重复元素,幂集的结果大小为 2 数组长度 2^{数组长度} 2数组长度

注意: 这是常规解法,对本文章以及很多特定其他解法没有包含进去,等算法达到了一定的水平可以自行创造。



四、进阶

通过前面的学习与练习,我们知道了回溯以及深广搜实际上是偏暴力遍历的方法类型,在数据量小的情况下可以得到很好的结果。

但有时候,数据量稍微大一点,代码便运行超时了。比如 46. 全排列 这道题数据长度为1到6,使用常规的回溯便能解答,但是像 2741.特别的排列 这道题,数据长度范围为1到15,虽然长度只是稍微增加一点,但单纯的使用回溯就会超时。

这在leetcode的比赛中较为常见,当我们还使用传统的解法时,可能只能通过部分案例,数据量一大就超时了。

有人说在原来代码的基础上,进行剪枝操作能否降低复杂度而避免超时呢?经过实际的比赛经验可以得出结论:大部分情况下不行,而且只会增加思考难度,所以我们需要向Top级选手学习更加高阶的方法和技巧。相信在掌握了看似复杂的方法后,会越来越觉得简单,剩下的交给练习和时间了。


4.1、记忆化搜索

6898. 字符串连接删减字母

6898. 字符串连接删减字母

状态定义 dfs(i, s, e): 返回前面形成的字符串开头为字符为 s,结尾字符为 e的情况下,剩余操作形成的最短字符串长度

状态转移 不妨假设前面形成的字符串为s1,当前需要处理的字符串为s2。 根据题目意思,有两种拼接方法:s1s2 or s2s1

s1s2: 状态转移到dfs(i + 1, s1[0], s2[-1]), 长度增加len(s2) - (若拼接处相同,则减 1)
s2s1: 状态转移到dfs(i + 1, s2[0], s1[-1]), 长度增加len(s2) - (若拼接处相同,则减 1)

重复子问题 考虑以下情况 [‘aa’, ‘aa’, ‘aa’]

i=1,s=a,e=a. 此时无论把word[1]接到前面还是后面,下一个状态都是: dfs(2, a, a)

class Solution:
    def minimizeConcatenatedLength(self, words: List[str]) -> int:
        n = len(words)

        @cache
        def dfs(ind, s, e):
            if ind==n:
                return 0

            string = words[ind]
            # 顺序拼接
            res1 = dfs(ind+1, s, string[-1])+len(string)-(string[0]==e)
            # 反序拼接
            res2 = dfs(ind+1, string[0], e)+len(string)-(string[-1]==s)
            return min(res1, res2)


        return dfs(1, words[0][0], words[0][-1])+len(words[0])

参考 [leetcode]


4.2、记忆化搜索+状态压缩

2741.特别的排列

2741.特别的排列
要解决该题需要掌握三个方法:
1.排列型回溯
2.记忆化搜索 / 递推
3.状态压缩,集合以及将集合翻译成二进制,以及相关位运算。

class Solution:
    def specialPerm(self, nums: List[int]) -> int:
        mod = 10**9 + 7
        n = len(nums)
        # 2.cache记忆化搜索
        # 注意:1. 不再使用set去记录遍历过的值 2. return 结果
        # 记录函数每次的参数s,pre的答案
        # 下次遍历到相同参数直接return答案
        @cache
        # 1. 深度优先搜索。s表示当前可以选的下标集合,pre表示上一个选过的下标为k
        def dfs(s, pre):
            res = 0
            # 递归边界:dfs(0, j)=1,即集合s中没有元素了
            if s==0:
                return 1
            for k, u in enumerate(nums):
                # 当前元素在集合中存在,并且满足题目的两个条件
                if (s>>k)&1 and (nums[pre]%u==0 or u%nums[pre]==0):
                    res+= dfs(s^(1<<k), k)
            return res
        # 3.状态压缩,使用二进制代替nums每个数字的状态,1表示没取,0表示取了
        # 比如 1101 在全集{0,1,2,3}中表示集合{0,2,3},注意二进制从右向左。
        s = (1<<n)-1
        # 递归入口:循环nums,每次先取一个数当做pre,剩余元素当做s传入
        # 这里在初始必须先定义一个pre
        return sum(dfs(s^(1<<i), i) for i in range(n))%mod

用位运算完成如下题目:

78.子集
77.组合
46.全排列
然后是一些状态压缩 DP。这类题目通常和排列/子集有关,可以先从暴力回溯开始思考,再过渡到记忆化搜索和递推。

2172.数组的最大与和,题解
1125.最小的必要团队,题解
2305.公平分发饼干,题解
1494.并行课程 II,题解
LCP 53. 守护太空城,题解
1879.两个数组最小的异或值之和
1986.完成任务的最少工作时间段
更多题目,可以在题库中同时选上「动态规划」和「位运算」这两个标签:链接。

参考:
关于cache可以参考: Python中的@cache有什么妙用?
状压 DP [leetcode]
状压 DP / 0-1 背包【力扣周赛 350】 [bilibili]
分享|从集合论到位运算,常见位运算技巧分类总结! [leetcode]


996. 正方形数组的数目

996. 正方形数组的数目
题目类型同上,但是注意去重,去重的方法和前面的第二章一致,只不过用状态压缩来表示。

import math
class Solution:
    def numSquarefulPerms(self, nums: List[int]) -> int:
        n = len(nums)
        # 记忆化搜索
        @cache
        def dfs(s, pre):
            if s==0:
                return 1
            res = 0
            for k, u in enumerate(nums):
                # 相邻元素排除,(s>>(k-1))&1表示上一个元素如果没被选则continue
                # 最终只会选出按顺序遍历重复元素的排列
                if k>0 and nums[k]==nums[k-1] and (s>>(k-1))&1:
                    continue
                # k处元素是否存在,存在又是否满足题意,即完全平方数
                if  (s>>k)&1 and ((u+nums[pre])==int(math.sqrt(u+nums[pre]))**2):
                    res+=dfs(s^(1<<k), k)
            return res
        # 状态压缩
        s = (1<<n)-1
        # 排序,将重复元素邻居在一起,方便通过i和i-1进行重复排除
        nums.sort()
        return sum(dfs(s^(1<<i), i) for i in range(n) if i==0 or (i>0 and not (nums[i]==nums[i-1])))

2172. 数组的最大与和

2172. 数组的最大与和

class Solution:
    def maximumANDSum(self, nums: List[int], numSlots: int) -> int:
        n = len(nums)

        @cache
        # 两个参数,一个是nums的索引ind,一个是当前状态s
        def dfs(ind, s):
            if ind>=n:
                return 0
            res = 0
            # 每个篮子装两个,改为:
            #  用长度为 2 * numSlots 进行状压,第0,1位代表第一个篮子,2,3位代表第二个篮子以此类推。
            for i in range(2*numSlots):
                # 如果该位置的篮子是空的
                if (s>>i)&1==0:
                    # 状压s每个状态位对应的篮子编号则是i // 2 + 1
                    AND = nums[ind]&(i//2+1)
                    res = max(res, AND+dfs(ind+1, s|(1<<i))) # 用或,将1加入到结果
            return res
        return dfs(0,0)

1125. 最小的必要团队

1125. 最小的必要团队

class Solution:
    def smallestSufficientTeam(self, req_skills: List[str], people: List[List[str]]) -> List[int]:
        m = len(req_skills)
        n = len(people)
        
		# 根据req_skills将对应的索引转换出来
        skill_to_index = dict()
        for i, skill in enumerate(req_skills):
            skill_to_index[skill] = i
        
        # 将people的技能转化为索引,再转为二进制
        person_mask = [0] * n
        for i, skills in enumerate(people):
            for skill in skills:
                person_mask[i] |= (1<<skill_to_index[skill])
        @cache
        def dfs(skill_mask: int):
        # 团队技能满了,就返回空
            if skill_mask == (1<<m)-1:
                return []
            # 先设置一个最大值,后续寻找最小人数团队
            res = float('inf')
            # 存储每次的答案
            ans = []

            for i in range(n):
            	# 取并集,skill_mask若果没变,即==,说明已经存在people的技能
            	# 若结果变了,即!=,说明该people有需要的技能,则可以考虑加上,即dfs
                if skill_mask|person_mask[i]!=skill_mask:
                	# cun用来存储每个dfs的完整结果
                    cur = [i]+dfs(skill_mask|person_mask[i])
                    # 比较,存储短的结果
                    if len(cur)<res:
                        res = len(cur)
                        ans = cur
            return ans
        return dfs(0)

1494. 并行课程 II

1494. 并行课程 II


参考



五、其他练习题

207. 课程表 - 类似于 332.重新安排行程

207. 课程表

class Solution:
    def canFinish(self, numCourses: int, prerequisites: List[List[int]]) -> bool:
        buckets = [[] for _ in range(numCourses)]
        visited = [0]*numCourses
        valid = True
        # result = list()

        for pre in prerequisites:
            buckets[pre[0]].append(pre[1])
        
        def dfs(course):
            nonlocal valid
            visited[course]=1
            for c in buckets[course]:
                if not visited[c]:
                    dfs(c)
                    if not valid:
                        return
                # 判断是否有环
                elif visited[c]==1:
                    valid = False
                    return
            # 搜索过程中为1,判断是否为有环。整个搜索过了改为2
            visited[course] = 2
            # result.append(course)
            # return True

        for i in range(numCourses):
            if not visited[i]:
                dfs(i)
        return valid

210. 课程表 II - 同上题

210. 课程表 II

这题深广搜均可

class Solution:
    def findOrder(self, numCourses: int, prerequisites: List[List[int]]) -> List[int]:
        # 存储有向图
        edges = collections.defaultdict(list)
        # 标记每个节点的状态:0=未搜索,1=搜索中,2=已完成
        visited = [0] * numCourses
        # 用数组来模拟栈,下标 0 为栈底,n-1 为栈顶
        result = list()
        # 判断有向图中是否有环
        valid = True

        for info in prerequisites:
            edges[info[1]].append(info[0])
        
        def dfs(u: int):
            nonlocal valid
            # 将节点标记为「搜索中」
            visited[u] = 1
            # 搜索其相邻节点
            # 只要发现有环,立刻停止搜索
            for v in edges[u]:
                # 如果「未搜索」那么搜索相邻节点
                if visited[v] == 0:
                    dfs(v)
                    if not valid:
                        return
                # 如果「搜索中」说明找到了环
                elif visited[v] == 1:
                    valid = False
                    return
            # 将节点标记为「已完成」
            visited[u] = 2
            # 将节点入栈
            result.append(u)
        
        # 每次挑选一个「未搜索」的节点,开始进行深度优先搜索
        for i in range(numCourses):
            if valid and not visited[i]:
                dfs(i)
        
        if not valid:
            return list()
        
        # 如果没有环,那么就有拓扑排序
        # 注意下标 0 为栈底,因此需要将数组反序输出
        return result[::-1]

994. 腐烂的橘子 - 同1162. 地图分析

994. 腐烂的橘子

from collections import deque
class Solution:
    def orangesRotting(self, grid: List[List[int]]) -> int:
        row = len(grid)
        col = len(grid[0])
        time = 0
        # 统计新鲜橘子1的数量,下面BFS,每找到一个则减一,
        # 最后非0,说明有橘子遍历不到,则为-1
        nums = 0

        stack = deque([])
        for i in range(row):
            for j in range(col):
                if grid[i][j]==2:
                    stack.append((i,j))
                if grid[i][j]==1:
                    nums+=1
        
        while True:
            next_stack = deque([])
            while stack:
                x,y = stack.pop()
                for xi,yj in (x+1, y),(x-1,y),(x,y+1),(x,y-1):
                    if 0<=xi<row and 0<=yj<col and grid[xi][yj]==1:
                        grid[xi][yj]=2
                        nums-=1
                        next_stack.append((xi,yj))
            stack=next_stack
            if stack:
                time+=1
            else:
                break
        return time if not nums else -1

面试题 16.19. 水域大小 - 增加两个对角线

面试题 16.19. 水域大小

class Solution:
    def pondSizes(self, land: List[List[int]]) -> List[int]:
        row = len(land)
        col = len(land[0])
        res = []
        def dfs(i,j):
            nonlocal area
            area+=1
            land[i][j]=-1
            # 8个方向
            for x, y in (i+1,j),(i-1,j),(i,j+1),(i,j-1),(i+1,j+1),(i-1,j-1),(i-1,j+1),(i+1,j-1):
                if 0<=x<row and 0<=y<col and land[x][y]==0:
                    dfs(x,y)
        for i in range(row):
            for j in range(col):
                if land[i][j]==0:
                    area = 0
                    dfs(i,j)
                    res.append(area)

        return sorted(res)

79. 单词搜索

79. 单词搜索
普通的深度优先搜索题目。注意:增加ind与单词对应的字母与每一层的搜索字母进行对比,不对应则提前return False以减少复杂度,否则全部完整搜索的结果来对比整个单词是否相等,一旦单词过长或矩阵过大会超时。
在这里插入图片描述

class Solution:
    def exist(self, board: List[List[str]], word: str) -> bool:
        row = len(board)
        col = len(board[0])
        visited = set()

        def backtracking(i,j,ind):
            if board[i][j]!=word[ind]:
                return False
            if ind==len(word)-1:
                return True
            visited.add((i,j))
            for x, y in (i+1, j), (i-1, j), (i,j+1),(i,j-1):
                if 0<=x<row and 0<=y<col and (x,y) not in visited:
                    if backtracking(x,y,ind+1):
                        return True
            visited.remove((i,j))
            return False
        
        for i in range(row):
            for j in range(col):
                if board[i][j]==word[0]:
                    if backtracking(i,j,0):
                        return True
        return False

22. 括号生成

22. 括号生成
注意条件判断,左括号的数量大于等于右括号。

class Solution:
    def generateParenthesis(self, n: int) -> List[str]:
        res = []
        path = []

        def backtracking(left,right):
            if left==right==n:
                res.append(''.join(path[:]))
                return
            if 0<=left<n:
                path.append('(')
                backtracking(left+1,right)
                path.pop()
            if right<left:
                path.append(')')
                backtracking(left,right+1)
                path.pop()
        backtracking(0, 0)
        return res



深广搜问题相比回溯问题更加的简单,并且都有相似且固定的模板,相信你通过这篇文章,能对回溯与深广搜有了较为深刻的学习和认识!

参考

Fairly Nerdy
carlprogramming

  • 5
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 4
    评论
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值