1.题目详情
给定一个无重复元素的数组 candidates 和一个目标数 target ,找出 candidates 中所有可以使数字和为 target 的组合。
candidates 中的数字可以无限制重复被选取。
说明:
所有数字(包括 target)都是正整数。
解集不能包含重复的组合。
2.解题思路
没什么好的思路,只能通过部分测试用例,看了题解
完整题解
回溯算法+剪枝解答
3.代码实现
class Solution:
def combinationSum(self, candidates: List[int], target: int) -> List[List[int]]:
candidates = sorted(candidates)
ans = []
def find(s, use, remain):
for i in range(s, len(candidates)):
c = candidates[i]
if c == remain:
ans.append(use + [c])
if c < remain:
find(i, use + [c], remain - c)
if c > remain:
return
find(0, [], target)
return ans
4.知识点
-
回溯算法
- 从一个结点开始往下走,一旦遇到该分布不能满足条件,往上退一个甚至多个结点,再通过去往别的分支找寻正确答案。回退
- 回溯法 采用试错的思想,它尝试分步的去解决一个问题。在分步解决问题的过程中,当它通过尝试发现现有的分步答案不能得到有效的正确的解答的时候,它将取消上一步甚至是上几步的计算,再通过其它的可能的分步解答再次尝试寻找问题的答案。回溯法通常用最简单的递归方法来实现,在反复重复上述的步骤后可能出现两种情况:
- 找到一个可能存在的正确的答案;
- 在尝试了所有可能的分步方法后宣告该问题没有答案。
-
深度优先算法
- 深度优先搜索 算法(英语:Depth-First-Search,DFS)是一种用于遍历或搜索树或图的算法。这个算法会 尽可能深 的搜索树的分支。当结点
v
的所在边都己被探寻过,搜索将 回溯 到发现结点v
的那条边的起始结点。这一过程一直进行到已发现从源结点可达的所有结点为止。如果还存在未被发现的结点,则选择其中一个作为源结点并重复以上过程,整个进程反复进行直到所有结点都被访问为止。
- 深度优先搜索 算法(英语:Depth-First-Search,DFS)是一种用于遍历或搜索树或图的算法。这个算法会 尽可能深 的搜索树的分支。当结点
-
「回溯算法」与「深度优先遍历」都有「不撞南墙不回头」的意思。我个人的理解是:「回溯算法」强调了「深度优先遍历」思想的用途,用一个 不断变化 的变量,在尝试各种可能的过程中,搜索需要的结果。强调了 回退 操作对于搜索的合理性。
-
与动态规划的区别
共同点
用于求解多阶段决策问题。多阶段决策问题即:
- 求解一个问题分为很多步骤(阶段);
- 每一个步骤(阶段)可以有多种选择。
不同点
- 动态规划只需要求我们评估最优解是多少,最优解对应的具体解是什么并不要求。因此很适合应用于评估一个方案的效果;
- 回溯算法可以搜索得到所有的方案(当然包括最优解),但是本质上它是一种遍历算法,时间复杂度很高。
从全排列问题开始理解回溯算法
我们尝试在纸上写 33 个数字、44 个数字、55 个数字的全排列,相信不难找到这样的方法。以数组
[1, 2, 3]
的全排列为例。- 先写以 11 开头的全排列,它们是:
[1, 2, 3], [1, 3, 2]
,即1
+[2, 3]
的全排列(注意:递归结构体现在这里); - 再写以 22 开头的全排列,它们是:
[2, 1, 3], [2, 3, 1]
,即2
+[1, 3]
的全排列; - 最后写以 33 开头的全排列,它们是:
[3, 1, 2], [3, 2, 1]
,即3
+[1, 2]
的全排列。
总结搜索的方法:按顺序枚举每一位可能出现的情况,已经选择的数字在 当前 要选择的数字中不能出现。按照这种策略搜索就能够做到 不重不漏。这样的思路,可以用一个树形结构表示。
看到这里的朋友,建议先尝试自己画出「全排列」问题的树形结构。
说明:
- 每一个结点表示了求解全排列问题的不同的阶段,这些阶段通过变量的「不同的值」体现,这些变量的不同的值,称之为「状态」;
- 使用深度优先遍历有「回头」的过程,在「回头」以后, 状态变量需要设置成为和先前一样 ,因此在回到上一层结点的过程中,需要撤销上一次的选择,这个操作称之为「状态重置」;
- 深度优先遍历,借助系统栈空间,保存所需要的状态变量,在编码中只需要注意遍历到相应的结点的时候,状态变量的值是正确的,具体的做法是:往下走一层的时候,
path
变量在尾部追加,而往回走的时候,需要撤销上一次的选择,也是在尾部操作,因此path
变量是一个栈; - 深度优先遍历通过「回溯」操作,实现了全局使用一份状态变量的效果。
使用编程的方法得到全排列,就是在这样的一个树形结构中完成 遍历,从树的根结点到叶子结点形成的路径就是其中一个全排列。