回溯算法和深度优先遍历

「回溯算法」与「深度优先遍历」都有「不撞南墙不回头」的意思。我个人的理解是:「回溯算法」强调了「深度优先遍历」思想的用途,用一个 不断变化 的变量,在尝试各种可能的过程中,搜索需要的结果。强调了 回退 操作对于搜索的合理性。而「深度优先遍历」强调一种遍历的思想,与之对应的遍历思想是「广度优先遍历」
回溯算法用于搜索一个问题的所有的解,用到了DFS的思想
回溯和动态规划
DP只需要知道最优解是什么,不关心怎么来的
回溯要列举出所有的可能性

排列问题

按顺序枚举每一位可能出现的情况,已经选择的数字在 当前 要选择的数字中不能出现。按照这种策略搜索就能够做到 不重不漏
image.png
回溯和普通的DFS的区别在于,有回头的过程,且回头的时候需要把当前已经访问的节点状态重置

  1. 按引用传递状态
  2. 所有的状态修改在递归完成后回改

46. 全排列

https://leetcode-cn.com/problems/permutations/

  1. # res = []
  2. # def backtracking(nums, tmp):
  3. # if not nums:
  4. # res.append(tmp)
  5. # return
  6. # for i in range(len(nums)):
  7. # backtracking(nums[:i]+nums[i+1:], tmp+[nums[i]])
  8. # backtracking(nums,[])
  9. # return res
  10. depth = len(nums)
  11. visited = [False for _ in range(depth)]
  12. res = []
  13. def backtracking(temp_list, height):
  14. if height == depth:
  15. res.append(temp_list)
  16. return
  17. for i in range(depth):
  18. if not visited[i]:
  19. visited[i] = True
  20. backtracking(temp_list+[nums[i]],height+1)
  21. visited[i] = False
  22. backtracking([],0)
  23. return res

47. 全排列 II

https://leetcode-cn.com/problems/permutations-ii/
主要是去重的判断条件.

  1. 排序, 保证相同的数字都相邻
  2. 不选择的情况有:

    1. 当前元素已经visited
    2. i>0 且当前元素等于前一个元素 且 前一个元素已经被使用.

      1. class Solution:
      2. def permuteUnique(self, nums: List[int]) -> List[List[int]]:
      3. n = len(nums)
      4. if n <2:
      5. return [nums]
      6. res = []
      7. nums.sort()
      8. visited = [False for _ in range(n)]
      9. def backtracking(temp_list, depth):
      10. if depth==n:
      11. res.append(temp_list)
      12. for i in range(n):
      13. if visited[i] or (i>0 and nums[i]==nums[i-1] and not visited[i-1]):
      14. continue
      15. visited[i]=True
      16. backtracking(temp_list+[nums[i]], depth+1)
      17. visited[i]=False
      18. backtracking([],0)
      19. return res

      77. 组合

      https://leetcode-cn.com/problems/combinations/

  1. class Solution:
  2. def combine(self, n: int, k: int) -> List[List[int]]:
  3. res = []
  4. if k>n:
  5. return res
  6. if k == 0:
  7. return [res]
  8. def backtracking(nums,temp_list, index):
  9. if len(temp_list)==k:
  10. res.append(temp_list[:])
  11. return
  12. for i in range(index,n):
  13. temp_list.append(nums[i])
  14. backtracking(nums[index:],temp_list, i+1)
  15. temp_list.pop()
  16. nums = [i for i in range(1,n+1)]
  17. backtracking(nums,[],0)
  18. return res

39. 组合总和

https://leetcode-cn.com/problems/combination-sum/
什么时候使用 used/visited数组,什么时候使用 begin 变量

  • 排列问题: 讲究顺序(即 [2, 2, 3] 与 [2, 3, 2] 视为不同列表时,需要记录哪些数字已经使用过,此时用 used 数组;
  • 组合问题:不讲究顺序(即 [2, 2, 3] 与 [2, 3, 2] 视为相同列表时,需要按照某种顺序搜索,此时使用 begin 变量。

注意:具体问题应该具体分析, 理解算法的设计思想 是至关重要的,请不要死记硬背。

  1. class Solution:
  2. def combinationSum(self, candidates: List[int], target: int) -> List[List[int]]:
  3. candidates.sort()
  4. res = []
  5. length = len(candidates)
  6. def backtracking(temp_list, index, target):
  7. if target==0:
  8. res.append(temp_list[:])
  9. return
  10. for i in range(index, length):
  11. residue = target - candidates[i]
  12. if residue < 0:
  13. break
  14. backtracking(temp_list+[candidates[i]], i, residue)
  15. backtracking([],0,target)
  16. return res

40. 组合总和 II

https://leetcode-cn.com/problems/combination-sum-ii/
这道题的核心相比上一题在于去重,也就是代码备注的部分
https://leetcode-cn.com/problems/combination-sum-ii/solution/hui-su-suan-fa-jian-zhi-python-dai-ma-java-dai-m-3/
参考高赞评论

  1. class Solution:
  2. def combinationSum2(self, candidates: List[int], target: int) -> List[List[int]]:
  3. size = len(candidates)
  4. res = []
  5. if size<1:
  6. return []
  7. def backtracking(temp_list, begin, target):
  8. if target == 0:
  9. res.append(temp_list[:])
  10. return
  11. for i in range(begin,size):
  12. residue = target - candidates[i]
  13. if residue<0:
  14. break
  15. #去重的核心在于这个判断
  16. # i>begin是为了保证同一层的第一个和上一级相同的数是允许的
  17. # i == i-1 是说同一层第二个及以后相同的数是不允许的
  18. if i > begin and candidates[i]==candidates[i-1]:
  19. continue
  20. backtracking(temp_list+[candidates[i]],i+1, residue)
  21. candidates.sort()
  22. backtracking([],0,target)
  23. return res

17. 电话号码的字母组合

https://leetcode-cn.com/problems/letter-combinations-of-a-phone-number/
难得被回溯虐了一天半之后, 自己调试出来一道回溯的题

  1. class Solution:
  2. def letterCombinations(self, digits: str) -> List[str]:
  3. lookup = {'2':'abc','3':'def','4':'ghi','5':'jkl','6':'mno','7':'pqrs','8':'tuv','9':
  4. 'wxyz'}
  5. size = len(digits)
  6. res = []
  7. if size==0:
  8. return []
  9. def backtracking(temp_list, index):
  10. if len(temp_list) == size:
  11. res.append("".join(temp_list[:]))
  12. # 不要试图去构建递归的整个过程,理解两层循环所对应的不同的树的层级
  13. for i in range(index,size): # 这一层循环对应每个数字
  14. panel = lookup[digits[i]] # 每个数字对应3 or 4个字母
  15. for j in range(len(panel)): # 这个字母里选一个
  16. backtracking(temp_list+[panel[j]],i+1)
  17. backtracking([],0)
  18. return res

79. 单词搜索

https://leetcode-cn.com/problems/word-search/
一开始我按照岛屿问题的方法写,发现返回不到想要的结果。

  1. line16标记,line22退回, 因为一个矩阵里,可能有多个相同的字母,有时候从一个位置出发不行,但从另一个位置出发是可以的

    1. class Solution:
    2. def exist(self, board: List[List[str]], word: str) -> bool:
    3. if not board:
    4. return False
    5. rows = len(board)
    6. cols = len(board[0])
    7. DIRECTION = ((1,0),(-1,0),(0,1),(0,-1))
    8. visited=[[False for _ in range(cols)] for _ in range(rows)]
    9. def inArea(x,y):
    10. return x>=0 and y>=0 and x<rows and y<cols
    11. def dfs(index,x,y):
    12. if index == len(word)-1:
    13. return board[x][y]==word[index]
    14. if board[x][y]==word[index]:
    15. visited[x][y]=True
    16. for k in range(4):
    17. new_x = x + DIRECTION[k][0]
    18. new_y = y + DIRECTION[k][1]
    19. if inArea(new_x,new_y) and not visited[new_x][new_y] and dfs(index+1,new_x,new_y):
    20. return True
    21. visited[x][y]=False
    22. for i in range(rows):
    23. for j in range(cols):
    24. if dfs(0,i,j):
    25. return True
    26. return False