基于visited_path+choice_list+target的回溯 2019-11-06(未经允许禁止转载)

20200804更新-回溯总结

回溯用于解决【1-n个元素的排列或组合问题】

  • 1.未优化的回溯算法需要遍历完所有的情况,一共是n(n-1)(n-2)...1即n!种,时间复杂度O(n!),是所有复杂度里面最大的。。。

  • 2.将原集合的子集看做子问题,做了子集备忘的回溯每个子问题只需要计算一次,而子问题的个数是2^n, 时间复杂度O(2^n),是所有复杂度里面正数第二大的。。。但是!这个复杂度也比O(n!)强多了。那么什么情况下可以使用【划分成 2^n个子集】呢?【解具有局部无序的特征】当只要求组间有序而组内无序的时候,如假设序列ABCDE为所求,但实际上BACED\ABDCE也都满足题意,AB一组,CDE一组,组间有序,组内无序——换句话说,无论先A后B,还是先B后A,都能够让问题演进到同样的状态。这一题的经典例题就是#### 312. 戳气球 and #### LCP 13. 寻宝 寻宝这题更难发现局部可无序这一潜在解特征。简单来讲就是,有M个机关要破解,机关之间的距离不同,找最短路径。一拿到估计就想回溯,时间复杂度M!,要优化一下。把M个机关映射到bitmap上面去,这样子问题数量缩减成2^M, 而不同子问题间存在递推关系,有递推就可以DP了

  • 3.【进阶】将原集合的子区间看做子问题,如集合[1,2,3,4,...,n],def(0, n)作为当前区间问题的解,则def(0, n) = best( def(0, i) + def(i, n), 0<=i<=n )。做了子区间备忘的回溯每个子问题只需要计算一次,而子问题的个数是n方,时间复杂度O(n^2)。这时实际上变成了更一般化的递归问题,无需状态复原。见 leetcode312. 戳气球,leetcode140. 单词拆分 II
    这里对140单词拆分进行简单分析

  1. 给定一个非空字符串 s 和一个包含非空单词列表的字典 wordDict,在字符串中增加空格来构建一个句子,使得句子中所有的单词都在词典中。返回所有这些可能的句子。
    说明:
    分隔时可以重复使用字典中的单词。
    你可以假设字典中没有重复的单词。
例如输入:
s = "catsanddog"
wordDict = ["cat", "cats", "and", "sand", "dog"]
输出:
[
  "cats and dog",
  "cat sand dog"
]

分析:
首先第一个想法肯定是暴力回溯,维护一个全局变量ans,当回溯到底的时候做结果结算,时间复杂度O(n!),n为s的长度
优化一下,考虑到某个子字符串s’可能会被重复计算,也就是会出现重复子问题,我们通过hashtable把子问题的解记录下来。具体地,对每一个字符串s,记录它的字典解析结果,不可解析时记录为[],通过记录实现剪枝,让整个问题从回溯转为记忆化dfs

class Solution:
    def __init__(self):
        self.res_dict = dict()

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

    # 记忆化dfs
    # dfs返回值: s对应的题目所求List[str]
    def dfs(self, s, wordDict):
        res = []
        # 递归出口。如果s为'',说明原始整个字符串已经顺利解析,返回非空的['']表示解析完毕;而返回[]则表示s不可解析
        if not s:
            return ['']
        
        if s not in self.res_dict:
            for i in range(len(s)+1):
                temp_word = s[:i]
                if temp_word in wordDict:
                    # dfs
                    behind_res = self.dfs(s[i:], wordDict)
                    # 如果behind_res为[],说明s[i:]不能进行字典解析,整个s也就不能顺利解析
                    for sentence in behind_res:
                        res.append(temp_word+(' '+sentence if sentence else ''))                                       
            self.res_dict[s] = res
        return self.res_dict[s]

小结:
当问题等价于求一个排列或者组合时,往往可以考虑回溯,很大概率命中
关键点就在于识别出这是个排列组合问题
一般地,求顺序的问题就是求排列,求子集的问题就是求组合


1、理解回溯

回溯就是回退,就是撤步/step back
举个例子,寻宝:

回溯简明图解

如上图,一个人从起点出发寻宝。他有3条路能走,为了找到宝藏,他决定按从左到右的顺序尝试每一条路:如果某条路走到头发现没有宝藏,就回退一步(回溯),重回上一点,继续尝试下一条路

2、回溯要点

  • 回溯问题是树形结构问题

**事实上,回溯问题是一个树形结构的问题,因为不断前进与回溯的过程会生长出一棵树--我叫做前进回溯树(forward back tree, FBT)

  • 回溯 = 深度优先遍历 + 状态复原 + [剪枝] (根据具体问题判断是否剪枝)

还是借用寻宝来理解


回溯树形图解

如图,假设只有C点有宝藏,寻宝的路程如图所示,其中黄色箭头表示叶结点向上回溯

  • 首先,人从起点到A到D,发现没有宝藏,回退到A;
  • 然后从A来到E处,发现没有宝藏,又回退到A;
  • 然后从A来到F处,发现没有宝藏,又回退到A;
  • 这时发现从A无论如何都到不了宝藏点,于是回退到起点
  • ......如此反复
  • 直到最终找到宝藏点C

由此我们可以看到,

  • 对于寻宝问题形成的FBT树,寻宝的过程遵循深度优先遍历,即先尝试一条路走到黑,达不到目的再step back;
  • 而step back,必须保证问题的各个状态回到上一结点的状态,即状态复原,如从D点回到A点,问题的状态需要恢复到A点的状态

    状态复原是回溯最最重要的一点

  • 可剪枝。对于某些回溯问题,可以通过避免不必要的前进(如只需找到一个解,那么找到之后就无需继续搜索)、按结点值的大小顺序构造树等操作来进行剪枝,减少多余计算

3、回溯问题模板

将问题对应一颗FBT树,解决问题的过程就是不断生长这棵树的过程。如何生长这棵树呢?

backtracking

backtracking(all_current_state),all_current_state包括当前已选路径visited_path+当前可选择列表choice_list+回溯出口target

  • 1.首先利用参数判断当前状态下是否进行结果结算
  • 2.从选择列表中选择一个son结点,将问题状态 更新为 son结点处对应的问题状态,表示选择son结点后问题来到了son结点处。【更新必须包括全部三项:visited_path+choice_list+target】
  • 3.递归调用自己
  • 4.递归返回后,进行状态复原,将前面第二步中修改的状态改回去
def backTrack(visited_path, choice_list, result, target):
        # visited_path, choice_list, target 这3种参数是必须的
        # visited_path表示当前的问题阶段,choice_list表示当前问题面临的可选项,target出现表示问题无需继续向下搜索,这时result用于结算可行解,
        # 递归出口
        if visited_path meets target:
            update result
            return
 
        # 尝试结点每一个可能的选择
        for c in choice_list:
            # 2.更新问题状态: 必须包含3项,visited_path+choice_list+target
            visited_path.add(c)
            new_choice_list = choice_list.remove(c)
            update target
            # 3.递归
            backTrack(visited_path, new_choice_list, result, target)
            # 4.状态复原
            visited_path.remove(c)
            reset target

4、回溯解决的问题

很简单,就2类,排列类问题和组合类问题

  • 排列类问题。每条路径对应一个排列,特点是路径可以纵向自由生长,不遵循某种约束规律
  • 组合类问题。在数学上,组合就是在排列的基础上消序;对应到回溯树上,就是在回溯树每次纵向向下生长的时候,必须遵循一定的规律,使得到整个路径是按照特定规律生成的,从而实现消序

因此,更抽象地来看,回溯解决的问题是【求解自由路径问题】和【求解有序路径问题】

5、例子

给定一个没有重复数字的序列,返回所有全排列
分析:
这是一个排列类问题,遍历完整个回溯树,叶子节点结算路径即可

class Solution:
    def permute(self, nums: List[int]) -> List[List[int]]:
        l = len(nums)
        used = [False] * l
        result = []
        cur_result = []
        self.backTracking(cur_result, used, nums, result)
        return result
        
        
    def backTracking(self, cur_result: List, used: List, nums: List, result: List):
        '''
        visited_path 用 cur_result表示,存放起点至当前结点形成的排列
        choice_list 可用 used: List & nums: List求出
        target  用cur_result长度 == nums长度表示
        
        used: List 标记序列中的某个元素是否被使用,长度与nums一致。
        result: List 用于存放给定序列的所有全排列                
        '''
        
        # 回溯出口
        if len(cur_result) == len(nums):
            # 结果结算
            print('result update')
            result.append(cur_result.copy())
            return
        
        for i in range(len(used)):
            # 如果元素未被使用,这些元素组成模板代码中的choice_list
            if not used[i]:
                # 更新状态:更新cur_result和used[i],target不变不用修改
                cur_result.append(nums[i])
                print('cur_result is %s' % str(cur_result))
                used[i] = True
                # 对结点进行操作,这里是进行递归操作
                self.backTracking(cur_result, used, nums, result)
                # 递归结束,恢复原来的状态
                used[i] = False
                cur_result.pop()

输入nums = [1,2,3],打印过程如下,显示了整棵FBT的生长过程

cur_result is [1]
cur_result is [1, 2]
cur_result is [1, 2, 3]
result update
cur_result is [1, 3]
cur_result is [1, 3, 2]
result update
cur_result is [2]
cur_result is [2, 1]
cur_result is [2, 1, 3]
result update
cur_result is [2, 3]
cur_result is [2, 3, 1]
result update
cur_result is [3]
cur_result is [3, 1]
cur_result is [3, 1, 2]
result update
cur_result is [3, 2]
cur_result is [3, 2, 1]
result update

返回:
[
[1,2,3],
[1,3,2],
[2,1,3],
[2,3,1],
[3,1,2],
[3,2,1]
]

有了本题的基础,我们可以给出求排列组合的一般形式:
以升序形式给定n个数,并指定一个k(0

显然,求n个数中所有k个数的排列,只是在上题的基础上,限制了树的深度。当len(visited_path)==k时,停止生长,即为target
而求n个数中k个数的所有组合,只需要在求排列的基础上进行消序操作即可。例如[1,2,3]和[2,1,3]是2个排列,但属于同一个组合。因此只需要选取排列中特定的一个作为组合结果,选升序的排列或者降序的排列都行。这也就是说,路径必须是升序或者降序的

class Solution:
    def combine(self, nums: list, k: int) -> List[List[int]]:
        n = len(nums)
        used = [False] * n
        layer_count = 0
        cur_result = []
        result = []
        self.backTracking(nums, used, cur_result, result, k)
        return result

    def backTracking(self, nums, used, cur_result, result, k):
        if len(cur_result) == k:
            result.append(cur_result[:])
            return
        else:
            for i in range(len(nums)):
                if not used[i]:
                    # 如果求组合,加入这句判断,通过形成升序的路径进行消序
                    if cur_result and nums[i] < cur_result[-1]:
                        continue
                    # 修改状态,visited_path + choice_list,target一直不变
                    used[i] = True
                    cur_result.append(nums[i])
                    # 递归
                    self.backTracking(nums, used, cur_result, result, k)
                    # 状态恢复
                    used[i] = False
                    cur_result.pop(-1)

趁热打铁
leetcode 40.
给定一个数组candidates和一个目标数target,找出candidates中所有可以使数字和为target的组合,并且candidates中的每个数字在每个组合中只能使用一次

说明:
所有数字(包括目标数)都是正整数。
解集不能包含重复的组合。
示例1:
输入: candidates =[10,1,2,7,6,1,5], target =8,
所求解集为:
[
[1, 7],
[1, 2, 5],
[2, 6],
[1, 1, 6]
]
原链接
分析:
看题,直接告诉我们,求的是【组合】。那么就需要设定路径的纵向生长顺序或者说生长规律。因此,这个规律可以是,将一个数num纳入路径后,再纵向生长的路径只能由num后面的数组成(这是万能的求组合的路径纵向生长规律)。这里有个小tip,我们选择将candidates先做个升序排序,这样一旦我们找到一个符合条件的路径,就可以进行剪枝。

class Solution:
    def combinationSum2(self, candidates: List[int], target: int) -> List[List[int]]:
        result = []
        part_result = []
        used = [False] * len(candidates)
        # 先排序candidates
        candidates = sorted(candidates)
        # 回溯
        self.backTracking(part_result, candidates, target, result)
        return result
        
            
    # 回溯函数    
    def backTracking(self, part_result, choice_list, target, result):
        # part_result 表示 visited_path
        # choice_list 表示 choice_list
        # target

        ## 到达叶子节点,返回True来实现剪枝。因为target < 0或者target == 0说明不用再纵向往下生长了,同一层也不需要横向生长了
        if target < 0:
            return True
        if target == 0:
            result.append(part_result.copy())
            return True

        last_ele = None
        for i in range(len(choice_list)):
            ele = choice_list[i]
            # 如果ele与同一层的前一个ele值相同,那么会纵向生长出相同的树枝就不满足题意。因此需要横向判断ele与同一层的前一个ele值
            if last_ele != ele:
                last_ele = ele

                # 修改3个状态。visited_path+choice_list+target
                part_result.append(ele)
                temp_target = target - ele
                # 递归。每次的choice_list都是candidates中ele之后的元素
                break_flag = self.backTracking(part_result, choice_list[i+1:], temp_target, result)
                # 状态复原
                target = temp_target + part_result.pop()
                # 根据break_flag进行【横向剪枝】
                if break_flag:
                    break
            else:
                continue
        ## 不是回溯树的叶子节点,【返回false表示不横向剪枝】
        return False                

递归返回后必须马上进行状态复原!!!!!!!!!!!!

还有 leetcode 51. N 皇后
N皇后问题,也是需要不断地尝试,当皇后不能以合法的位置放置时,回溯进行新的尝试,直到找到合法的放置方法

对于一个n X n的棋盘,我们希望从上往下逐行合法地放置一个皇后
那么,

  • 维护一个queen_pos作为阶段性结果集,保存当前已经放置好的皇后的位置
  • 维护一个dict,dict[pos] > 0 表示当前情况下pos位置非法,该位置属于dict[pos]个皇后的势力范围,现在不可以放置新皇后;如果dict[pos] == 0,则可以在此放置皇后
    我们每放置一个皇后,就把行、列和两个斜线上的所有位置的字典值+1;每次回溯移除一个皇后,就把相应的字典值-1
    理清思路后,代码也可以清晰地写出
class Solution:
    def solveNQueens(self, n: int) -> List[List[str]]:
        # 设置好规则,【dfs】暴力尝试所有解。如果能够dfs到第n层的,说明整个路径上的皇后位置合法,纳入结果集。
        # 用queen_pos list记录可行的皇后位置;用字典invalid_pos记录当前不可放置的位置,0可放,非0不可放
        queen_pos = list()
        invalid_pos = dict()
        for i in range(n):
            for j in range(n):
                invalid_pos[(i,j)] = 0
        final_res = []

        def helper(queen_pos, invalid_pos, row_index, final_res):
            # 已经到整个棋盘的n行,说明0-n-1行都放好了,结算并返回
            if row_index == n:
                temp = []
                for p in queen_pos:
                    s = '.'*p[1] + 'Q' + '.'*(n-1-p[1])
                    temp.append(s)
                final_res.append(temp)
                return

            for i in range(n):
                pos = (row_index, i)
                if invalid_pos[pos] == 0:
                    # 尝试放一个皇后
                    queen_pos.append(pos)
                    ## 更新不可放的位置 ##
                    additional_invalid_pos = []
                    for j in range(n):
                        # 同一行不可再放
                        additional_invalid_pos.append((row_index, j))
                    for j in range(n):
                        # 同一列不可再放
                        additional_invalid_pos.append((j, i))
                    s = row_index + i
                    delta = i - row_index
                    for j in range(n):
                        # 双斜线不可再放
                        if n > s-j >= 0 : additional_invalid_pos.append((j, s-j)) 
                        if 0 <= j + delta < n: additional_invalid_pos.append((j, j+delta)) 
                    for ele in additional_invalid_pos:
                        invalid_pos[ele] += 1
                    ## 更新不可放的位置,完毕 ##
                    # dfs
                    helper(queen_pos, invalid_pos, row_index+1, final_res)
                    # 状态复原
                    queen_pos.pop()
                    for ele in additional_invalid_pos:
                        invalid_pos[ele] -= 1
        
        helper(queen_pos, invalid_pos, 0, final_res)
        return final_res

6、再进阶一下

在上面的问题中,问题起点都是唯一的,即只有一个根结点,只产生一棵FBT树。但有时候,会存在多个问题起点,那么我们就需要对应产生多棵FBT树

例如leetcode79题:
给定一个二维网格和一个单词,找出该单词是否存在于网格中。
单词必须按照字母顺序,通过相邻的单元格内的字母构成,其中“相邻”单元格是那些水平相邻或垂直相邻的单元格。同一个单元格内的字母不允许被重复使用

示例:
board =
[
['A','B','C','E'],
['S','F','C','S'],
['A','D','E','E']
]
给定 word = "ABCCED", 返回 true.
给定 word = "SEE", 返回 true.
给定 word = "ABCB", 返回 false.

分析
以word = "ABCCED"为例

字符串以A开头,那么搜索的起点可以是board[0][0],也可以是board[2][0],起点有多个。对每一个起点,都要进行尝试

当访问完某个字符时,问题下一步可以去的位置是这个字符的上下左右位置,但注意,上下左右中溢出board边界的位置和已经访问过的位置要剔除,剔除后的位置就是可以去的一个个可选项

class Solution:
    def __init__(self):
        self.res = False

    def exist(self, board: List[List[str]], word: str) -> bool:
        n = len(board)
        m = len(board[0])
        for i in range(n):
            for j in range(m):
                if board[i][j] == word[0]:
                    visited_path = [(i,j)]
                    all_neibor = self.getNeibor(board, (i,j))
                    self.backTracking(visited_path, board, all_neibor, word[1:])
                    if self.res:
                        return True
        return False

    
    def getNeibor(self, board, pos):
        n = len(board)
        m = len(board[0])
        a, b = pos[0], pos[1]
        for i, j in [a, b-1],[a,b+1],[a-1,b],[a+1,b]:
            if 0 <= i < n and 0 <= j < m:
                yield (i, j)

    # 核心代码
    def backTracking(self, visited_path, board, all_neibor, target):
        # visited_path, list类型,记录走过的路径
        # visited_path和all_neibor结合可求出choice_list
        # target, 待查找的字符串

        if target == '':
            self.res = True
            return 
        
        for pos in all_neibor:
            if board[pos[0]][pos[1]] == target[0] and pos not in visited_path:
                # 改变visited_path的状态
                visited_path.append(pos)
                new_all_neibor = self.getNeibor(board, pos)
                self.backTracking(visited_path, board, new_all_neibor, target[1:])
                # 改回visited_path的状态
                visited_path.pop()
                if self.res:
                    return

7.进一步思考,回溯与dp的不同之处

回溯和dp看似有点相似,一个自顶向下,一个自底向上,但它们有本质的区别

  • 回溯——关键词【暴力试】回溯本质上就是【暴力遍历】找到符合要求的路径,因此回溯的每一次纵向向下生长都是尝试性的,就是不断地试,一路生长下去直到得到一条合规的路径,而不在乎中间结点是否是最优的
  • dp——关键词【最优解】dp本质上是求解子问题的最优解,dp table的每一个值都是有目的地计算得到的最优解,而不像回溯那样盲目尝试

同时,它们也有相同的地方:
不管是回溯树的生长,还是dp table的填写,都要基于之前的状态。它们都是用来解决【前置状态依赖问题】的方法。

例:
给定一个二维矩阵,0表示不能种树,1表示可以种树,同时相邻位置不能种树,求一共有多少种种树的方法
分析:
1.在某个地方种树与否,会影响相邻位置是否能够种树,进而影响到更远的位置是否能够种树。不同规模的问题会产生相互影响。好了,到这里我们发现,每次考虑要不要种树,都需要基于之前的状态,那么要么回溯要么dp。而dp是解决最优解问题的,回溯是解决排列组合问题的,问多少种方法,明显是组合题,因此考虑用回溯
2.再回到问题,求的是组合的种数。事实上,我们不管从哪个位置开始,也不管以怎么样的顺序去逐个安排种树,最终种数都是一样多的。因此,我们给回溯树纵向生长的路径指定一个规则:不妨从第一个1的位置开始,按从左到右,从上到下的顺序,考虑每一个1的位置是否种树

## 处理输入输出
# 输入:
# 2 3  // 表示2*3的矩阵
# 0 1 1
# 1 1 0
# 输出:8
import sys
content = list(sys.stdin)
n, m = [int(ele) for ele in content[0].strip().split()]
matrix = []
for i in range(1,len(content)):
    temp = [int(ele) for ele in content[i].strip().split()]
    matrix.append(temp)
## 处理输入输出

res = 0

# 获得某位置的4个邻居位置
def getNeibor(position_i, position_j):
    global n, m
    for i_index, j_index in [position_i+1, position_j], [position_i-1,position_j], [position_i, position_j-1], [position_i, position_j+1]:
        if 0 <= i_index < n and 0 <= j_index < m:
            yield (i_index, j_index)

# 筛选出所有能够进行组合的位置(值为1的位置),用数组保存。如[(0,1),(0,2),(1,0),(1,1)]            
def getValidPositions(matrix, n, m):
    res = []
    for i in range(n):
        for j in range(m):
            if matrix[i][j] == 1:
                res.append((i,j))
    return res

# tree_path 已种树的位置
# choice_list 选择列表
# 通过这两个参数就可以描述visited_path, choice_list和target
def backTracking(tree_path, choice_list):
    global res
    # 到达target回溯出口
    if choice_list == []:
        res += 1
        return 
    
    # 当前位置
    a, b = choice_list[0]
    ## 当前状态下,问题面临2种选择:种或者不种
    # 检查当前位置的邻居是否种树了
    flag = False
    for nb in getNeibor(a, b):
        if nb in tree_path:
            flag = True
            break
    # 1.周围没有种树,在当前位置种树,带着种树的状态推动回溯树继续纵向生长
    if not flag:
        tree_path.add((a,b))
        # 递归
        backTracking(tree_path, choice_list[1:])
        tree_path.remove((a,b))
    
    # 2.当前位置不种树,带着不种树的状态推动回溯树继续纵向生长
    backTracking(tree_path, choice_list[1:])

validpositions = getValidPositions(matrix, n, m)
# 回溯的初始状态,tree_path = set(),choice_list = validpositions
backTracking(set(), validpositions)
print(res)


leetcode面试题46. 把数字翻译成字符串
给定一个数字,我们按照如下规则把它翻译为字符串:0 翻译成 “a” ,1 翻译成 “b”,……,11 翻译成 “l”,……,25 翻译成 “z”。一个数字可能有多个翻译。请编程实现一个函数,用来计算一个数字有多少种不同的翻译方法。
示例 1:
输入: 12258
输出: 5
解释: 12258有5种不同的翻译,分别是"bccfi", "bwfi", "bczi", "mcfi"和"mzi"

分析:本题我第一个想到的是回溯,回溯是比较自然的正向思维。但是回溯的话涉及到了对重复子问题的计算,后来又优化了一下改成了dp。从本题其实可以看到,回溯是深度优先以线(路径达到最深)推进;dp是以面推进,面对重叠子问题有优势

# 以下是dp解法,因为这里面存在重叠的子问题,且子问题间可以建立起状态转移关系
class Solution:
    # 按照dp解法模板
    # 1.确认dp[i]记录s[:i]字串的翻译方法,即以i-1位置结尾的字串的翻译方法
    # 2.状态转移方程:dp[i] = dp[i-1] + dp[i-2] or dp[i] = dp[i-1]
    # 3.基础状态dp[0] = 1, dp[1] = 1
    # 4.dp table的遍历方向:从左到右
    def translateNum(self, num: int) -> int:
        s = str(num)
        dp = [0] * (len(s)+1)
        dp[0] = 1
        dp[1] = 1
        for i in range(2, (len(s)+1)):
            if 9 < int(s[i-2:i]) < 26:
                dp[i] = dp[i-1] + dp[i-2]
            else:
                dp[i] = dp[i-1]
        # print(dp)
        return dp[-1]

另外,上面提到,这里的回溯涉及到对重复子问题的计算,因此属于【图形回溯】,那么我们做个中间结果备忘也可以做的

class Solution:
    def __init__(self):
        # 字典记录,避免多次运算
        self.d = {"":1}

    def translateNum(self, num: int) -> int:
        # 问总和,就是暴力
        return self.dfsHelper(str(num))
    
    def dfsHelper(self, s) -> int :
        # 递归出口
        if s in self.d:
            return self.d[s]
        
        res = 0
        # ele最多为e2位数
        for i in range(1,min(3, len(s)+1)):
            ele = int(s[:i])
            if ele > 25:
                break                
            else:
                res += self.dfsHelper(s[i:])
                if ele == 0:
                    break

        self.d[s] = res
        return self.d[s]

你可能感兴趣的:(基于visited_path+choice_list+target的回溯 2019-11-06(未经允许禁止转载))