【算法-面试】回溯法专题

1. 回溯法

回溯法主要体现在排列、组合、子集问题上。

Leetcode上的题目如下:

  • 22 括号⽣成(组合问题)
  • 37 解数独(排列问题)
  • 39 组合总和
  • 40 组合总和 II
  • 46 全排列
  • 47 全排列 II
  • 51 N 皇后(排列问题)
  • 77 组合
  • 78 子集
  • 90 子集 II
  • 494 ⽬标和(组合问题)
  • 698 划分为 k 个相等的⼦集(子集问题)

主要思想

  1. 子集问题可以利用数学归纳思想,假设已知一个规模较小的问题的结果,思考如何推导出原问题的结果。也可以用回溯算法,要用 start 参数排除已选择的数字。
  2. 组合问题利用的是回溯思想,结果可以表示成树结构,我们只要套用回溯算法模板即可,关键点在于要用一个 start 排除已经选择过的数字。
  3. 排列问题是回溯思想,也可以表示成树结构套用算法模板,不同之处在于使用 contains 方法排除已经选择的数字,这里主要是和组合问题作对比。
    1. contains方法用于递归算法backtrack的for循环内部的判断,用于排除已经选中的目标

2. 题目

【22. 括号⽣成】

# coding = "utf-8"
def generateParenthesis(n):
    '''
    数字 n 代表⽣成括号的对数,请你设计⼀个函数,⽤于能够⽣成所有可能的并且有效的括号组合。
    有效括号组合需满⾜:左括号必须以正确的顺序闭合。
    leetcode:22. 括号⽣成
    input:n=3
    output:["((()))","(()())","(())()","()(())","()()()"]
    思路:
        1. 利用回溯法,将所有的2n个括号进行组合
        2. 回溯判断条件:
            1) 当左右括号计数同时为n个的时候,说明括号数正好
            2) 当左右括号其中一个大于n时,说明括号不合法
            3) 当右括号数大于左括号时,说明括号不合法,这个判断条件很重要,可以筛选掉类似"())("这类不合法的组合
        3. 初始条件:左右括号数为0,path为""
        4. 入参:入参即左右括号数,记录路径的path
    '''
    if n == 0:
        return []
    res = []

    def backtrack(left, right, trace):
        if left == n and right == n:
            res.append(trace)

        if left > n or right > n or left < right:
            return
        backtrack(left + 1, right, trace + "(")
        backtrack(left, right + 1, trace + ")")

    backtrack(0, 0, "")
    print(res)
    return res

【37. 解数独】

def solveSudoku(board):
    '''
    编写⼀个程序,通过填充空格来解决数独问题,数独的解法需遵循如下规则:
    1、数字 1-9 在每⼀⾏只能出现⼀次。
    2、数字 1-9 在每⼀列只能出现⼀次。
    3、数字 1-9 在每⼀个以粗实线分隔的 3x3 宫内只能出现⼀次。
    数独部分空格内已填⼊了数字,空⽩格⽤ '.' 表示。
    leetcode: 37. 解数独
    input:board =   [["5","3",".",".","7",".",".",".","."],
                    ["6",".",".","1","9","5",".",".","."],
                    [".","9","8",".",".",".",".","6","."],
                    ["8",".",".",".","6",".",".",".","3"],
                    ["4",".",".","8",".","3",".",".","1"],
                    ["7",".",".",".","2",".",".",".","6"],
                    [".","6",".",".",".",".","2","8","."],
                    [".",".",".","4","1","9",".",".","5"],
                    [".",".",".",".","8",".",".","7","9"]]
    output:[["5","3","4","6","7","8","9","1","2"],
            ["6","7","2","1","9","5","3","4","8"],
            ["1","9","8","3","4","2","5","6","7"],
            ["8","5","9","7","6","1","4","2","3"],
            ["4","2","6","8","5","3","7","9","1"],
            ["7","1","3","9","2","4","8","5","6"],
            ["9","6","1","5","3","7","2","8","4"],
            ["2","8","7","4","1","9","6","3","5"],
            ["3","4","5","2","8","6","1","7","9"]]
    思路:
        1. 回溯算法
        2. 每个格子遍历1-9,遇到不合法的数字则跳过:
            1). 横向数字有重复的
            2). 竖向数字有重复的
            3). 九个格子内的数字有重复的
        3. 如果找到一个合适的数字就找下一个空格
    '''
    m, n = 9, 9

    def backtrace(board, i, j):
        if j == m:
            # 到最后一列,触发下一行
            return backtrace(board, i + 1, 0)

        if i == m:
            # 到最后一行,触发base case
            return True

        if board[i][j] != '.':
            # 由于先判断j在前,所以不用担心下一个j即j+1会越界
            return backtrace(board, i, j + 1)

        for ch in range(1, 10):
            if not is_valid(board, i, j, str(ch)):
                continue
            board[i][j] = str(ch)
            if backtrace(board, i, j + 1):
                return True
            board[i][j] = '.'

        return False

    def is_valid(board, row, col, n):
        for i in range(9):
            if board[row][i] == n:
                return False
            if board[i][col] == n:
                return False
            if board[(row / 3) * 3 + i / 3][(col / 3) * 3 + i % 3] == n:
                return False
        return True

    backtrace(board, 0, 0)
    return board

【78. ⼦集】

def subsets(nums):
    '''
    输入一个不包含重复数字的数组,要求算法输出这些数字的所有子集。
    解集不能包含重复的⼦集。你可以按任意顺序返回解集
    leetcode: 78. ⼦集
    input:nums = [1,2,3]
    output:[[],[1],[2],[1,2],[3],[1,3],[2,3],[1,2,3]]
    思路:
        1. [1,2,3]的子集是
        2.
        3.
    '''
    import copy
    path, res = [], []

    def backtrack(start, trace):
        # 由于python是取的标签,需要copy下
        t = copy.copy(trace)
        res.append(t)
        for i in range(start, len(nums)):
            trace.append(nums[i])
            backtrack(i + 1, trace)
            trace.pop()

    backtrack(0, path)
    print(res)
    return res

【77. 组合】

def combine(n, k):
    '''
    输入两个数字 n, k,算法输出 [1..n] 中 k 个数字的所有组合。
    leetcode: 77. 组合
    input: n = 4, k = 2
    output:
        [
            [2,4],
            [3,4],
            [2,3],
            [1,2],
            [1,3],
            [1,4],
        ]
    思路:
        1. 回溯
        2. 注意是从1开始的,因此range的时候尾部+1,即range(start, n + 1)
        3. 更新start部分
    '''
    import copy
    res, path = [], []

    def backtrace(start, path):
        if len(path) == k:
            p = copy.copy(path)
            res.append(p)
            return

        for i in range(start, n + 1):
            path.append(i)
            backtrace(i + 1, path)
            path.pop()

    backtrace(1, path)
    print(res)
    return res

【46. 全排列】

def permute(nums):
    '''
    输入一个不包含重复数字的数组 nums,返回这些数字的全部排列。
    leetcode:46. 全排列
    input:[1,2,3]
    output:
        [
         [1,2,3],
         [1,3,2],
         [2,1,3],
         [2,3,1],
         [3,1,2],
         [3,2,1]
        ]
    思路:
        1. 回溯法
        2. 需要判断提出无效排列,比如[1,1,1], [1,2,1]
        3.
    '''
    import copy
    res, path, n = [], [], len(nums)

    def backtrace(path):
        if len(path) == n:
            p = copy.copy(path)
            res.append(p)
            return

        for i in range(n):
            # 排除的条件
            if nums[i] in path:
                continue

            path.append(nums[i])
            backtrace(path)
            path.pop()

    backtrace(path)
    print(res)
    return res

【51. N 皇后】


def solveNQueens(n):
    '''
    给你一个 N×N 的棋盘,让你放置 N 个皇后,使得它们不能互相攻击。
    攻击:皇后可以攻击同一行、同一列、左上左下右上右下四个方向的任意单位。
    leetcode: 51. N 皇后
    input: n
    output:
    思路:
        1. 加入判断条件的回溯法,本质上是一个排列问题
        2. is_valid函数用来判断Q的位置是否合适,判断同一列、左上、右上是否有Q
        3. 同一行不需要判断是因为有撤销选择,撤销后,该行不存在Q
    '''
    import copy
    board = [['.'] * n for _ in range(n)]
    res = []

    def backtrace(board, row):
        if row == n:
            b = copy.deepcopy(board)
            res.append(b)
            return
        for col in range(n):
            # 排除的条件
            if not is_valid(board, row, col):
                continue

            board[row][col] = 'Q'
            backtrace(board, row + 1)
            board[row][col] = '.'

    def is_valid(board, row, col):
        # 同一列
        for i in range(n):
            if board[i][col] == 'Q':
                return False
        # 右上方
        i, j = row - 1, col + 1
        while i >= 0 and j < n:  # 注意循环条件是带0的
            if board[i][j] == 'Q':
                return False
            i -= 1
            j += 1

        # 左上方
        i, j = row - 1, col - 1
        while i >= 0 and j >= 0:  # 注意循环条件是带0的
            if board[i][j] == 'Q':
                return False
            i -= 1
            j -= 1
        return True

    backtrace(board, 0)
    print(res)
    print(len(res))
    return board

【494. ⽬标和】

def findTargetSumWays(nums, target):
    '''
    给你⼀个整数数组 nums 和⼀个整数 target,向数组中的每个整数前添加 '+' 或 '-',然后串联起所有整数,可以构造⼀个表达式。
    例如,nums = [2, 1],可以在 2 之前添加 '+',在 1 之前添加 '-',然后串联起来得到表达式 "+2-1"。
    返回可以通过上述⽅法构造的、运算结果等于 target 的不同表达式的数⽬。
    leetcode: 494. ⽬标和
    input:nums = [1,1,1,1,1], target = 3
    output:5
        ⼀共有 5 种⽅法让最终⽬标和为 3。
        -1 + 1 + 1 + 1 + 1 = 3
        +1 - 1 + 1 + 1 + 1 = 3
        +1 + 1 - 1 + 1 + 1 = 3
        +1 + 1 + 1 - 1 + 1 = 3
        +1 + 1 + 1 + 1 - 1 = 3
    思路:
        1. 回溯法
            1) 本质上是组合问题,给定一个start值
            2) 一次遍历需要做两次选择,一次是选择"+",另一次是选择"-"
            3) base case是start达到数组的长度和s(路径或者本题的运算值)值与target相等
        2. 动态规划
            1). sum(A)-sum(B)=target => sum(A)=target+sum(B)
            2). 2sum(A) = target+sum(B)+sum(A) => sum(A)=(target+sum(nums))/2
            3). 转换为求subsets(nums, sum(A)), 即nums中存在几个子集,使得其和为(target+sum(nums))/2
            4). 转换为有一个背包的容量为sum(A), 有N个物品,每个物品重量为nums[i-1],每个物品只放一次,有多少种不同的方法存放?
            5). 状态:背包的容量和可选择的物品
                选择:装背包和不装背包
                dp数组:dp[i][j]代表前i个物品中,背包容量为j,有dp[i][j]种方法存放
                base case:dp[0][.]代表没有物品,自然存放方法为0;dp[.][0]代表载重为0,那么什么都不放也是一种选择,因此为1
                状态
    '''

    class Track:
        def __init__(self, nums, target):
            self.res = 0
            self.nums_size = len(nums)
            self.target = target

        def backtrace(self, i, s):
            if i == self.nums_size:
                if s == self.target:
                    self.res += 1
                return
            # 选择"+"
            s += nums[i]
            self.backtrace(i + 1, s)
            s -= nums[i]

            # 选择"-"
            s -= nums[i]
            self.backtrace(i + 1, s)
            s += nums[i]

    class DP:
        def __init__(self, nums, target):
            self.nums = nums
            self.target = target
            self._sum = 0
            self._target = 0

        def find_ways(self):
            for num in self.nums:
                self._sum += num

            # 剔除目标值大于全是和值的情况 以及 和值与目标值加起来为奇数的情况
            if self._sum < self.target or (self._sum + target) % 2 == 1:
                return 0

            # 根据公式得到新的target
            self._target = (self._sum + self.target) // 2

            return self.subsets()

        def subsets(self):
            n = len(self.nums)
            dp = [[0] * (self._target + 1) for _ in range(n + 1)]
            for i in range(n + 1):
                dp[i][0] = 1

            for i in range(1, n + 1):
                for j in range(self._target + 1):
                    # 背包容量足
                    if j >= self.nums[i - 1]:
                        # dp[i - 1][j]表示不把物品放入背包,结果取决于上一个j容量时的状态
                        # dp[i - 1][j - self.nums[i - 1]]表示把物品放入背包,结果取决于上一个容量为j - self.nums[i - 1]容量时的状态
                        dp[i][j] = dp[i - 1][j] + dp[i - 1][j - self.nums[i - 1]]
                    # 背包容量不足
                    else:
                        dp[i][j] = dp[i - 1][j]
            print(dp)
            return dp[n][self._target]

    t = Track(nums, target)
    t.backtrace(0, 0)
    print(t.res)

    dp = DP(nums, target)
    res = dp.find_ways()
    # res = dp.subsets()
    print(res)
    return res

【698. 划分为 k 个相等的⼦集】


def canPartitionKSubsets(nums, k):
    '''
    给定⼀个整数数组 nums 和⼀个正整数 k,找出是否有可能把这个数组分成 k 个⾮空⼦集,其总和都相等。
    leetcode: 698. 划分为 k 个相等的⼦集
    input:nums = [4, 3, 2, 3, 5, 2, 1], k = 4
    output:True
    思路:
        1.
        2.
        3.
    '''

    class Trace:
        def __init__(self, nums, target, k):
            self.bucket = [0] * k
            self.k = k
            self.nums = nums
            self.target = target

        def backtrace(self, index):
            if len(self.nums) == index:
                for i in range(self.k):
                    if self.bucket[i] != self.target:
                        return False
                return True

            for i in range(self.k):
                if self.bucket[i] > self.target:
                    return False

                self.bucket[i] += self.nums[index]
                if self.backtrace(index + 1):
                    return True
                self.bucket[i] -= self.nums[index]

            return False

    if len(nums) < k:
        return False
    s = 0
    for num in nums:
        s += num
    if s % k != 0:
        return False
    target = s // k

    t = Trace(nums, target, k)
    res = t.backtrace(0)
    print(res)
    return res

【测试例】


if __name__ == "__main__":
    # generateParenthesis(3)
    # subsets([1, 2, 3])
    # combine(4, 2)
    # permute([1, 2, 3])
    # solveNQueens(8)
    # findTargetSumWays([1, 1, 1, 1, 1], 3)
    canPartitionKSubsets([4, 3, 2, 3, 5, 2, 1], 4)
    canPartitionKSubsets([4, 3, 2, 3, 5, 2, 5], 4)

你可能感兴趣的:(算法,算法,面试,leetcode,回溯法)