现在你问「怎么剪枝」,其实就是在 循环里面提前判断,如果后面剩下的数字不够了,就不用再递归下去了。
好问题 👍,这行代码是剪枝的关键。
我给你拆开解释,再举一个例子。
原来的写法
for i in range(start, len(nums)):
👉 意思是从 start
到 len(nums)-1
都试一遍。
这样写没问题,但是可能会进入没必要的递归。
改进的写法
for i in range(start, len(nums) - (k - len(path)) + 1):
这里 len(nums) - (k - len(path)) + 1
是一个剪枝上界。
我们来分解它:
len(nums)
:总共有多少个数len(path)
:当前已经选了多少个数k - len(path)
:还需要多少个数len(nums) - (k - len(path))
:保证后面至少能取到k - len(path)
个数+1
:因为range
的右边界是开区间,要多加 1
def simple_backtrack():nums = [1, 2, 3]k = 2result = []def backtrack(path, start):# 结束条件:长度为 k 就收集答案if len(path) == k:result.append(path[:])return# 剪枝: i 最大只能到 len(nums) - (k - len(path))for i in range(start, len(nums) - (k - len(path)) + 1):# 做选择path.append(nums[i])print(f"选择 {nums[i]},当前path: {path}")# 递归backtrack(path, i + 1)# 撤销选择path.pop()print(f"撤销 {nums[i]},当前path: {path}")backtrack([], 0)return result# 运行
result = simple_backtrack()
print(f"最终结果: {result}")
---判断的意义。后面的全算上也不能满足组合的长度了————
👌 好的,我给你把 len(nums) - (k - len(path)) + 1
的意义和随 path
变化的情况拆开分析。
设:
n = len(nums)
k
= 目标组合长度len(path)
= 当前已经选了多少个数
公式:
上界 = n - (k - len(path)) + 1
意思是:
我必须保证从当前位置 i
开始,后面剩下的数足够补齐 k - len(path)
个,否则没必要再选。
举例:n=5, k=3
nums = [1,2,3,4,5]
① 初始状态
path = []
len(path) = 0
还需要
k - len(path) = 3
上界 =
5 - 3 + 1 = 3
👉 for i in range(start, 3)
只能取 i = 0,1,2(对应数字 1,2,3)
不会取 i=3,4(因为只剩下 2 个数,不够 3 个了)。
② 当 path=[1]
len(path)=1
还需要
k - len(path) = 2
上界 =
5 - 2 + 1 = 4
👉 for i in range(start, 4)
可以取 i=1,2,3(对应数字 2,3,4)
不会取 i=4(因为只剩 5,不够 2 个了)。
③ 当 path=[1,2]
len(path)=2
还需要
k - len(path) = 1
上界 =
5 - 1 + 1 = 5
👉 for i in range(start, 5)
可以取 i=2,3,4(对应数字 3,4,5)
这时基本没剪枝,因为只需要再补 1 个,随便选。
④ 当 path=[1,2,3]
len(path)=3
已经够了,不会再进入循环。
总结成规律:
path 短(差很多个数) → 上界更小,循环范围收紧,剪枝多。
path 长(差很少个数) → 上界更接近 n,循环范围放宽,剪枝少。
它的作用就是:
保证“至少还能凑齐 k 个数”,避免白白进入不可能成功的递归分支。
要不要我帮你写个小程序,把 len(path)
从 0 到 k-1 的变化,以及对应的上界值打印出来,让你一步步看?