刷了一天回溯算法 2022年6月16日

博主花费一整天时间专注于回溯算法的实践,通过《代码随想录》第九章的14道题目,涵盖数独、N皇后、全排列、子集等问题。在解题过程中,博主深入理解了回溯算法的核心——撤销操作,并通过实例分析了层间和树枝下去重的策略。此外,博主还反思了过去使用库函数的局限性,强调了手动实现算法的重要性。
摘要由CSDN通过智能技术生成

刷了一天回溯算法


由于发现自己递归的回溯部分还是不太清楚,今天就特意抽出一整天的时间来练习回溯算法。

练习方式:把《代码随想录》上面第九章回溯算法这一章的所有题目,总共有十四道题目都做了一遍。

32 分钟前#37 解数独困难2 次
4 小时前#51 N 皇后困难3 次
5 小时前#47 全排列 II中等1 次
5 小时前#46 全排列中等1 次
5 小时前#491 递增子序列中等3 次
7 小时前#90 子集 II中等3 次
8 小时前#78 子集中等1 次
8 小时前#93 复原 IP 地址中等1 次
8 小时前#131 分割回文串中等1 次
10 小时前#40 组合总和 II中等4 次
11 小时前#39 组合总和中等1 次
12 小时前#17 电话号码的字母组合中等2 次
12 小时前#216 组合总和 III中等1 次
12 小时前#77 组合中等2 次

回顾复盘一下

77. 组合
class Solution:
    def combine(self, n: int, k: int) -> List[List[int]]:
        res = []

        def dfs(left: int, lst: list, remain: int):
            if remain == 0:
                res.append(lst[:])
                return

            if left > n:
                return

            for i in range(left, n + 1):
                lst.append(i)
                dfs(i + 1, lst, remain - 1)
                lst.pop()  # 精髓,撤销!!

        dfs(1, [], k)
        return res

这个题以前是直接用python的combination做的,但是总感觉用库心里不踏实,没有学到精髓。也不利于递归思维的提升。所以就看书学了学回溯算法。

感觉回溯算法的核心精髓,就是:每次递归操作完了之后都会撤销当前操作。,就比如这段代码里面的 lst.pop(),忽然感觉以前的自己实在是太屑了。不知道在遍历决策树的时候怎么倒退,干着急也没办法,原来就是这么简单,感觉很妙。

216. 组合总和 III
from itertools import combinations


class Solution:
    def combinationSum3(self, k: int, n: int) -> List[List[int]]:
        return [list(item) for item in combinations(range(1, 10), k) if sum(item) == n]

这个题偷了个懒。感觉和第一题有点相似了。就调了个库

17. 电话号码的字母组合
class Solution:
    def letterCombinations(self, digits: str) -> List[str]:
        res = []
        if digits == "":
            return res
        digs = [
            "abc",
            "def",
            "ghi",
            "jkl",
            "mno",
            "pqrs",
            "tuv",
            "wxyz",
        ]

        def dfs(digIndex: int, path: str):
            # digIndex:表示当前选到了第几个数字
            # path,写入答案的暂存小数组
            if digIndex >= len(digits):
                res.append(path)
                return

            for char in digs[int(digits[digIndex]) - 2]:  # 这里稍微绕了一下的就是把数字转化成索引,-2
                # 树形宽度遍历每一种字符可能
                path += char
                dfs(digIndex + 1, path)
                path = path[:-1]  # 去掉最后一个字符,用的切片
            ...

        dfs(0, "")
        return res

这个题踩了个坑WA了一下,传入空字符串的时候居然回出错,于是就加了个特殊判断。

这个题目看了之后梦回小学时代,那个时候我还在用诺基亚。题目就是用一个九键手机,给出一串数字,给出所有的能匹配出来的字母,这个题好像和实际的现实还不太一样,因为现实情况实际上是想打出一个b,要按两下2才行。这个只是直接求组合的所有可能了。就是按一下2,abc都有可能了。

想起来以前周赛好像做过一个题,是给出了很长很长一串数字,问可能打出多少种字母,那个题的配图和这个题的配图居然是一样的我记得。但是这个题好像感觉不如那个题难,那个题当时是用动态规划来解决的。当时居然还做了快一个小时,当时那个题的dp递推式子居然是刚好碰出来的。

39. 组合总和
class Solution:
    def combinationSum(self, candidates: List[int], target: int) -> List[List[int]]:
        res = []

        def dfs(left: int, sumVal: int, path: List[int]):
            # left 当前要选择的数组的下标
            # sumval 当前已经选择了的和
            # path 当前选择了的数字 list
            if sumVal > target:
                # 如果已经选超了,就不要再选下去了
                return
            if sumVal == target:
                # 刚好!记录答案
                res.append(path[:])
                return
            # sum 还不够
            for i in range(left, len(candidates)):
                path.append(candidates[i])  # 添加一个当前这个数字
                dfs(i, sumVal + candidates[i], path)  # 第一个参数i不用加1.因为可以无限选择
                path.pop()  # 撤销
            ...

        dfs(0, 0, [])
        return res

这个题不一样的点在于里面的元素可以无限次被选择。

做到这道题的时候就感觉自己在22年五月份还是四月份的时候,也就是刷剑指offer1的时候,自己当时的很多这种组合问题好像也遇到过,当时就是全用的combination全偷懒做的,一遇到变种,发现不能偷懒,就不会了,当时就直接抄别人的答案提交了。但是打开这道题的时候找不到自己的提交记录了,可能是剑指offer上面的题有重复的一样的题,题源地址可能不是一个。

40. 组合总和 II
class Solution:
    # 2022年6月16日
    def combinationSum2(self, candidates: List[int], target: int) -> List[List[int]]:
        candidates.sort()
        res = []

        def dfs(left: int, sumVal: int, path: list):
            if sumVal > target:
                # 已经加超过了
                # print("add over")
                return
            if sumVal == target:
                # 刚好
                res.append(path[:])
            if sumVal < target:
                # 继续递归
                s = set()
                for i in range(left + 1, len(candidates)):

                    addNum = candidates[i]
                    if addNum in s:
                        continue
                    s.add(addNum)

                    path.append(addNum)
                    dfs(i, sumVal + addNum, path)
                    path.pop()

        dfs(-1, 0, [])  # 从-1开始,因为for循环遍历会+1,就加成0了
        return res

这个题总是超过时间限制

这个题的特点是:结果的每一个组合里可以有重复的数字,但是!!所有的结果里不能有重复的组合。。。

就是有一个组合 [1, 1, 2] 是合法的,但是不能有两个 [1, 1, 2]。

这个我和书上的最开始的做法很一致,一上来先拍了序,但是排序之后怎么去重是个大问题,最开始我是直接遍历所有组合,单纯用一个集合放在最外层来去重,但是发现这样总是超时。

其实应该是在遍历决策树的时候,直接用同层重复数字来剪枝去重,不仅达到了去重的效果,而且还达到大大大大节省时间的效果。

这里的同层去重就是在内层放一个set,就是刚好在for的外一层的地方。然后先检测是否在集合里就行了。

131. 分割回文串
def test(a: str):
    return a == a[::-1]


class Solution:
    def partition(self, s: str) -> List[List[str]]:
        res = []

        def dfs(remain: str, path: list):
            # print(path)
            if not remain:
                return
            if test(remain):
                res.append(path[:] + [remain])
            for i in range(len(remain)):
                # 在当前位置i一切割
                left = remain[:i + 1]
                if not test(left):
                    continue
                right = remain[i + 1:]
                # print("\t", path, left, right)
                path.append(left)
                dfs(right, path)
                path.pop()

        dfs(s, [])
        return res

判断一个字符串是不是回文的,直接用python语法糖了。本质上都是用的线性的时间复杂度。

这个题问的意思是,在一个字符串的各个缝隙位置上放若干个隔板,隔板分割出来的每一个字符串都是回文的。

有多少种放隔板的方式。

我规定的下标是在对应i下标元素之后,与i+1之间放隔板。

想了想执行一次之后就通过了

93. 复原 IP 地址
class Solution:
    def restoreIpAddresses(self, s: str) -> List[str]:
        res = []

        def dfs(remain: str, path: list):
            if len(path) == 4 and not remain:
                # 没有剩余的了,并且path刚好也等于四,记录答案!
                res.append(".".join(path))
            if len(path) == 4 and remain:
                # 已经满了但是还没切完呢
                return
            for i in range(min(3, len(remain))):
                if remain[0] == "0":  # 因为不能有前导零,所以加个图书判断
                    path.append("0")
                    dfs(remain[1:], path)
                    path.pop()
                    break
                # 遍历每一个剩余可切割索引
                leftStr = remain[:i + 1]
                rightStr = remain[i + 1:]
                if int(leftStr) < 256:
                    path.append(leftStr)
                    dfs(rightStr, path)
                    path.pop()
                ...
        dfs(s, [])
        return res

这个题是把所有可能的IP地址返回出来,就是不停的加点,和上一个题有点像。

这个题里面有一些特殊情况的处理。

78. 子集
class Solution:
    # 2022年6月16日
    def subsets(self, nums: List[int]) -> List[List[int]]:
        res = []

        def dfs(left: int, path: list):
            if left >= len(nums):
                res.append(path[:])
                return

            # 我要当前这个
            path.append(nums[left])
            dfs(left + 1, path)
            path.pop()
            # 我不要当前这个
            dfs(left + 1, path)
            ...

        dfs(0, [])
        return res

这题就是获取一个集合的幂集,以前做这个题的时候我发现自己是用combination偷懒了。这次好好做一下。

这个做的时候懵了一下,看了一下书上画的图才意识到,原来是在决策树上每走一步都记录添加。

这个不是for循环横向扩展决策树宽度了。而是一个二叉树的形状,要不要当前位置了。

90. 子集 II
class Solution:
    def subsetsWithDup(self, nums: List[int]) -> List[List[int]]:
        res = []
        nums.sort()

        def dfs(left: int, path: list):
            if left >= len(nums):
                return
            s = set()
            for i in range(left + 1, len(nums)):
                # 要
                n = nums[i]
                if n in s:
                    continue
                else:
                    s.add(n)
                path.append(n)
                res.append(path[:])
                dfs(i, path)
                path.pop()
                ...
            ...

        dfs(-1, [])  # 又是从-1开始

        return res + [[]]

这个题是变成了集合里有重复元素了,然后返回幂集。

答案里面可以有 [1, 1, 2] 这样的子集,但是不能有重复的子集,就不能有两个[1, 1, 2]。这又成了去重问题了

这个去重问题也是,先排序,然后再对决策树同层去重。

491. 递增子序列
class Solution:
    def findSubsequences(self, nums: List[int]) -> List[List[int]]:
        res = []

        def dfs(left: int, path: list):
            s = set()
            if len(path) >= 2:
                res.append(path[:])
            for i in range(left + 1, len(nums)):

                n = nums[i]
                if n not in s:
                    s.add(n)
                else:
                    continue
                if path and path[-1] > n:
                    continue
                path.append(n)
                dfs(i, path)

                path.pop()
            ...

        dfs(-1, [])  # 在for循环里才开始选择,会+1,所以写了-1
        # print(len(res))
        return res

这个题目就如题的名字一样简洁,获得一个数组的所有的递增的子序列。因为递增性质,所以所有子序列的长度必须是大于等于2的。

这个题也是,发现得去重,去重就挺麻烦的,发现是同层去重。

然后记录答案的时候,是只要path数组的长度一旦大于2,就会记录。

最开始看到这个题我想的是,这不直接来个二重循环就可以了吗。后来逝了一下才发现,不行。因为二重循环里面那一层没法做到间断跳跃的选择。这个题WA了两次。边界条件还得处理。

46. 全排列

这个题好像也可以直接调用itertools里面的全排列直接写,但是就失去练习的意义了。所以特意要学习一下回溯怎么实现全排列。

class Solution:
    def permute(self, nums: List[int]) -> List[List[int]]:
        res = []

        def dfs(path: list):
            if len(path) == len(nums):
                res.append([nums[i] for i in path])

            for i in range(len(nums)):
                if i in path:  # 排除自己选自己
                    continue
                path.append(i)
                dfs(path)
                path.pop()

        dfs([])
        return res

这个决策树就是一个完整的树了,像之前的题一样,左边深右边空。

记录答案的时候就刚好是path数组写满了的时候就记录。

for循环里面写的也是,直接遍历满,要排除自己。

以前大二的时候刚做行列式的程序就用到了全排列算法,当时自己不知道python内置库实现了,就自己瞎搞,搞的代码又臭又长,空间复杂度On!,直接生成了一个多叉树,也根本不会回溯,就无奈又把多叉树改成了三叉链表。增加了指向父亲节点的指针。特别特别麻烦,居然还搞了一下午。现在看看以前的自己真的好憨憨。但也没办法。只是感觉过去的自己学习的进度和效率低了。

现在看看这个全排列算法,感觉不怎么难。看着这么几行代码,居然还感觉有点简单。

也许我应该有这样一个警醒:当我特别想做出一个东西,但是我的算法卡脖子了,然后我硬是去用自己的方法做。虽然可能很有成就感,但是这可能说明我的算法技能和知识有着严重漏洞。

47. 全排列 II
class Solution:
    def permuteUnique(self, nums: List[int]) -> List[List[int]]:
        res = []

        def dfs(choice: list, path: list):
            if len(nums) == len(path):
                res.append(path[:])

            s = set()
            for i in range(len(nums)):
                if i in choice or nums[i] in s:
                    continue
                s.add(nums[i])
                # 选择这个元素
                choice.append(i)
                path.append(nums[i])
                dfs(choice, path)
                path.pop()
                choice.pop()
            ...

        dfs([], [])
        return res

这个题就是在上一个题的基础上,要给出全排列的序列里面有了重复的元素了。

这个去重又有点麻烦了。搞了挺长时间。

递归参数搞了两个数组,一个是记录选择下标的数组,一个是记录选择的元素的数组,

选择下标的数组不能重复选择,选择元素的数组在树的同层也是要去重的。

51. N 皇后
class Solution:
    def solveNQueens(self, n: int) -> List[List[str]]:
        res = []

        def dfs(lines: list, y: int):
            if len(lines) == n:
                # write
                # print(lines, y)
                ans = []
                for x in lines:
                    line = ["."] * n
                    line[x] = "Q"
                    ans.append("".join(line))
                res.append(ans)
                return
            if y >= n:
                return
            for x in range(n):
                # 判断竖着是否重复
                if x in lines:
                    continue
                # 判断斜着是否重复
                lst = []
                for i, q in enumerate(lines):
                    lst.extend((q - (y - i), q + (y - i)))
                # print("\t", lines, lst)
                if x in lst:
                    continue
                # 这个位置可以放
                lines.append(x)
                dfs(lines, y + 1)
                lines.pop()
            ...

        dfs([], 0)
        return res

这个N皇后问题,以前做过,一看提交记录发现,竟然恰好是上一年的这个月

提交结果执行用时内存消耗语言提交时间备注
通过72 ms15.3 MBPython32022/06/16 17:10
添加备注
通过996 ms15.8 MBPython32021/06/13 18:03
DFS,减少了搜索范围
超出时间限制N/AN/APython32021/06/13 16:26

上一年的今天左右相差了三天,回忆起以前这个题当时搞了好像将近两个多小时,这次好像半小时就搞过了。看来掌握回溯就是好啊。

并且当时的运行时间也好多。

现在写的时候,就判断斜着相撞的时候,绕了一下。

字符串处理也感觉得心应手了。

37. 解数独
def valid(board: List[List[str]]) -> bool:
    # line
    for line in board:
        for char in line:
            if char == ".":
                continue
            if line.count(char) > 1:
                return False
    # col
    for x in range(9):
        s = set()
        for y in range(9):
            char = board[y][x]
            if char == ".":
                continue
            if char in s:
                return False
            s.add(char)
    # box
    for y in range(3):
        locY = y * 3

        for x in range(3):
            locX = x * 3

            s = set()
            for dy in range(3):
                realY = locY + dy
                for dx in range(3):
                    realX = locX + dx
                    char = board[realY][realX]
                    if char == ".":
                        continue
                    if char in s:
                        return False
                    s.add(char)
    return True


def full(board: List[List[str]]):
    for line in board:
        for char in line:
            if char == ".":
                return False
    return True


class Solution:
    def solveSudoku(self, board: List[List[str]]) -> None:
        """
        Do not return anything, modify board in-place instead.
        """
        # 记录下来所有可以更改的坐标点
        modifyList = []  # len = 51
        for y, line in enumerate(board):
            modifyList.extend((x, y) for x, char in enumerate(line) if char == ".")
        success = False

        # 在所有可以更改的坐标点上一步一步往前实验
        # 每试一次就全局检测一次,如果检测失败就往回反

        def dfs(modifyIndex: int):
            nonlocal success
            if success:
                return
            if not valid(board):
                return
            if modifyIndex >= len(modifyList):
                success = True
                return

            mx, my = modifyList[modifyIndex]
            for i in range(1, 10):
                board[my][mx] = str(i)
                dfs(modifyIndex + 1)
                if not success:
                    board[my][mx] = "."
                else:
                    break

        dfs(0)

这个解数独,以前大一的时候2020年上半年,也就是刚开始学python的那半年搞出来过,当时还没学算法,还没学数据结构,就是想搞出来解决数独的问题。当时也还做了框图。只是没有用递归。做了整整大半天。

后来知道了leetcode,发现了有解数独这个题,只是没想到格式居然是字符串格式,太麻烦了,就一直没做。

今天终于把这个题给解决了。

判断数独整体是否有效的时候还是出了点小bug。

整个回溯递归的时候逻辑也遇到了点问题,发现自己明明写好了,但是最后好像又全给擦掉了,就用了一个success变量。但是又发现最后填写的数全都变成9了,原来是自己在递归for循环里忘了break了。还是有好多小细节问题。

总结

今天用了整整一天的时间又弥补了一下以前算法一直欠缺的地方,感觉很有收获。

这些去重的问题,层间去重和树枝下去重,书里讲的真的挺清楚的。这个书里给出的做题顺序也真的不错。

可能以后有时间还是要回头继续做这些题,时间长了可能又会变得生疏。也许一年之后,我又会变成一个更强的自己。回头看到自己现在写的代码可能还是不够好。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值