回溯算法通常被视为一种特殊类型的递归遍历。它在探索所有可能的解决方案时,使用递归来进行深度优先搜索(DFS)。回溯算法的关键特点和机制包括:
关键特点
- 递归:
- 回溯算法通过递归函数来遍历所有可能的选择。在每一步,递归函数会选择一个选项,并继续深入下去,直到满足某个条件或达到一个结束状态。
- 状态管理:
- 在递归过程中,使用额外的数据结构(如列表、集合或数组)来维护当前状态。这通常用于跟踪当前选择的路径或已选择的元素,以确保不重复选择。
- 选择与撤销:
- 回溯算法在每次选择一个选项后,递归深入探索可能的后续选项。如果发现当前选择不能导致有效解或达到了一个结束条件,算法会撤销最近的选择,回到之前的状态,以尝试其他可能的选项。
- 结束条件:
- 每个回溯算法都有明确的结束条件,比如找到一个有效解、达到特定深度或满足某个条件。这些条件帮助算法确定何时停止递归并返回结果。
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
参数确保了不重复地选择每个元素,从而实现“选”和“不选”的逻辑划分。
通过这种递归和循环的结合,“选”和“不选”的操作不需要显式定义,而是通过递归函数的自然分裂来实现。如果对这一过程还有疑问或者需要具体示例,请继续提问!
是的,回溯算法通过“回到上一个状态”的操作配合循环来实现元素的“选”与“不选”。每当选择一个元素进入当前路径时,递归进入下一层决策;当递归结束或不满足条件时,通过撤销选择(回溯)返回上一个状态,再尝试其他可能的选择。这种操作机制使得回溯算法能够系统地遍历所有可能的组合。
回溯的工作原理
- 递归进入:每次递归进入时,当前元素被“选中”,加入路径中,形成一个新的状态。
- 递归返回:递归到某个状态完成后,会撤销最近的选择,回到上一个状态,这个撤销的过程就是回溯。
- 循环控制:配合循环控制,在回溯后继续尝试下一个元素的选择,实现不选当前元素而选择其他元素的操作。
选与不选的实现过程
- “选”:在当前递归层,选择当前元素加入路径,并递归进入下一层。
- “不选”:通过回溯返回上一个状态,退出当前选择的路径,然后在循环中跳过当前元素,尝试其他选择。
回溯配合循环实现上诉操作。
传递 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)。如果digits
是None
、空字符串""
、空列表[]
、空字典{}
、空元组()
或者零(0
),这条语句都会返回True
。
- 这条语句会检查
-
适用场景:
- 适用于想要检查变量是否“为空”的情况。在处理字符串、列表或其他可迭代对象时,通常用来判断对象是否有内容。
-
示例:
digits = "" if not digits: # 这里会返回 True,因为 digits 是一个空字符串 print("Digits is empty.")
2. if digits is None:
-
意义:
- 这条语句专门检查
digits
是否为None
。只有在digits
是None
时,条件才会返回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
函数:
candidates
或nums
:- 包含所有可用的选项,通常是一个列表。
start
或index
:- 当前选择的起始位置,防止重复选择,尤其在组合问题中很重要。
target
(可选):- 目标值(如目标和、长度等),用于判断当前选择是否满足条件。
current
或track
:- 当前路径或选择的状态,用于存储当前递归的结果。
results
:- 存储所有有效结果的列表,通常用作全局变量或类属性。
3. 思考顺序
在设计 backtrack
函数时,可以遵循以下思考顺序:
- 理解问题:
- 明确输入、输出和约束条件。思考所需的组合、排列或解的性质。
- 确定状态的表示:
- 决定如何表示当前选择的状态(例如,使用一个列表存储当前组合)。
- 选择参数:
- 选择哪些参数是必要的,以便在递归调用中传递正确的信息。
- 基础条件:
- 确定递归的终止条件,例如何时记录结果、何时返回。
- 做选择和撤销选择:
- 在递归中添加当前选择,进行下一步的递归,然后撤销选择以返回到上一步
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
对列表的浅拷贝有效是因为浅拷贝创建了一个新的列表对象,但新列表中的元素仍然引用原始列表中的元素。这意味着:
- 新列表: 浅拷贝会创建一个新列表,具有独立的内存地址。
- 引用原始元素: 新列表中的元素仍然是原始列表中元素的引用。对于可变对象(如列表或字典),如果你修改了这些可变对象,原始列表会受到影响。
浅拷贝 创建了一个新列表,但其内容是对原始元素的引用。
对于可变对象的修改会影响原始列表,但新列表和原列表是独立的对象,具有不同的内存地址。
浅拷贝对于简单数据结构和不可变数据类型通常是足够的,并且效率更高。
深拷贝在需要完全独立的副本或处理复杂数据结构时是必要的。
也就是浅拷贝拷贝里面原先元素的地址,放入一个新的存储空间,若内部还是可变变量,则改变浅拷贝中可变变量值也会影响原列表
- 可变对象: 改变其内容时,内存地址保持不变。
- 不可变对象: 改变其内容时,会创建一个新的对象,内存地址会改变。
这种特性使得在使用可变和不可变对象时,具有不同的行为和适用场景。
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 中非常实用,允许程序员在模块中编写可重用的功能,同时又能方便地添加测试或示例代码,从而增强代码的可维护性和可读性。