今天我们要深入探讨LeetCode上三个经典回溯算法问题:组合总和(39题)、组合总和II(40题)和分割回文串(131题)。这三个题目虽然属于不同编号,但都围绕着回溯算法的核心思想展开,是面试中高频出现的题型。作为刷题路上的必经关卡,掌握它们不仅能提升算法能力,更能培养解决复杂问题的思维模式。
我在准备算法面试时,这三个题目前后刷了不下十遍,每次都有新的收获。特别是组合总和系列题目,看似简单实则暗藏玄机,而回文串分割则展示了回溯算法的另一种典型应用场景。下面我将结合自己的刷题经验,详细解析这三个问题的解题思路、实现细节和常见陷阱。
给定一个无重复元素的整数数组candidates和一个目标数target,找出candidates中所有可以使数字和为target的组合。candidates中的数字可以无限制重复被选取。
示例:
输入:candidates = [2,3,6,7], target = 7
输出:[[2,2,3],[7]]
这个问题的关键在于:
回溯算法是解决这类组合问题的利器。其核心思想是:通过递归尝试所有可能的组合,当组合的和等于target时保存结果,超过target时则回溯(剪枝)。
python复制def combinationSum(candidates, target):
def backtrack(start, path, remaining):
if remaining == 0:
res.append(path.copy())
return
for i in range(start, len(candidates)):
num = candidates[i]
if num > remaining:
continue # 剪枝
path.append(num)
backtrack(i, path, remaining - num) # 注意这里传入i而不是i+1
path.pop()
res = []
candidates.sort() # 排序有助于剪枝
backtrack(0, [], target)
return res
关键点解析:
start参数确保不会重复之前的组合(避免[2,2,3]和[2,3,2]这样的重复)path.copy()确保保存的是当前状态的副本注意:在Python中直接append(path)会导致后续修改影响已保存的结果,必须使用copy()
最坏情况下时间复杂度为O(N^(target/min)),其中min是candidates中的最小值。这是因为在最坏情况下,每个数字都可能被重复使用多次。
实际应用中常见的变种:
与39题不同,40题有两个关键变化:
示例:
输入:candidates = [10,1,2,7,6,1,5], target = 8
输出:[[1,1,6],[1,2,5],[1,7],[2,6]]
这个变化带来了新的挑战:如何避免重复组合(如[1,2,5]和[2,1,5]被视为相同)?
python复制def combinationSum2(candidates, target):
def backtrack(start, path, remaining):
if remaining == 0:
res.append(path.copy())
return
for i in range(start, len(candidates)):
# 跳过同一层级相同的元素
if i > start and candidates[i] == candidates[i-1]:
continue
num = candidates[i]
if num > remaining:
break # 剪枝
path.append(num)
backtrack(i+1, path, remaining - num) # i+1确保不重复使用
path.pop()
res = []
candidates.sort() # 必须先排序
backtrack(0, [], target)
return res
关键改进点:
i > start确保只在同一层级跳过重复元素(不同层级可以重复)常见错误:直接在循环开始判断
if i > 0 and candidates[i] == candidates[i-1]会错误地跳过所有重复元素
在实际应用中,有两种主要的去重方法:
排序+跳过相邻重复(如上所示)
使用哈希表记录已使用元素
在面试中,第一种方法通常是首选,因为它更符合回溯算法的常规思路。
给定一个字符串s,将s分割成若干子串,使得每个子串都是回文串。返回所有可能的分割方案。
示例:
输入:s = "aab"
输出:[["a","a","b"],["aa","b"]]
这个问题可以看作是一种特殊的组合问题:在字符串的各个位置决定是否分割。回溯算法的框架依然适用,但需要结合回文串的判断。
python复制def partition(s):
def is_palindrome(sub):
return sub == sub[::-1]
def backtrack(start, path):
if start == len(s):
res.append(path.copy())
return
for end in range(start+1, len(s)+1):
substr = s[start:end]
if is_palindrome(substr):
path.append(substr)
backtrack(end, path)
path.pop()
res = []
backtrack(0, [])
return res
优化方向:
预先计算所有子串是否为回文,可以显著提高效率:
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
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()
res = []
backtrack(0, [])
return res
这种预处理将回文判断的时间复杂度从O(n)降到了O(1),整体时间复杂度从O(n*2^n)优化到O(2^n)。
通过这三个题目,我们可以总结出回溯算法的通用模式:
调试建议:
在实际面试中,这些问题可能会有各种变种:
组合总和系列:
回文分割系列:
回溯算法常与其他算法结合使用:
在刷了上百道回溯问题后,我总结了以下几点经验:
对于这三个特定问题,我建议: