[leetcode·回溯算法]回溯算法解题套路框架

本文参考labuladong算法笔记[回溯算法解题套路框架 | labuladong 的算法笔记]

本文解决几个问题:

回溯算法是什么?解决回溯算法相关的问题有什么技巧?如何学习回溯算法?回溯算法代码是否有规律可循?

其实回溯算法和我们常说的 DFS 算法基本可以认为是同一种算法,它们的细微差异我在 关于 DFS 和回溯算法的若干问题 中有详细解释,本文聚焦回溯算法,不展开。

抽象地说,解决一个回溯问题,实际上就是遍历一棵决策树的过程,树的每个叶子节点存放着一个合法答案。你把整棵树遍历一遍,把叶子节点上的答案都收集起来,就能得到所有的合法答案

站在回溯树的一个节点上,你只需要思考 3 个问题:

1、路径:也就是已经做出的选择。

2、选择列表:也就是你当前可以做的选择。

3、结束条件:也就是到达决策树底层,无法再做选择的条件。

如果你不理解这三个词语的解释,没关系,我们后面会用「全排列」这个经典的回溯算法问题来帮你理解这些词语是什么意思,现在你先留着印象。

回溯算法框架:

python 复制代码
result = []
def backtrack(路径, 选择列表):
    if 满足结束条件:
        result.add(路径)
        return
    
    for 选择 in 选择列表:
        做选择
        backtrack(路径, 选择列表)
        撤销选择

其核心就是 for 循环里面的递归,在递归调用之前「做选择」,在递归调用之后「撤销选择」,特别简单

全排列问题解析

力扣第 46 题「全排列」就是给你输入一个数组 nums,让你返回这些数字的全排列。

46. 全排列

给定一个不含重复数字的数组 nums ,返回其 所有可能的全排列 。你可以 按任意顺序 返回答案。

示例 1:

复制代码
输入:nums = [1,2,3]
输出:[[1,2,3],[1,3,2],[2,1,3],[2,3,1],[3,1,2],[3,2,1]]

示例 2:

复制代码
输入:nums = [0,1]
输出:[[0,1],[1,0]]

示例 3:

复制代码
输入:nums = [1]
输出:[[1]]

提示:

  • 1 <= nums.length <= 6
  • -10 <= nums[i] <= 10
  • nums 中的所有整数 互不相同

我们这次讨论的全排列问题不包含重复的数字,包含重复数字的扩展场景我在后文 回溯算法秒杀排列组合子集的九种题型 中讲解

另外,有些读者之前看过的全排列算法代码可能是那种 swap 交换元素的写法,和我在本文介绍的代码不同。这是回溯算法两种穷举思路,我会在后文 球盒模型:回溯算法穷举的两种视角 讲明白。现在还不适合直接跟你讲那个解法,你照着我的思路学习即可。

我们在高中的时候就做过排列组合的数学题,我们也知道 n 个不重复的数,全排列共有 n! 个。那么我们当时是怎么穷举全排列的呢?

比方说给三个数 [1,2,3],你肯定不会无规律地乱穷举,一般是这样:

先固定第一位为 1,然后第二位可以是 2,那么第三位只能是 3;然后可以把第二位变成 3,第三位就只能是 2 了;然后就只能变化第一位,变成 2,然后再穷举后两位......

其实这就是回溯算法,我们高中无师自通就会用,或者有的同学直接画出如下这棵回溯树:

只要从根遍历这棵树,记录路径上的数字,其实就是所有的全排列。我们不妨把这棵树称为回溯算法的「决策树」

为啥说这是决策树呢,因为你在每个节点上其实都在做决策。比如说你站在下图的红色节点上:

你现在就在做决策,可以选择 1 那条树枝,也可以选择 3 那条树枝。为啥只能在 1 和 3 之中选择呢?因为 2 这个树枝在你身后,这个选择你之前做过了,而全排列是不允许重复使用数字的。

现在可以解答开头的几个名词:[2] 就是「路径」,记录你已经做过的选择;[1,3] 就是「选择列表」,表示你当前可以做出的选择;「结束条件」就是遍历到树的底层叶子节点,这里也就是选择列表为空的时候

如果明白了这几个名词,可以把「路径」和「选择」列表作为决策树上每个节点的属性,比如下图列出了几个蓝色节点的属性:

我们定义的 backtrack 函数其实就像一个指针,在这棵树上游走,同时要正确维护每个节点的属性,每当走到树的底层叶子节点,其「路径」就是一个全排列

再进一步,如何遍历一棵树?这个应该不难吧。回忆一下之前 学习数据结构的框架思维 写过,各种搜索问题其实都是树的遍历问题,而多叉树的遍历框架就是这样:

python 复制代码
def traverse(root: TreeNode):
    for child in root.children:
        # 前序位置需要的操作
        traverse(child)
        # 后序位置需要的操作

而所谓的前序遍历和后序遍历,他们只是两个很有用的时间点,我给你画张图你就明白了:

前序遍历的代码在进入某一个节点之前的那个时间点执行,后序遍历代码在离开某个节点之后的那个时间点执行

回想我们刚才说的,「路径」和「选择」是每个节点的属性,函数在树上游走要正确处理节点的属性,那么就要在这两个特殊时间点搞点动作:

现在,你是否理解了回溯算法的这段核心框架?

python 复制代码
for 选择 in 选择列表:
    # 做选择
    将该选择从选择列表移除
    路径.add(选择)
    backtrack(路径, 选择列表)
    # 撤销选择
    路径.remove(选择)
    将该选择再加入选择列表

我们只要在递归之前做出选择,在递归之后撤销刚才的选择,就能正确得到每个节点的选择列表和路径。

Python解法代码:

python 复制代码
from typing import List

class Solution:
    def __init__(self):
        self.res = []

    # 主函数,输入一组不重复的数字,返回它们的全排列
    def permute(self, nums: List[int]) -> List[List[int]]:
        # 记录「路径」
        track = []
        # 「路径」中的元素会被标记为 true,避免重复使用
        used = [False] * len(nums)
        
        self.backtrack(nums, track, used)
        return self.res

    # 路径:记录在 track 中
    # 选择列表:nums 中不存在于 track 的那些元素(used[i] 为 false)
    # 结束条件:nums 中的元素全都在 track 中出现
    def backtrack(self, nums: List[int], track: List[int], used: List[bool]):
        # 触发结束条件
        if len(track) == len(nums):
            self.res.append(track.copy())
            return
        
        for i in range(len(nums)):
            # 排除不合法的选择
            if used[i]: 
                # nums[i] 已经在 track 中,跳过
                continue
            # 做选择
            track.append(nums[i])
            used[i] = True
            # 进入下一层决策树
            self.backtrack(nums, track, used)
            # 取消选择
            track.pop()
            used[i] = False

我们这里稍微做了些变通,没有显式记录「选择列表」,而是通过 used 数组排除已经存在 track 中的元素,从而推导出当前的选择列表:

至此,我们就通过全排列问题详解了回溯算法的底层原理。当然,这个算法解决全排列不是最高效的,你可能看到有的解法连 used 数组都不使用,通过交换元素达到目的。但是那种解法稍微难理解一些,我会在 球盒模型:回溯算法两种穷举视角 中介绍。

但是必须说明的是,不管怎么优化,都符合回溯框架,而且时间复杂度都不可能低于 O(N!),因为穷举整棵决策树是无法避免的,你最后肯定要穷举出 N! 种全排列结果。

这也是回溯算法的一个特点,不像动态规划存在重叠子问题可以优化,回溯算法就是纯暴力穷举,复杂度一般都很高

最后总结

回溯算法就是个多叉树的遍历问题,关键就是在前序遍历后序遍历的位置做一些操作。

backtrack 函数时,需要维护走过的「路径」和当前可以做的「选择列表」,当触发「结束条件」时,将「路径」记入结果集

其实想想看,回溯算法和动态规划是不是有点像呢?我们在动态规划系列文章中多次强调,动态规划的三个需要明确的点就是「状态」「选择」和「base case」,是不是就对应着走过的「路径」,当前的「选择列表」和「结束条件」?

动态规划和回溯算法底层都把问题抽象成了树的结构,但这两种算法在思路上是完全不同的。

leetcode练习

数独游戏

大家应该都玩过数独游戏,就是给你一个 9x9 的棋盘,其中有一些格子预先填入了数字,让你在其余空的格子中填入数字 1~9,要求每行、每列和每个 3x3 的九宫格内数字都不能重复。

下面是一个数独游戏的例子(来源 Wikipedia):

我小的时候也尝试过玩数独游戏,但只要稍微有些难度,就搞不定了。后来我才知道做数独是有技巧的,有一些比较专业的数独游戏软件会教你玩数独的技巧,不过在我看来这些技巧都太复杂,根本就没有兴趣看下去。

现在学习了回溯算法,多困难的数独问题都拦不住我了。只要有规则,就一定可以暴力穷举出符合条件的答案来,不是吗?

N 皇后问题

在国际象棋中,皇后可以攻击同一行、同一列和同一条对角线上的任意单位。N 皇后问题是指在一个 N×N 的棋盘上摆放 N 个皇后,要求任何两个皇后之间都不能互相攻击。

换句话说,就是让你在一个 N×N 的棋盘上放置 N 个皇后,使得每行、每列和每个对角线都只有一个皇后。

比如这是 8 皇后问题的一个解(来源 Wikipedia):

可以看到,对于任意一个皇后,它所在的行、列和对角线(左上、右上、左下、右下)都没有其他皇后,所以这就是一个符合规则的解。

在讲上述题目之前,我需要先讲一道比较简单的回溯算法问题,把这个问题作为铺垫,就能更容易理解数独游戏和 N 皇后问题的解法了。

n 位二进制数的所有可能

我来给你编一道简单的题目,请你实现这样一个函数:

python 复制代码
def generateBinaryNumber(n: int) -> List[str]:

函数的输入是一个正整数 n,请你返回所有长度为 n 的二进制数(0、1 组成),你可以按任意顺序返回答案。

比如说 n = 3,那么你需要以字符串形式返回如下 2^3=8种不同的二进制数:

000
001
010
011
100
101
110
111

这道题可以认为是数独游戏和 N 皇后问题的简化版:

这道题相当于让你对一个长度为 n 的一维数组中的每一个位置进行穷举,其中每个位置可以填 01本质还是遍历一颗二叉树。

数独游戏相当于让你对一个 9x9 的二维数组中的每个位置进行穷举,其中每个位置可以是数字 1~9,且同一行、同一列和同一个 3x3 的九宫格内数字不能重复。本质是遍历一颗九叉树。

N 皇后问题相当于让你对一个 N x N 的二维数组中的每个位置进行穷举,其中每个位置可以不放皇后或者放置皇后(相当于 01),且不能存在多个皇后在同一行、同一列或同一对角线上。本质是遍历一颗N叉树。

所以,只要你把这道简化版的题目的穷举过程搞明白,其他问题都迎刃而解了,无非是规则多了一些而已。

思路分析

题目是对一个长度为 n 的数组进行穷举,每个位置可以填入 01,让你返回所有可能的结果。

Important

看到这种题目,脑海中应该立刻出现一棵递归树;有了这棵树,你就有办法使用 回溯算法框架 无遗漏地穷举所有可能的解;能无遗漏地穷举所有解,就一定能找到符合题目要求的答案

具体来说,这道题的递归树应该是一棵 n + 1 层的二叉树。

递归树的节点就是穷举过程中产生的状态,以不同的视角来看这些节点,会有不同的作用。

具体来说就是以下问题:

应该如何理解这棵树每一层的节点 ;应该如何理解这棵树每一列(每条树枝)的节点 ;应该如何理解每个节点本身

你可以把鼠标移动到可视化面板生成的这棵递归树的每个节点上,会显示节点和树枝的信息。请你结合可视化面板的这些信息,听我来分析:

1、从层的角度来看,第 i 层的节点表示的就是长度为 i 的所有二进制数(i 从 0 开始算),比如第 2 层的节点分别是 00、01、10、11,这些就是长度为 2 的所有二进制数。

2、从列(树枝)的角度来看,每条树枝上的节点记录了状态转移的过程。比如你把鼠标移动到 001 这个节点上,树枝上会显示这个 001 是如何被穷举出来的,即首先选择 0,然后选择 0,最后选择 1。

3、从节点的角度来看,每个节点都是一个状态,从该节点向下延伸的树枝,就是当前状态的所有可能的选择。比如第二层的节点 01,它的前两位已经确定,现在要对第三位进行穷举。第三位可以是 0 或 1,所以从 01 这个节点向下延伸的两个子节点就是 010 和 011。

回溯算法代码框架

python 复制代码
class Solution:

    # 生成所有长度为 n 的二进制数
    def generateBinaryNumber(self, n: int) -> List[str]:
        res = []
        # 回溯路径
        track = []

        # 回溯算法核心框架
        def backtrack(i):
            if i == n:
                # 当递归深度等于 n 时,说明找到一个长度为 n 的二进制数,结束递归
                res.append(''.join(track))
                return

            # 选择有两种,0 或 1
            # 所以递归树是一棵二叉树
            for val in [0, 1]:
                # 做选择
                track.append(str(val))

                backtrack(i + 1)

                # 撤销选择
                track.pop()

        backtrack(0)
        return res

37. 解数独 | 力扣 | LeetCode | 🔴

编写一个程序,通过填充空格来解决数独问题。

数独的解法需遵循如下规则

  1. 数字 1-9 在每一行只能出现一次。
  2. 数字 1-9 在每一列只能出现一次。
  3. 数字 1-9 在每一个以粗实线分隔的 3x3 宫内只能出现一次。(请参考示例图)

数独部分空格内已填入了数字,空白格用 '.' 表示。

示例 1:

复制代码
输入: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"]]
输出:[["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"]]
解释:输入的数独如上图所示,唯一有效的解决方案如下所示:

提示:

  • board.length == 9
  • board[i].length == 9
  • board[i][j] 是一位数字或者 '.'
  • 题目数据 保证 输入数独仅有一个解

先不管数独游戏的规则,反正现在就是有 9x9=81 个格子,每个格子都可以填入 1~9 对吧,那总共就有 981981 种填法,把这些解法都穷举一遍,一定可以找到符合规则的答案。

对于这个问题,你心里应该抽象出一棵九叉递归树,共有 81 层,只要心里有了这棵树,就可以参照前面实现的 generateBinaryNumber 函数写出求解数独游戏的代码了。

不过你可能会有以下疑惑:

1、前面 generateBinaryNumber 是针对一维数组的穷举,如何才能适配到数独游戏的二维数组呢?

2、981981 是一个非常大的数字,穷举这么多递归树节点,即便写出回溯算法代码,也会超时吧?

3、generateBinaryNumber 函数返回的是所有解,而数独题目只要求找到一个可行解,所以当我们找到一个可行解时就应该结束算法,这样可以减少时间复杂度。

我来一一解答。

对于第一个问题 ,不熟悉递归的读者可能会有这个疑问,是不是在二维数组上递归就把你绕晕了?那么我们干脆玩个花样,直接绕过二维数组的问题,因为不管是几维的数组,都可以转化成一维数组

说白了,就是设计一个编解码算法,能够把一个多维坐标(多元组)编码到一个一维坐标(数字),且可以反向解码回去。

对于一个 MxN 的二维数组,可以把二维坐标 (i, j) 映射到一维坐标 index = i * N + j,反之可以通过 i = index / N 和 j = index % N 得到原来的二维坐标。

有了这个编解码算法,数独游戏的 9x9 二维数组就可以在逻辑上看做一个长度为 81 的一维数组,然后按照一维数组的递归树来穷举即可。

对于第二个问题,981981 只是一个非常粗略的估算,实际写代码时,要考虑数独的规则。

首先,初始棋盘会给你预先填充一些数字,这些数字是不能改变的,所以这些格子不用穷举,应该从 81 个格子去除。

另外,数独规则要求每行、每列和每个 3x3 的九宫格内数字都不能重复,所以实际上每个空格子可以填入的数字并不是真的有 9 种选择,而是要根据当前棋盘的状态,去除不合法的数字(剪枝)。

而且,981981 是穷举所有可行解的复杂度估算,实际上题目只要求我们寻找一个可行解,所以只要找到一个可行解就可以结束算法了,没必要穷举所有解。

那么这样算下来,穷举的实际复杂度取决于空格子的个数,远低于 981981,所以不用担心超时问题。

讲到这里,我们应该可以发现一个有趣的现象:

按照人类的一般理解,规则越多,问题越难;但是对于计算机来说,规则越多,问题反而越简单。

因为它要穷举嘛,规则越多,越容易剪枝,搜索空间就越小,就越容易找到答案。反之,如果没有任何规则,那就是纯暴力穷举,效率会非常低。

我们对回溯算法的剪枝优化,本质上就是寻找规则,提前排除不合法的选择,从而提高穷举的效率。

对于第三个问题 ,让算法在找到一个可行解时立即结束递归。这个问题有多种解决方案,我会建议使用一个全局变量来标记是否找到可行解(原因见 解答回溯/DFS的若干问题),辅助递归算法提前终止。

回溯算法代码实现:

python 复制代码
class Solution:
    # 标记是否已经找到可行解
    found = False

    def solveSudoku(self, board):
        self.backtrack(board, 0)


    # 路径:board 中小于 index 的位置所填的数字
    # 选择列表:数字 1~9
    # 结束条件:整个 board 都填满数字
    def backtrack(self, board, index):
        if self.found:
            # 已经找到一个可行解,立即结束
            return

        m, n = 9, 9
        i, j = index // n, index % n
        if index == m * n:
            # 找到一个可行解,触发 base case
            self.found = True
            return

        if board[i][j] != '.':
            # 如果有预设数字,不用我们穷举
            self.backtrack(board, index + 1)
            return

        for ch in '123456789':
            # 剪枝:如果遇到不合法的数字,就跳过
            if not self.isValid(board, i, j, ch):
                continue

            # 做选择
            board[i][j] = ch

            self.backtrack(board, index + 1)
            if self.found:
                # 如果找到一个可行解,立即结束
                # 不要撤销选择,否则 board[i][j] 会被重置为 '.'
                return

            # 撤销选择
            board[i][j] = '.'


    # 判断是否可以在 (r, c) 位置放置数字 num
    def isValid(self, board, r, c, num):
        for i in range(9):
            # 判断行是否存在重复
            if board[r][i] == num:
                return False
            # 判断列是否存在重复
            if board[i][c] == num:
                return False
            # 判断 3 x 3 方框是否存在重复
            if board[(r // 3) * 3 + i // 3][(c // 3) * 3 + i % 3] == num:
                return False
        return True

51. N 皇后 | 力扣 | LeetCode | 🔴

按照国际象棋的规则,皇后可以攻击与之处在同一行或同一列或同一斜线上的棋子。

n 皇后问题 研究的是如何将 n 个皇后放置在 n×n 的棋盘上,并且使皇后彼此之间不能相互攻击。

给你一个整数 n ,返回所有不同的 n皇后问题 的解决方案。

每一种解法包含一个不同的 n 皇后问题 的棋子放置方案,该方案中 'Q''.' 分别代表了皇后和空位。

示例 1:

复制代码
输入:n = 4
输出:[[".Q..","...Q","Q...","..Q."],["..Q.","Q...","...Q",".Q.."]]
解释:如上图所示,4 皇后问题存在两个不同的解法。

示例 2:

复制代码
输入:n = 1
输出:[["Q"]]

提示:

  • 1 <= n <= 9

其实 N 皇后问题和数独游戏的解法思路是一样的,具体有以下差别:

1、数独的规则和 N 皇后问题的规则不同,我们需要修改 isValid 函数,判断一个位置是否可以放置皇后。

2、题目要求找到 N 皇后问题所有解,而不是仅仅找到一个解。我们需要去除算法提前终止的逻辑,让算法穷举并记录所有的合法解。

这两个问题都比较容易解决,按理说可以直接参照数独游戏的代码实现 N 皇后问题。

但 N 皇后问题相对数独游戏还有一个优化:我们可以以行为单位进行穷举,而不是像数独游戏那样以格子为单位穷举

举个直观的例子,在数独游戏中,如果我们设置 board[i][j] = 1,接下来呢,要去穷举 board[i][j+1] 了对吧?而对于 N 皇后问题,我们知道每行必然有且只有一个皇后,所以如果我们决定在 board[i] 这一行的某一列放置皇后,那么接下来就不用管 board[i] 这一行了,应该考虑 board[i+1] 这一行的皇后要放在哪里。

所以 N 皇后问题的穷举对象是棋盘中的行,每一行都持有一个皇后,可以选择把皇后放在该行的任意一列。

回溯算法代码实现:

python 复制代码
class Solution:
    def __init__(self):
        self.res = []

    def solveNQueens(self, n: int) -> list[list[str]]:
        # 每个字符串代表一行,字符串列表代表一个棋盘
        # '.' 表示空,'Q' 表示皇后,初始化空棋盘
        board = ["." * n for _ in range(n)]
        self.backtrack(board, 0)
        return self.res

    # 路径:board 中小于 row 的那些行都已经成功放置了皇后
    # 选择列表:第 row 行的所有列都是放置皇后的选择
    # 结束条件:row 超过 board 的最后一行
    def backtrack(self, board: list[str], row: int):
        if row == len(board):
            # 棋盘已经填满,找到一个解
            self.res.append(board[:])
            return

        n = len(board[row])
        for col in range(n):
            # 剪枝,排除不合法选择
            if not self.isValid(board, row, col):
                continue
            # 做选择
            board[row] = board[row][:col] + 'Q' + board[row][col+1:]

            # 进入下一行决策
            self.backtrack(board, row + 1)

            # 撤销选择
            board[row] = board[row][:col] + '.' + board[row][col+1:]

    # 是否可以在 board[row][col] 放置皇后?
    def isValid(self, board: list[str], row: int, col: int) -> bool:
        n = len(board)

        # 因为我们是从上往下放置皇后的,
        # 所以只需要检查上方是否有皇后互相冲突,
        # 不需要检查下方

        # 检查列是否有皇后互相冲突
        for i in range(row):
            if board[i][col] == 'Q':
                return False

        # 检查右上方是否有皇后互相冲突
        i, j = row - 1, col + 1
        while i >= 0 and j < n:
            if board[i][j] == 'Q':
                return False
            i, j = i - 1, j + 1

        # 检查左上方是否有皇后互相冲突
        i, j = row - 1, col - 1
        while i >= 0 and j >= 0:
            if board[i][j] == 'Q':
                return False
            i, j = i - 1, j - 1

        return True

52. N 皇后 II | 力扣 | LeetCode | 🔴

n 皇后问题 研究的是如何将 n 个皇后放置在 n × n 的棋盘上,并且使皇后彼此之间不能相互攻击。

给你一个整数 n ,返回 n 皇后问题 不同的解决方案的数量。

示例 1:

复制代码
输入:n = 4
输出:2
解释:如上图所示,4 皇后问题存在两个不同的解法。

示例 2:

复制代码
输入:n = 1
输出:1

提示:

  • 1 <= n <= 9

直接把 res 变量变成 int 类型,每次在 base case 找到一个合法答案的时候递增 res 变量即可:

python 复制代码
class Solution:
    def __init__(self):
        # 仅仅记录合法结果的数量
        self.res = 0

        def backtrack(self, board: list[str], row: int):
            if row == len(board):
                # 找到一个合法结果
                self.res += 1
                return

            # 其他都一样

总结

回溯算法本质上就是遍历一颗多叉树:

1、确定递归终止条件

2、执行单层操作逻辑

3、确定下一层递归所需的操作列表

4、注意剪枝和代码优化

相关推荐
一只各种都搞一下的攻城狮2 分钟前
3天用AI辅助完成资源卫星高光谱数据基本处理软件
python·ai辅助·遥感光谱·几何校正
小姚也要变强8 分钟前
sort排序 计数排序 map set C++ 蓝桥杯
开发语言·c++·算法·蓝桥杯
WBluuue19 分钟前
大一计算机的自学总结:数据结构设计相关题
数据结构·c++·算法·leetcode·链表
白白糖22 分钟前
Day 28 卡玛笔记
python·算法·力扣
Lx35230 分钟前
Pandas高级数据处理:数据流处理
后端·python·pandas
JNU freshman32 分钟前
蓝桥杯思维训练营(四)
算法·蓝桥杯
BingLin-Liu33 分钟前
蓝桥杯备考:前缀和算法之最大子段和
算法
AI量化投资实验室35 分钟前
celery策略回测任务运行及金融量化数据增量更新|年化18.8%,回撤8%的组合策略(python代码)
开发语言·python·金融
和风化雨37 分钟前
排序算法--归并排序
c语言·数据结构·c++·算法·排序算法
Doopny@1 小时前
统计满足条件的4位数(信息学奥赛一本通-1077)
算法