LeetCode第39题"组合总和"是回溯算法中的经典问题。给定一个无重复元素的整数数组candidates和一个目标数target,找出所有可以使数字和等于target的唯一组合。candidates中的数字可以无限制重复被选取。
这个问题在实际开发中有着广泛的应用场景,比如:
回溯算法的核心框架如下:
python复制def combinationSum(candidates, target):
res = []
def backtrack(start, path, remaining):
if remaining == 0:
res.append(path.copy())
return
for i in range(start, len(candidates)):
if candidates[i] > remaining:
continue
path.append(candidates[i])
backtrack(i, path, remaining - candidates[i])
path.pop()
candidates.sort()
backtrack(0, [], target)
return res
关键点解析:
注意:path列表在添加到结果集时需要使用copy(),否则后续修改会影响已存储的结果
最坏情况下时间复杂度为O(N^(T/M+1)),其中:
空间复杂度主要取决于递归调用栈的深度,最坏为O(T/M)
第40题"组合总和II"与前一题的主要区别在于:
这带来了两个新的挑战:
python复制def combinationSum2(candidates, target):
res = []
candidates.sort()
def backtrack(start, path, remaining):
if remaining == 0:
res.append(path.copy())
return
for i in range(start, len(candidates)):
# 剪枝:当前值大于剩余值
if candidates[i] > remaining:
break
# 去重:跳过同一层相同的元素
if i > start and candidates[i] == candidates[i-1]:
continue
path.append(candidates[i])
backtrack(i+1, path, remaining - candidates[i])
path.pop()
backtrack(0, [], target)
return res
关键改进点:
这种带限制条件的组合问题常见于:
第131题要求将字符串s分割成若干子串,使得每个子串都是回文串。返回所有可能的分割方案。
这个问题的挑战在于:
优化方案是先用动态规划预处理回文信息:
python复制def partition(s):
n = len(s)
dp = [[False]*n for _ in range(n)]
for i in range(n):
dp[i][i] = True
for length in range(2, n+1):
for i in range(n-length+1):
j = i+length-1
if s[i] == s[j]:
if length == 2 or dp[i+1][j-1]:
dp[i][j] = True
res = []
def backtrack(start, path):
if start == n:
res.append(path.copy())
return
for end in range(start, n):
if dp[start][end]:
path.append(s[start:end+1])
backtrack(end+1, path)
path.pop()
backtrack(0, [])
return res
通过这三道题目,我们可以总结出回溯算法的通用模板:
python复制def backtrack(选择列表, 路径, 其他参数):
if 满足结束条件:
结果列表.append(路径副本)
return
for 选择 in 选择列表:
if 不符合条件:
continue/break
做选择
backtrack(更新后的参数)
撤销选择
在实际工程中是否使用回溯算法需要考虑:
在解决这类组合问题时,我个人的经验是:先画出决策树明确递归结构,再考虑如何剪枝优化。对于字符串处理问题,动态规划预处理往往是性能优化的关键。在实际编码时,要特别注意递归终止条件和状态回退的处理,这是最容易出错的地方。