回溯算法,hot100

回溯算法通常被视为一种特殊类型的递归遍历。它在探索所有可能的解决方案时,使用递归来进行深度优先搜索(DFS)。回溯算法的关键特点和机制包括:

关键特点

  1. 递归
    • 回溯算法通过递归函数来遍历所有可能的选择。在每一步,递归函数会选择一个选项,并继续深入下去,直到满足某个条件或达到一个结束状态。
  2. 状态管理
    • 在递归过程中,使用额外的数据结构(如列表、集合或数组)来维护当前状态。这通常用于跟踪当前选择的路径或已选择的元素,以确保不重复选择。
  3. 选择与撤销
    • 回溯算法在每次选择一个选项后,递归深入探索可能的后续选项。如果发现当前选择不能导致有效解或达到了一个结束条件,算法会撤销最近的选择,回到之前的状态,以尝试其他可能的选项。
  4. 结束条件
    • 每个回溯算法都有明确的结束条件,比如找到一个有效解、达到特定深度或满足某个条件。这些条件帮助算法确定何时停止递归并返回结果。

46. 全排列 元素不重复,

在回溯算法中,将状态标记(如 used)作为局部变量传递给递归函数是一个良好的实践。这种方法确保了每个递归调用之间的状态独立性,使得代码更加清晰易懂,避免潜在的状态干扰问题。

通过遍历 nums 的长度,我们能够检查每一个数字,确保算法在构建全排列时不会重复使用同一个数字。遍历所有可能的索引是回溯算法的核心,因为我们需要探索所有可能的路径并在必要时进行撤回和重新选择。

这种方法使得回溯算法能够有效生成所有不重复的排列,并保持对当前选择状态的跟踪。

仔细思考清楚代码的实现。

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

    def permute(self, nums: List[int]) -> List[List[int]]:
        track = []
        used = [False] * len(nums)
        
        self.backtrack(nums, track, used)
        return self.res 
        
    def backtrack(self, nums, track, used):
        if len(track) == len(nums):
            self.res.append(track.copy())
            return 
            #递归到最后返回
        
        for i in range(len(nums)):#每一次进入回溯都重新过一遍这些数字  这一层循环走一遍其实就是选与不选,也就是分叉
        #这个for循环很关键,就是每一次都从所有数字中来找到没有使用过得数字,这里是决策树,树枝的分叉地
            if used[i]:
                continue 
            
            track.append(nums[i])
            used[i] = True 

            self.backtrack(nums, track, used)

            track.pop()
            used[i] = False 

78. 子集

如何实现元素的选和不选? 选与不选通过循环加遍历来实现,

总结如何实现“选”与“不选”

  • “选”:在循环中显式地将当前元素加入路径 track.append(nums[i]) 并继续递归。
  • “不选”:通过递归和循环控制实现。在一次循环结束或跳过时,自然不选择该元素,转而选择后续的元素,这就相当于“不选”。

理解方式

  • 递归中的分支:每一次递归都有两个自然分支——一个是选择当前元素进入下一层,另一个是跳过当前元素进入下一轮循环。
  • 循环的控制start 参数确保了不重复地选择每个元素,从而实现“选”和“不选”的逻辑划分。

通过这种递归和循环的结合,“选”和“不选”的操作不需要显式定义,而是通过递归函数的自然分裂来实现。如果对这一过程还有疑问或者需要具体示例,请继续提问!

是的,回溯算法通过“回到上一个状态”的操作配合循环来实现元素的“选”与“不选”。每当选择一个元素进入当前路径时,递归进入下一层决策;当递归结束或不满足条件时,通过撤销选择(回溯)返回上一个状态,再尝试其他可能的选择。这种操作机制使得回溯算法能够系统地遍历所有可能的组合。

回溯的工作原理

  1. 递归进入:每次递归进入时,当前元素被“选中”,加入路径中,形成一个新的状态。
  2. 递归返回:递归到某个状态完成后,会撤销最近的选择,回到上一个状态,这个撤销的过程就是回溯。
  3. 循环控制:配合循环控制,在回溯后继续尝试下一个元素的选择,实现不选当前元素而选择其他元素的操作。

选与不选的实现过程

  • “选”:在当前递归层,选择当前元素加入路径,并递归进入下一层。
  • “不选”:通过回溯返回上一个状态,退出当前选择的路径,然后在循环中跳过当前元素,尝试其他选择。

回溯配合循环实现上诉操作。

传递 track 作为参数是标准的回溯设计,确保了每一层递归调用都能正确地构建和撤销操作,从而完整且无误地生成所有子集。 将track恢复到调用之前的状态

我们通过保证元素之间的相对顺序不变来防止出现重复的子集

1. 全局变量的缺点

  • 共享同一个状态:如果 track 是一个全局变量,并且直接在递归中修改,那么所有递归层级都会对同一个 track 进行操作。这样,当一个递归调用改变 track 的内容时,这个改变会立即反映在所有递归层级中。
  • 递归层级干扰:由于递归的回溯操作需要撤销上一次选择,以恢复到递归前的状态,如果多个递归层级共享同一个 track,撤销操作可能会误改其他层级的 track,导致最终结果不正确。例如,某个递归调用可能在执行回溯时误移除或添加元素,干扰了其他层级的构造过程。

2. track 作为参数的优点

  • 每次递归有独立状态:将 track 作为参数传递,意味着每次递归调用都在处理它自己上下文中的 track。每个递归调用对 track 的修改不会影响其他递归调用。
  • 状态隔离:通过传递 track,每个递归层级能够独立管理自己的状态。回溯操作仅作用于当前层级,不会影响其他层级的 track,从而避免了全局变量带来的状态污染问题。
class Solution:
    #track作为局部变量,并且每次只能从之后元素中选值进行自己的选择
    def __init__(self):
        self.res = []
    def subsets(self, nums: List[int]) -> List[List[int]]:
        track = []
        
        self.backtrack(nums, 0, track) #track作为参数传递
        return self.res 
    
    def backtrack(self, nums, start, track):
        self.res.append(track.copy())
        for i in range(start, len(nums)):
            track.append(nums[i])
            self.backtrack(nums, i + 1, track)
            track.pop()
            

理解回溯算法的关键在于把握“选择”、“探索”和“撤销”的过程。可以通过不断练习不同的回溯问题,加深对这个框架的理解。对于每个具体问题,细化状态和决策逻辑将帮助你更清晰地思考如何实现回溯。

17. 电话号码的字母组合

在计算机科学中的含义:

  • 在计算机科学和编程中,“ordinal” 常常用于描述数据类型的顺序或位置。例如,在某些编程语言中,字符的“ordinal”值是指该字符在 Unicode 字符集中对应的整数值。
  • 例如,字符 ‘A’ 的 ordinal 值是 65,因为在 ASCII 和 Unicode 中,‘A’ 对应的码点是 65。

空列表并非是None,在 Python 中,if not digits:if digits is None: 这两种条件语句虽然在某些情况下可以达到相似的目的,但它们的含义和适用场景是不同的。下面详细说明这两者的区别。

1. if not digits:

  • 意义:

    • 这条语句会检查 digits 的真值(truthiness)。如果 digitsNone、空字符串 ""、空列表 []、空字典 {}、空元组 () 或者零(0),这条语句都会返回 True
  • 适用场景:

    • 适用于想要检查变量是否“为空”的情况。在处理字符串、列表或其他可迭代对象时,通常用来判断对象是否有内容。
  • 示例:

    digits = ""
    if not digits:  # 这里会返回 True,因为 digits 是一个空字符串
        print("Digits is empty.")
    

2. if digits is None:

  • 意义:

    • 这条语句专门检查 digits 是否为 None。只有在 digitsNone 时,条件才会返回 True。如果 digits 是其他类型的空值(如空字符串、空列表等),这个条件将返回 False
  • 适用场景:

    • 适用于需要严格检查一个变量是否为 None 的情况。这通常是在处理可选参数或检查函数返回值时非常有用。
  • 示例:

    digits = None
    if digits is None:  # 这里会返回 True,因为 digits 是 None
        print("Digits is None.")
    

3. 区别总结

语句意义适用场景
if not digits:检查变量是否为空(包括 None 和其他空值)判断是否有内容,常用于可迭代对象的处理
if digits is None:检查变量是否严格等于 None检查可选参数或函数返回值是否为 None

选择的建议

  • 如果你希望在函数中处理任何空值(如空字符串、空列表),使用 if not digits:
  • 如果你只关心变量是否为 None,则使用 if digits is None:

在具体代码实现中,选择适当的条件语句可以提高代码的可读性和逻辑清晰性。

class Solution:
    mapping = ['','', 'abc', 'def', 'ghi', 'jkl', 'mno',
    'pqrs', 'tuv', 'wxyz']
    def letterCombinations(self, digits: str) -> List[str]:
        self.res = [] 
        self.sb = []
    
        if not digits:
            return self.res 
            
        self.backtrack(digits, 0)
        return self.res 
    
    def backtrack(self, digits, start) -> None:
        if len(self.sb) == len(digits):
            self.res.append(''.join(self.sb))
            return 
        
        #当前数字
        digit = ord(digits[start]) - ord('0')
        for c in self.mapping[digit]:
            #这一层循环配合递归,实现选择和不选择
            self.sb.append(c)#这是一个一个加进去,所以之后要连接起来
            self.backtrack(digits, start + 1) #选第二个数字对应的字母
            self.sb.pop()

记得思考终止条件是什么,

解决回溯问题的思考顺序可以归纳为理解问题、确定解的结构、选择与探索、检查终止条件、撤销选择、

39. 组合总和

class Solution:
    def __init__(self):
        # 初始化结果列表和当前路径列表
        self.res = []  # 存储所有符合条件的组合
        self.track = []  # 存储当前的组合路径!!!!

    def combinationSum(self, candidates: List[int], target: int) -> List[List[int]]:
        # 从索引 0 开始回溯
        self.backtrack(candidates, 0, target, 0)
        return self.res  # 返回所有组合的结果

    def backtrack(self, candidates, start, target, sum: int) -> None:
        # 基础情况:如果当前和等于目标值,记录当前路径,用sum记录当前和,可以找到不需要再递归下去的情况。 
        if sum == target:
            self.res.append(self.track[:])  # 复制当前组合并添加到结果中
            return 

        # 如果当前和超过目标值,终止此路径
        if sum > target:
            return 

        # 遍历候选数字
        for i in range(start, len(candidates)):
            # 做选择:将当前候选数字添加到组合中
            self.track.append(candidates[i])
            sum += candidates[i]  # 更新当前和
            
            # 递归调用,继续寻找组合,注意这里传递 i 而不是 i + 1,以允许重复使用同一个数字
            self.backtrack(candidates, i, target, sum)

            # 撤销选择:回溯到上一状态
            sum -= candidates[i]  # 撤销当前和的更新
            self.track.pop()  # 移除最后添加的数字,准备下一次选择

写回溯函数时候思考要用到哪些变量,

在回溯算法中,自定义的 backtrack 函数的参数通常取决于具体问题的要求。为了设计一个合适的 backtrack 函数,我们需要考虑以下几个关键要素:

1. 问题状态的表示

  • 当前路径:表示当前的选择或组合。通常用一个列表来存储当前的状态或选择。
  • 输入数据:包含所有可能的选择(如候选数字、字符等)。
  • 目标条件:用于判断当前选择是否有效(如是否达到目标和、组合的长度等)。

2. 需要的参数

以下是一些常见的参数,通常会用于自定义的 backtrack 函数:

  1. candidatesnums:
    • 包含所有可用的选项,通常是一个列表。
  2. startindex:
    • 当前选择的起始位置,防止重复选择,尤其在组合问题中很重要。
  3. target (可选):
    • 目标值(如目标和、长度等),用于判断当前选择是否满足条件。
  4. currenttrack:
    • 当前路径或选择的状态,用于存储当前递归的结果。
  5. results:
    • 存储所有有效结果的列表,通常用作全局变量或类属性。

3. 思考顺序

在设计 backtrack 函数时,可以遵循以下思考顺序:

  1. 理解问题
    • 明确输入、输出和约束条件。思考所需的组合、排列或解的性质。
  2. 确定状态的表示
    • 决定如何表示当前选择的状态(例如,使用一个列表存储当前组合)。
  3. 选择参数
    • 选择哪些参数是必要的,以便在递归调用中传递正确的信息。
  4. 基础条件
    • 确定递归的终止条件,例如何时记录结果、何时返回。
  5. 做选择和撤销选择
    • 在递归中添加当前选择,进行下一步的递归,然后撤销选择以返回到上一步

22. 括号生成

括号的性质,

1、一个「合法」括号组合的左括号数量一定等于右括号数量,这个很好理解

2、对于一个「合法」的括号字符串组合 p,必然对于任何 0 <= i < len(p) 都有:子串 p[0..i] 中左括号的数量都大于或等于右括号的数量

因为从左往右算的话,肯定是左括号多嘛,到最后左右括号数量相等,说明这个括号组合是合法的。

left 记录还可以使用多少个左括号,用 right 记录还可以使用多少个右括号

任意位置,左括号的数量一定要大于右括号,否则不可能是合法的括号。


class Solution:
    #track是字符串, left, right传递给回溯函数,分别记录剩余能使用的左右对
    def generateParenthesis(self, n: int) -> List[str]:
        self.res = []
        self.track = []

        self.backtrack(n, n)
        return self.res 
    def backtrack(self, left: int, right: int):
        if right < left: #左括号剩下的多,不合法
            return 
        if left < 0 or right < 0:
            return 
        
        if left == 0 and right == 0:
            self.res.append(''.join(self.track))
        

        #尝试放一个左括号
        self.track.append('(')
        self.backtrack(left - 1, right)
        self.track.pop()
        #放配合回溯,其实就代表了选与不选
        self.track.append(')')
        self.backtrack(left, right - 1)
        self.track.pop()

79. 单词搜索

类似岛屿问题,这道题用标记来标记当前访问的这个格子,避免走回头路。

这一题在4个方向进行探索,之后回溯。

class Solution:
    def exist(self, board: List[List[str]], word: str) -> bool:
        if not board:
            return False
        
        self.rows, self.cols = len(board), len(board[0])
        #这样处理,就可以在全局使用这个类变量,行数和列数了,避免重复写行数和列数
        for i in range(self.rows):
            for j in range(self.cols):
                if self.dfs(board, word, i, j, 0):
                    return True
        return False
  #先遍历所有格子,找到第一个需要的,然后接着四个方向搜索第二位置所需要的
    def dfs(self, board: List[List[str]], word: str, i: int, j: int, index: int) -> bool:
        #都是先写结束条件以及边界条件。
        #这一题的回溯是进去4个方向,然后再回溯。
        # 如果找到完整的单词
        if index == len(word):
            return True
        
        # 超出边界或字母不匹配或已访问
        if (i < 0 or i >= self.rows or 
            j < 0 or j >= self.cols or 
            board[i][j] != word[index]):
            return False
        
        # 保存当前字母并标记为已访问
        temp = board[i][j]
        board[i][j] = '#'

        # 递归搜索相邻的单元格
        found = (self.dfs(board, word, i + 1, j, index + 1) or  # 下
                 self.dfs(board, word, i - 1, j, index + 1) or  # 上
                 self.dfs(board, word, i, j + 1, index + 1) or  # 右
                 self.dfs(board, word, i, j - 1, index + 1))    # 左
        
        # 恢复单元格的状态
        board[i][j] = temp
        
        return found

131. 分割回文串

class Solution:
    
    def partition(self, s: str) -> List[List[str]]:
        self.res = []
        self.track = []
        self.backtrack(s, 0)
        return self.res 
    
    def backtrack(self, s:str, start: int) -> None:
        if start == len(s):
            self.res.append(self.track.copy()) #进行浅拷贝,如果只是单纯给引用,最后返回就都是None
        for i in range(start, len(s)):
            if not self.isPalindrome(s, start, i):
                continue 
            #s[start, i] 是回文串
            self.track.append(s[start : i + 1])
            self.backtrack(s, i + 1)
            self.track.pop()
        
    
    def isPalindrome(self, s, lo, hi) -> bool:
        while lo < hi:
            if s[lo] != s[hi]:
                return False 
            lo += 1
            hi -= 1
        return True 

对列表的浅拷贝有效是因为浅拷贝创建了一个新的列表对象,但新列表中的元素仍然引用原始列表中的元素。这意味着:

  1. 新列表: 浅拷贝会创建一个新列表,具有独立的内存地址。
  2. 引用原始元素: 新列表中的元素仍然是原始列表中元素的引用。对于可变对象(如列表或字典),如果你修改了这些可变对象,原始列表会受到影响。

浅拷贝 创建了一个新列表,但其内容是对原始元素的引用。

对于可变对象的修改会影响原始列表,但新列表和原列表是独立的对象,具有不同的内存地址。

浅拷贝对于简单数据结构和不可变数据类型通常是足够的,并且效率更高。

深拷贝在需要完全独立的副本或处理复杂数据结构时是必要的。

也就是浅拷贝拷贝里面原先元素的地址,放入一个新的存储空间,若内部还是可变变量,则改变浅拷贝中可变变量值也会影响原列表

  • 可变对象: 改变其内容时,内存地址保持不变。
  • 不可变对象: 改变其内容时,会创建一个新的对象,内存地址会改变。

这种特性使得在使用可变和不可变对象时,具有不同的行为和适用场景。

list.copy()是浅拷贝,大部分情况够用了,把元素地址存入新列表中。 新列表是新的地址

是的,列表在 Python 中确实有自己的内存地址。每个对象(包括列表)在内存中都有一个唯一的地址,这个地址可以通过 id() 函数查看。

示例

# 创建一个列表
my_list = [1, 2, 3]

# 获取并打印列表的内存地址
print("List ID:", id(my_list))  # 输出列表的内存地址

说明

  • id(my_list) 返回的是列表对象在内存中的地址。这个地址在对象的生命周期内是唯一的。
  • 当你对列表进行修改时(例如添加、删除或改变元素),列表的地址不会改变,因为你是在原地修改对象。

总结

  • 列表是对象: 列表在 Python 中是一个对象,拥有自己的内存地址。
  • 地址可用: 可以使用 id() 函数获取列表的内存地址,帮助理解其在内存中的存储情况。

如何用pycharm自己调试

if __name__ == "__main__": 机制是 Python 中非常重要的一种控制结构,用于确定代码块的执行方式。以下是对这一机制的详细介绍:

1. __name__ 的作用

在 Python 中,每个模块都有一个内置的属性 __name__。这个属性用于表示模块的名称。

  • 当模块直接运行时: 如果你直接执行某个 Python 文件,例如 python my_script.py,Python 会将该文件的 __name__ 设置为 "__main__"

  • 当模块被导入时: 如果你在其他文件中导入该模块,例如 from my_script import some_function,则该模块的 __name__ 会被设置为模块的文件名(不带 .py 后缀)。

2. 代码块的作用

使用 if __name__ == "__main__": 结构,可以控制某些代码仅在模块作为主程序执行时才会运行。这通常用于:

  • 测试代码: 在模块中添加测试代码,以便在直接运行模块时测试其功能。
  • 示例用法: 提供如何使用模块中的类和函数的示例。

3. 具体示例

以下是一个示例代码,展示了这一机制的使用:

# my_module.py

def greet(name):
    return f"Hello, {name}!"

# 这个代码块只在直接运行该模块时执行
if __name__ == "__main__":
    # 测试代码
    print(greet("World"))  # 输出: Hello, World!
  • 如果你运行 python my_module.py,你将看到输出:

    Hello, World!
    
  • 如果你在另一个文件中导入这个模块:

# main.py
from my_module import greet

print(greet("Alice"))  # 输出: Hello, Alice!
  • 运行 python main.py 时,my_module.py 中的 print(greet("World")) 不会被执行。

4. 好处

  • 模块重用: 通过这种机制,你可以将代码组织成模块,并在其他程序中重用,而不会意外地执行不必要的测试代码。
  • 清晰的结构: 使得代码结构更清晰,分离了模块功能与测试或示例代码。
  • 调试方便: 可以快速测试模块的某些功能,而不需要创建额外的测试文件。

5. 何时使用

  • 模块开发: 当你编写可重用的代码库时,应该使用这个机制来提供示例和测试。
  • 脚本开发: 当你编写需要独立运行的脚本时,也可以使用此机制来控制程序的执行流。

总结

if __name__ == "__main__": 机制在 Python 中非常实用,允许程序员在模块中编写可重用的功能,同时又能方便地添加测试或示例代码,从而增强代码的可维护性和可读性。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值