算法系列:回溯算法(四)一系列经典题:组合总和、不同路径、单词拆分和单词搜索

前言

回溯算法的一系列经典题:

  • 组合总和
  • 不同路径
  • 单词拆分
  • 单词搜索

1、组合总和

leet上39题

给定一个无重复元素的数组 candidates 和一个目标数 target ,找出 candidates 中所有可以使数字和为 target 的组合。
candidates 中的数字可以无限制重复被选取。
所有数字(包括 target)都是正整数。
解集不能包含重复的组合。 

典型的回溯题

class Solution:
    def combinationSum(self, candidates: List[int], target: int) -> List[List[int]]:
        candidates.sort()
        res = []
        n = len(candidates)
        def backtrack(i, tmp, num):
            if i == n or num > target:
                return
            if num == target:
                res.append(tmp)
                return 
            backtrack(i, tmp + [candidates[i]], num + candidates[i])
            backtrack(i+1, tmp, num)
        backtrack(0, [], 0)
        return res
变种1

candidates 中的数字只能用一次

class Solution:
    def combinationSum2(self, candidates: List[int], target: int) -> List[List[int]]:
        if not candidates: return []
        candidates.sort()
        n = len(candidates)
        res = []
        def backtrack(i, num, tmp):
            if num == target:
                res.append(tmp)
                return
            for j in range(i, n):
                if num + candidates[j] > target: 
                    break
                if j > i and candidates[j] == candidates[j - 1]:
                    continue
                backtrack(j + 1, num + candidates[j], tmp + [candidates[j]])
        backtrack(0, 0, [])
        return res
变种2

在变种1的基础上限定 candidates = [1, 2, 3, 4, 5, 6, 7, 8, 9]
且每种组合的长度设定为k

class Solution:
    def combinationSum3(self, k: int, n: int) -> List[List[int]]:
        if n <= 0 or n > (k*9): return []
        res = []
        def backtrack(k, n, i, tmp):
            if k == 0:
                if n == 0:
                    res.append(tmp)
                return
            for j in range(i, 10):
                if n-j < 0: break
                backtrack(k-1, n-j, j+1, tmp+[j])
        backtrack(k, n, 1, [])
        return res
变种3

顺序不同的序列算不同组合
求组合数

这样一改
再用回溯时间就会超时

又只求组合数
就用动态规划了

class Solution:
    def combinationSum4(self, nums: List[int], target: int) -> int:
        from collections import defaultdict
        dp = defaultdict(int)
        dp[0] = 1
        for i in range(1, target + 1):
            for num in nums:
                dp[i] += dp[i - num]
        return dp[target]

2、不同路径

leet上62题

一个机器人位于一个 m x n 网格的左上角 (起始点在下图中标记为 “Start” )。
机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为 “Finish” )。
问总共有多少条不同的路径?

这当然可以用回溯
但会超时

class Solution:
    def uniquePaths(self, m: int, n: int) -> int:
        def backtrack(x,y):
            num1 = 0
            num2 = 0
            if x == m and y == n:
                return 1
            if y < n:
                num1 = backtrack(x, y+1)
            if x < m :
                num2 = backtrack(x+1, y)
            return num1 + num2
        return backtrack(1, 1)

这里用动态规划

# 动态规划
class Solution:
    def uniquePaths(self, m: int, n: int) -> int:
        dp = [1] * n
        for i in range(1, m):
            for j in range(1, n):
                dp[j] += dp[j-1]
        return dp[-1]
变种

设置了障碍

1 表示起始方格,且只有一个起始方格
2 表示结束方格,且只有一个结束方格
0 表示我们可以走过的空方格
-1 表示我们无法跨越的障碍

且不再是从左上到右下,只能向右向下
而是从起始到结束,要通过每个空方格

这就是标准的回溯了

class Solution:
    def uniquePathsIII(self, grid: List[List[int]]) -> int:
        m, n = len(grid), len(grid[0])
        direct = [(0, 1), (1, 0), (0, -1), (-1, 0)]
        self.res = 0
        start = end = None
        empty = set()
        for i in range(m):
            for j in range(n):
                if grid[i][j] == 1:
                    start = (i, j)
                elif grid[i][j] == 2:
                    end = (i, j)
                elif grid[i][j] == 0:
                    empty.add((i, j))
        def backtrack(x, y):
            for dx, dy in direct:
                tmp_x, tmp_y = x+dx, y+dy
                if not empty:
                    if (tmp_x, tmp_y) == end:
                        self.res += 1
                elif (tmp_x, tmp_y) in empty:
                    empty.remove((tmp_x, tmp_y))
                    backtrack(tmp_x, tmp_y)
                    empty.add((tmp_x, tmp_y))
        backtrack(start[0],start[1])
        return self.res

3、单词拆分

leet上139题

给定一个非空字符串 s 和一个包含非空单词的列表 wordDict
判定 s 是否可以被空格拆分为一个或多个在字典中出现的单词
拆分时可以重复使用字典中的单词
你可以假设字典中没有重复的单词

暴力回溯会超时
拿[aaaab]来举例,它可分为[“a”,“a”,“aab”],[“aa”,“aab”],两个都包含了"aab",所以就产生了重复的递归
于是[aaaaaa…aaab]这样的例子就超时了

要记忆化

class Solution:
    def wordBreak(self, s: str, wordDict: List[str]) -> bool:
        if not wordDict: return not s
        from functools import lru_cache
        @lru_cache(None)
        def backtrack(s):
            if not s:
                return True
            res = False
            for i in range(len(s)+1):
                if s[:i] in wordDict:
                    res = backtrack(s[i:]) or res
            return res
        return backtrack(s)

当然这题用动态规划记录更好

class Solution:
    def wordBreak(self, s: str, wordDict: List[str]) -> bool:
        if not wordDict: return not s
        n = len(s)
        dp = [False] * (n+1)
        dp[0] = True
        for i in range(1, n+1):
            for j in range(i-1, -1, -1):
                if dp[j] and s[j:i] in wordDict:
                    dp[i] = True
                    break
        return dp[-1]
变种

改为列出所有可能性

class Solution:
    def wordBreak(self, s: str, wordDict: List[str]) -> List[str]:
        if not wordDict: return []
        wordDict = set(wordDict)
        n = len(s)
        from functools import lru_cache
        @lru_cache(None)
        def backtrack(i):
            if i == n:
                return [[]]
            res = []
            for j in range(i+1, n+1):
                word = s[i:j]
                if word in wordDict:
                    tmp = backtrack(j)
                    for item in tmp:
                        res.append(item.copy()+[word])
            return res
        res_list = backtrack(0)
        return [" ".join(words[::-1]) for words in res_list]

4、单词搜索

leet上79题

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

就是回溯
就是枚举

class Solution:
    def exist(self, board: List[List[str]], word: str) -> bool:
        m, n = len(board), len(board[0])
        lenth = len(word)
        direct = [(-1, 0), (1, 0), (0, 1), (0, -1)]
        def backtrack(x, y, k, tmp):
            if k == lenth:
                return True
            for dx, dy in direct:
                tmp_x, tmp_y = dx+x, dy+y
                if 0 <= tmp_x < m and 0 <= tmp_y < n and (tmp_x, tmp_y) not in tmp and board[tmp_x][tmp_y] == word[k]:
                    tmp.add((tmp_x, tmp_y))
                    if backtrack(tmp_x, tmp_y, k+1, tmp):
                        return True
                    tmp.remove((tmp_x, tmp_y))
            return False
        for i in range(m):
            for j in range(n):
                if board[i][j] == word[0] and backtrack(i, j, 1, {(i,j)}):
                    return True
        return False    
变种

用一个列表的words替换word
找出words中在表格中的单词

直接沿用就是了

class Solution:
    def findWords(self, board: List[List[str]], words: List[str]) -> List[str]:
        ans = []
        for word in words:
            if self.exist(board, word):
                ans.append(word)
        return ans

    def exist(self, board: List[List[str]], word: str) -> bool:
        m, n = len(board), len(board[0])
        lenth = len(word)
        direct = [(-1, 0), (1, 0), (0, 1), (0, -1)]
        def backtrack(x, y, k, tmp):
            if k == lenth:
                return True
            for dx, dy in direct:
                tmp_x, tmp_y = dx+x, dy+y
                if 0 <= tmp_x < m and 0 <= tmp_y < n and (tmp_x, tmp_y) not in tmp and board[tmp_x][tmp_y] == word[k]:
                    tmp.add((tmp_x, tmp_y))
                    if backtrack(tmp_x, tmp_y, k+1, tmp):
                        return True
                    tmp.remove((tmp_x, tmp_y))
            return False
        for i in range(m):
            for j in range(n):
                if board[i][j] == word[0] and backtrack(i, j, 1, {(i,j)}):
                    return True
        return False  

结语

四篇下来
应该对回溯有个感觉了

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值