【leetcode刷题版】回溯算法

系列文章目录


文章目录

  • 系列文章目录
  • 背景知识
  • 一、组合
  • 二、组合优化
  • 三、电话号码的字母组合
  • 四、组合总和
  • 五、组合总和Ⅱ
  • 六、分割回文串
  • 七、复原IP地址
  • 八、子集
  • 九、子集(需要去重)
  • 十、非递减子序列
  • 十一、全排列
  • 十一、全排列Ⅱ
  • 十二、重新安排行程(难)
  • 十三、N皇后
  • 十四、解数独


背景知识

回溯算法是一种通过试错来解决问题的算法。它会在解决问题的过程中剪枝,以避免无效搜索。在Python中实现回溯算法通常涉及以下几个步骤:

  1. 定义问题:明确你要解决的问题,比如八皇后问题、数独求解、组合问题等。

  2. 建立递归结构:回溯算法通常以递归函数的形式实现。这个递归函数会尝试不同的解决方案,并在解决方案不可行时回溯。

  3. 剪枝:在递归过程中,如果发现当前路径不可能产生正确的完整解决方案,就放弃这条路径,回溯到上一步。

  4. 记录解:一旦找到一个完整的解决方案,就将其记录下来。

  5. 回溯:从最后一步向前撤销,尝试其他可能的选项。

下面是一个简单的回溯算法的例子,用于解决一个组合问题:给定一组不重复的整数,找出所有可能的组合,其和为特定目标数。

def combination_sum(candidates, target):
    def backtrack(remain, combo, start):
        if remain == 0:  # 找到一个解
            result.append(list(combo))
            return
        elif remain < 0:  # 超过目标值,剪枝
            return
        for i in range(start, len(candidates)):
            combo.append(candidates[i])  # 选择当前数字
            backtrack(remain - candidates[i], combo, i)  # 继续寻找下一个数字
            combo.pop()  # 撤销选择,回溯

    candidates.sort()  # 对数字进行排序,有助于剪枝
    result = []
    backtrack(target, [], 0)
    return result

# 示例
candidates = [2, 3, 6, 7]
target = 7
print(combination_sum(candidates, target))

在这个例子中:

  • candidates 是一个不包含重复元素的整数数组。
  • target 是目标和。
  • backtrack 是递归函数,它尝试构建一个组合,使其和为 target
  • 如果 remain(剩余需要达到的目标和)变成 0,说明找到了一个有效的组合,将其添加到结果列表 result 中。
  • 如果 remain 变成负数,说明当前路径不可能成功,因此回溯。
  • 通过 candidates.sort() 对候选数字进行排序,可以避免选择相同的数字,有助于剪枝。
  • result 存储所有找到的组合。

回溯算法是一种强大的算法框架,可以解决许多涉及搜索的问题,尤其是在组合数学和约束满足问题中非常有用。

回溯算法是一种强大的问题解决方法,它通过探索所有可能的候选解来找到所有解或最优解。这种算法特别适合于解决组合问题、排列问题、划分问题、优化问题等。以下是回溯算法的一些主要应用领域:

  1. 组合问题

    • 如找出集合中所有可能的k个元素的组合,例如在药物组合、商品推荐等领域。
  2. 排列问题

    • 如找出集合中所有可能的排列,常用于密码破解、排列组合问题等。
  3. 划分问题

    • 如将一个集合划分为满足特定条件的多个子集,例如在资源分配、任务调度等领域。
  4. 优化问题

    • 如旅行商问题(TSP),寻找最短的可能路线,经过每个城市恰好一次并返回起点。
  5. 游戏AI

    • 在棋类游戏(如国际象棋、围棋)和牌类游戏(如扑克)中,用于计算最佳走法。
  6. 约束满足问题(CSP):

    • 如数独游戏,通过回溯算法找到满足所有约束条件的解决方案。
  7. 图论问题

    • 如寻找图中的所有路径、回路,或特定的图结构,例如生成树、最短路径等。
  8. 字符串问题

    • 如找出满足特定模式的所有子串,或进行字符串编辑(如找出最少的编辑操作将一个字符串转换为另一个字符串)。
  9. 调度问题

    • 如课程表的安排、任务调度等,需要满足各种约束条件。
  10. 网络设计

    • 如网络路由问题,寻找最优的数据传输路径。
  11. 逻辑推理

    • 如在人工智能中,用于解决逻辑推理问题,如八皇后问题。
  12. 生物信息学

    • 如序列比对、基因序列分析等。

回溯算法的关键特点是它能够通过剪枝(即放弃那些不可能产生最优解的路径)来减少搜索空间,从而提高算法的效率。虽然回溯算法可能在最坏情况下需要指数级的时间复杂度,但在实际应用中,合理的剪枝策略可以显著减少计算量。

一、组合

给定两个整数n和k,返回1.n中所有可能的k个数的组合
示例:输入:n=4, k=2 输出: [ [2,4],[3,4],[2,3],[1,2],[1,3],[1,4],]

from typing import List
class Solution:
    def combine(self, n: int, k: int) -> List[List[int]]:
        def backtrack(start,path):
            if len(path) == k:
                result.append(path[:])
            for i in range(start,n+1):
                path.append(i)
                backtrack(i+1,path)
                path.pop()
        result = []    
        backtrack(1,[])
        return result

二、组合优化

找出所有相加之和为n的k个数的组合。组合中只允许含有1-9的正整数,并且每种组合中不存在重复的数字。
说明:
所有数字都是正整数。
解集不能包含重复的组合,
示例 1:输入: k=3, n=7 输出: [[1,2,4]]
示例 2:输入: k=3,n=9 输出: [[1,2,6],[1,3,5],[2,3,4]]

class Solution:
    def combinationSum3(self, k: int, n: int) -> List[List[int]]:
        def backtrack(remain,start,path):
            if len(path) == k:
                if remain == 0 :
                    result.append(path[:])
                return
            for i in range(start, 10):
                if remain - i < 0:
                    break
                path.append(i)
                backtrack(remain-i,i+1,path)
                path.pop()
        result = []
        backtrack(n,1,[])
        return result
        

三、电话号码的字母组合

给定一个仅包含数字 2-9 的字符串,返回所有它能表示的字母组合。
给出数字到字母的映射如下(与电话按键相同)。注意 1 不对应任何字母。

class Solution:
    def letterCombinations(self, digits: str) -> List[str]:
        # 电话按键对应的字母
        phone_map = {'2':'abc',
        '3':'def',
        '4':'ghi',
        '5':'jkl',
        '6':'mno',
        '7':'pqrs',
        '8':'tuv',
        '9':'wxyz'}
        result = []
        if not digits:
            return result
        def backtrack(combination,next_digits):
            if len(next_digits) == 0:
                result.append(combination)
            else:
                for letter in phone_map[next_digits[0]]:
                    backtrack(combination + letter, next_digits[1:])
          
        backtrack("",digits)
        return result
        

四、组合总和

给定一个无重复元素的数组 candidates 和一个目标数 target ,找出 candidates 中所有可以使数字和为 target 的组合。
candidates 中的数字可以无限制重复被选取。

from typing import List
class Solution:
    def combinationSum(self, candidates: List[int], target: int) -> List[List[int]]:
        
        result = []
        def trackback(remain,combo,index):
            if remain == 0:
                result.append(list(combo))
                return
            if remain < 0:
                return
            for i in range(index,len(candidates)):
                combo.append(candidates[i])
                trackback(remain-candidates[i],combo,i)
                combo.pop()
        candidates.sort()


        trackback(target,[],0)
        return result

        

五、组合总和Ⅱ

给定一个字符串 s,将 s 分割成一些子串,使每个子串都是回文串。
返回 s 所有可能的分割方案。
示例: 输入: “aab” 输出: [ [“aa”,“b”], [“a”,“a”,“b”] ]

from typing import List

class Solution:
    def combinationSum2(self, candidates: List[int], target: int) -> List[List[int]]:
        result = []
        candidates.sort()  # 排序,以便跳过重复的数字

        def backtrack(remain, combo, index):
            if remain == 0:
                result.append(list(combo))  # 找到一个解,添加到结果中
                return
            if remain < 0:
                return  # 超过目标值,剪枝

            for i in range(index, len(candidates)):
                # 跳过重复的数字
                if i > index and candidates[i] == candidates[i - 1]:
                    continue
                # 如果当前数字大于剩余需要的数字,剪枝
                if candidates[i] > remain:
                    break
                combo.append(candidates[i])
                backtrack(remain - candidates[i], combo, i + 1)  # i + 1 确保不使用同一元素
                combo.pop()  # 回溯,移除最后一个元素

        backtrack(target, [], 0)
        return result

# 输入示例
candidates = [10, 1, 2, 7, 6, 1, 5]
target = 8
solution = Solution()
print(solution.combinationSum2(candidates, target))

六、分割回文串

给定一个字符串 s,将 s 分割成一些子串,使每个子串都是回文串。
返回 s 所有可能的分割方案。
示例: 输入: “aab” 输出: [ [“aa”,“b”], [“a”,“a”,“b”] ]

def is_palindrome(s: str) -> bool:
    # 将字符串转换为统一的小写或大写,以忽略大小写差异
    s = s.lower()
    # 反转字符串
    reverse_s = s[::-1]
    # 比较原始字符串和反转后的字符串
    return s == reverse_s

# 测试
print(is_palindrome("madam"))  # 输出: True
print(is_palindrome("hello"))  # 输出: False

七、复原IP地址

给定一个只包含数字的字符串,复原它并返回所有可能的 IP 地址格式。
有效的 IP 地址 正好由四个整数(每个整数位于 0 到 255 之间组成,且不能含有前导 0),整数之间用 ‘.’ 分隔。
例如:“0.1.2.201” 和 “192.168.1.1” 是 有效的 IP 地址,但是 “0.011.255.245”、“192.168.1.312” 和 “[email protected]” 是 无效的 IP 地址。

from typing import List

class Solution:
    def restoreIpAddresses(self, s: str) -> List[str]:
        def backtrack(start, path, s):
            # 如果路径中的部分数量为4且已经处理完所有字符,则添加到结果中
            if len(path) == 4 and start == len(s):
                result.append(".".join(path))
                return
            # 尝试添加下一个部分到路径中
            #从当前开始索引 start 开始,尝试长度为1到3的子字符串
            for end in range(start + 1, min(start + 4, len(s) + 1)):
                part = s[start:end]
                # 检查部分是否有效
                if (part[0] == '0' and len(part) > 1) or (int(part) > 255):
                    continue
                backtrack(end, path + [part], s)

        result = []
        backtrack(0, [], s)
        return result

# 示例
solution = Solution()
s = "25525511135"
print(solution.restoreIpAddresses(s))

八、子集

给定一组不含重复元素的整数数组 nums,返回该数组所有可能的子集(幂集)。
说明:解集不能包含重复的子集。
示例: 输入: nums = [1,2,3] 输出: [ [3], [1], [2], [1,2,3], [1,3], [2,3], [1,2], [] ]

from typing import List

class Solution:
    def subsets(self, nums: List[int]) -> List[List[int]]:
        def backtrack(start, path):
            # 将当前路径添加到结果中
            result.append(list(path))
            for i in range(start, len(nums)):
                # 选择当前元素
                path.append(nums[i])
                # 递归选择下一个元素
                backtrack(i + 1, path)
                # 回溯,移除最后一个元素
                path.pop()
        
        result = []
        backtrack(0, [])
        return result

# 示例
solution = Solution()
nums = [1, 2, 3]
print(solution.subsets(nums))

九、子集(需要去重)

给定一个可能包含重复元素的整数数组 nums,返回该数组所有可能的子集(幂集)。
说明:解集不能包含重复的子集。

from typing import List

class Solution:
    def subsetsWithDup(self, nums: List[int]) -> List[List[int]]:
        def backtrack(start, path):
            # 将当前路径添加到结果中
            result.append(list(path))
            for i in range(start, len(nums)):
                # 跳过重复的元素
                if i > start and nums[i] == nums[i - 1]:
                    continue
                # 选择当前元素
                path.append(nums[i])
                # 递归选择下一个元素,注意下一次从i+1开始,因为i+1的元素可能与i的元素相同
                backtrack(i + 1, path)
                # 回溯,移除最后一个元素
                path.pop()
        
        # 对数组进行排序,以便后续去重
        nums.sort()
        result = []
        backtrack(0, [])
        return result

# 示例
solution = Solution()
nums = [1, 2, 2]
print(solution.subsetsWithDup(nums))

十、非递减子序列

给定一个整型数组, 你的任务是找到所有该数组的递增子序列,递增子序列的长度至少是2。
示例:
输入: [4, 6, 7, 7]
输出: [[4, 6], [4, 7], [4, 6, 7], [4, 6, 7, 7], [6, 7], [6, 7, 7], [7,7], [4,7,7]]
说明:
给定数组的长度不会超过15。
数组中的整数范围是 [-100,100]。
给定数组中可能包含重复数字,相等的数字应该被视为递增的一种情况。

from typing import List

class Solution:
    def findSubsequences(self, nums: List[int]) -> List[List[int]]:
        def backtrack(start, path):
            # 将当前路径添加到结果中,如果路径长度至少为2
            if len(path) > 1:
                result.append(list(path))
            for i in range(start, len(nums)):
                # 跳过重复的元素
                if i > start and nums[i] == nums[i - 1]:
                    continue
                # 如果当前元素大于前一个元素,不选择它
                if path and nums[i] < path[-1]:
                    continue
                # 选择当前元素
                path.append(nums[i])
                # 递归选择下一个元素,注意下一次从i+1开始
                backtrack(i + 1, path)
                # 回溯,移除最后一个元素
                path.pop()
        
        result = []
        backtrack(0, [])
        return result

# 示例
solution = Solution()
nums = [4, 6, 7, 7]
print(solution.findSubsequences(nums))

十一、全排列

给定一个 没有重复 数字的序列,返回其所有可能的全排列。

示例:

输入: [1,2,3]
输出: [ [1,2,3], [1,3,2], [2,1,3], [2,3,1], [3,1,2], [3,2,1] ]

from typing import List

class Solution:
    def permute(self, nums: List[int]) -> List[List[int]]:
        def backtrack(first=0):
            if first == len(nums):
                # 找到一个全排列,添加到结果中
                result.append(nums[:])
            for i in range(first, len(nums)):
                # 交换 first 和 i 位置上的元素
                nums[first], nums[i] = nums[i], nums[first]
                # 继续递归寻找排列
                backtrack(first + 1)
                # 回溯,恢复 first 和 i 位置上的元素
                nums[first], nums[i] = nums[i], nums[first]
        
        result = []
        backtrack()
        return result

# 示例
solution = Solution()
nums = [1, 2, 3]
print(solution.permute(nums))

十一、全排列Ⅱ

给定一个可包含重复数字的序列 nums ,按任意顺序 返回所有不重复的全排列。

class Solution:
    # 主函数,接收一个整数数组nums,并返回所有唯一的排列组合
    def permuteUnique(self, nums: List[int]) -> List[List[int]]:
        # 对输入数组进行排序,以便于后续去重
        nums.sort()
        # 初始化结果数组,用于存储所有排列
        result = []
        # 初始化路径数组,用于构建当前排列
        # 初始化已使用数组,用于标记数组中的元素是否已经被使用
        # 初始化为False,表示所有元素都未使用
        self.backtrack(nums, [], [False]*len(nums), result)
        # 返回结果数组
        return result

    # 回溯函数,用于生成所有排列
    def backtrack(self, nums, path, used, result):
        # 如果当前路径的长度等于原数组的长度,说明找到了一个完整的排列
        if len(path) == len(nums):
            # 将当前排列添加到结果数组中
            result.append(path[:])
        # 遍历数组中的每个元素
        for i in range(len(nums)):
            # 如果当前元素与前一个元素相同,并且前一个元素没有被使用过,或者当前元素已经被使用过,则跳过
            # 这是为了避免重复的排列
            if (i > 0 and nums[i] == nums[i - 1] and not used[i - 1]) or used[i]:
                continue
            # 将当前元素标记为已使用
            used[i] = True
            # 将当前元素添加到路径中
            path.append(nums[i])
            # 递归调用回溯函数,处理下一个元素
            self.backtrack(nums, path, used, result)
            # 回溯,移除路径中的最后一个元素,恢复状态
            path.pop()
            # 将当前元素标记为未使用
            used[i] = False

十二、重新安排行程(难)

给定一个机票的字符串二维数组 [from, to],子数组中的两个成员分别表示飞机出发和降落的机场地点,对该行程进行重新规划排序。所有这些机票都属于一个从 JFK(肯尼迪国际机场)出发的先生,所以该行程必须从 JFK 开始。

class Solution:
    def findItinerary(self, tickets: List[List[str]]) -> List[str]:
        # 初始化邻接表,用于存储图的邻接表表示
        self.adj = {}
        # 根据目的地对机票进行排序,确保在DFS中优先考虑字典序较小的目的地
        tickets.sort(key=lambda x: x[1])
        # 构建图的邻接表
        for u, v in tickets:
            if u in self.adj:
                self.adj[u].append(v)
            else:
                self.adj[u] = [v]
        # 初始化结果列表,用于存储最终的行程
        self.result = []
        # 从'JFK'开始深度优先搜索
        self.dfs('JFK')
        # 返回结果列表,由于路径是反向添加的,所以需要反转
        return self.result[::-1]

    def dfs(self, s):
        # 只要当前节点有可到达的目的地,就继续搜索
        while s in self.adj and len(self.adj[s]) > 0:
            v = self.adj[s][0]
            # 使用第一张机票,从当前目的地出发
            self.adj[s].pop(0)
            # 递归地探索下一个目的地
            self.dfs(v)
        # 如果当前节点没有可到达的目的地,将其添加到结果列表中
        self.result.append(s)

十三、N皇后

n 皇后问题 研究的是如何将 n 个皇后放置在 n×n 的棋盘上,并且使皇后彼此之间不能相互攻击。
给你一个整数 n ,返回所有不同的 n 皇后问题 的解决方案。
每一种解法包含一个不同的 n 皇后问题 的棋子放置方案,该方案中 ‘Q’ 和 ‘.’ 分别代表了皇后和空位。

class Solution:
    def solveNQueens(self, n: int) -> List[List[str]]:
        # 初始化结果列表
        self.result = []
        # 初始化棋盘,None代表空,0代表皇后
        self.board = [[None for _ in range(n)] for _ in range(n)]
        # 记录列是否有皇后
        self.cols = [False] * n
        # 记录对角线是否有皇后
        self.diags1 = [False] * (2 * n - 1)
        # 记录反对角线是否有皇后
        self.diags2 = [False] * (2 * n - 1)
        # 开始回溯搜索
        self.backtrack(0, n)
        return self.result

    def backtrack(self, row, n):
        # 如果已经放置了n个皇后,记录解法
        if row == n:
            # 将棋盘转换为结果所需的格式
            solution = [''.join(['Q' if cell is not None else '.' for cell in row]) for row in self.board]
            self.result.append(solution)
            return

        for col in range(n):
            if not self.cols[col] and not self.diags1[row - col + n - 1] and not self.diags2[row + col]:
                # 放置皇后
                self.board[row][col] = 'Q'
                # 标记列、对角线和反对角线
                self.cols[col] = True
                self.diags1[row - col + n - 1] = True
                self.diags2[row + col] = True
                # 递归放置下一行的皇后
                self.backtrack(row + 1, n)
                # 回溯,移除皇后
                self.board[row][col] = None
                self.cols[col] = False
                self.diags1[row - col + n - 1] = False
                self.diags2[row + col] = False

# 测试代码
n = 4
sol = Solution()
solutions = sol.solveNQueens(n)
for solution in solutions:
    for row in solution:
        print(row)
    print()

十四、解数独

编写一个程序,通过填充空格来解决数独问题。
一个数独的解法需遵循如下规则: 数字 1-9 在每一行只能出现一次。 数字 1-9 在每一列只能出现一次。 数字 1-9 在每一个以粗实线分隔的 3x3 宫内只能出现一次。 空白格用 ‘.’ 表示。

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

    def backtracking(self, board: List[List[str]]) -> bool:
        # 若有解,返回True;若无解,返回False
        for i in range(len(board)): # 遍历行
            for j in range(len(board[0])):  # 遍历列
                # 若空格内已有数字,跳过
                if board[i][j] != '.': continue
                for k in range(1, 10):
                    if self.is_valid(i, j, k, board):
                        board[i][j] = str(k)
                        if self.backtracking(board): return True
                        board[i][j] = '.'
                # 若数字1-9都不能成功填入空格,返回False无解
                return False
        return True # 有解

    def is_valid(self, row: int, col: int, val: int, board: List[List[str]]) -> bool:
        # 判断同一行是否冲突
        for i in range(9):
            if board[row][i] == str(val):
                return False
        # 判断同一列是否冲突
        for j in range(9):
            if board[j][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

你可能感兴趣的:(算法,leetcode,python)