
深度优先搜索(DFS)和回溯法是解决复杂问题中不可或缺的算法工具,尤其在组合问题(如全排列、子集等)中,发挥着至关重要的作用。通过递归的方式,DFS 能够遍历问题的解空间,而回溯法则通过撤销不合法的选择,避免重复计算,提高效率。在解题过程中,剪枝是优化回溯法的重要手段,它通过提前排除无效路径,进一步减少了运算的复杂度。本文将深入探讨如何使用 DFS、回溯法及剪枝技术,构建解决全排列和子集问题的决策树,并优化算法的执行效率。
题目链接:https://leetcode.cn/problems/permutations/description/
这段代码实现了生成一个数组的所有排列(Permutation),并返回所有可能的排列列表。采用了**深度优先搜索(DFS)**的方式,配合一个 check 数组来记录哪些元素已经使用过,从而避免重复使用。
vector<vector<int>> ret:存储最终结果,包含所有排列。vector<int> path:存储当前正在构造的排列。bool check[7]:标记数组,记录某个位置是否已经被使用,避免重复选择。默认全为 false。permute: dfs(nums) 开始进行递归遍历,寻找所有排列。ret。dfs: path 的长度等于 nums.size(),说明已经构造出一个完整的排列,将其加入结果集 ret。check[i] == false)。path,同时更新 check[i] = true。dfs(nums),继续构造排列。path.pop_back()。check[i] = false,允许后续尝试其他路径。如图分析:

class Solution {
public:
vector<vector<int>> ret;
vector<int> path;
bool check[7];
vector<vector<int>> permute(vector<int>& nums) {
dfs(nums);
return ret;
}
void dfs(vector<int>& nums) {
if (path.size() == nums.size()) {
ret.push_back(path);
return;
}
for (int i = 0; i < nums.size(); i++) {
if (check[i] == false) {
path.push_back(nums[i]);
check[i] = true;
dfs(nums);
// -> ֳָ
path.pop_back();
check[i] = false;
}
}
}
};nums 的所有元素,同时需要递归构造排列。 ,其中 n 为 nums 的长度。
的时间复杂度,因此总时间复杂度为:
,对应递归栈的最大深度。
ret 大小为 (每个排列长度为 n,共有 n! 个排列)。
题目链接:https://leetcode.cn/problems/subsets/description/
核心思想:对于每个位置,都需要明确“选”或者“不选”两种可能,然后继续对后续位置进行递归。
nums = [1, 2, 3]。pos 达到数组末尾(pos == nums.size())时,将当前路径 path 加入结果集 ret。dfs(nums, 0) path = [1],递归进入 dfs(nums, 1) path = [1, 2],递归进入 dfs(nums, 2) path = [1, 2, 3],递归终止,保存结果 [[1, 2, 3]]path = [1, 2]path = [1, 2],递归终止,保存结果 [[1, 2, 3], [1, 2]]path = [1]path = [1],递归进入 dfs(nums, 2) path = [1, 3],递归终止,保存结果 [[1, 2, 3], [1, 2], [1, 3]]path = [1],递归终止,保存结果 [[1, 2, 3], [1, 2], [1, 3], [1]]path = []path = [],递归进入 dfs(nums, 1) path = [2],递归类似上面。path = [],递归继续处理后续。最终得到所有子集。
如图分析:

class Solution {
public:
vector<vector<int>> ret;
vector<int> path;
vector<vector<int>> subsets(vector<int>& nums) {
dfs(nums, 0);
return ret;
}
void dfs(vector<int>& nums, int pos) {
if (pos == nums.size()) {
ret.push_back(path);
return;
}
// 选
path.push_back(nums[pos]);
dfs(nums, pos + 1);
// 回溯 -> 恢复现场
path.pop_back();
// 不选
dfs(nums, pos + 1);
}
};核心思想:直接将当前路径加入结果集,然后从当前位置逐个向后选取元素递归。
nums = [1, 2, 3]。pos 开始,依次向后选择元素,尝试递归。ret = [[]]pos = 0 开始: path = [1],加入结果集:ret = [[], [1]],递归进入 dfs(nums, 1) path = [1, 2],加入结果集:ret = [[], [1], [1, 2]],递归进入 dfs(nums, 2) path = [1, 2, 3],加入结果集:ret = [[], [1], [1, 2], [1, 2, 3]]path = [1, 2]path = [1]path = []如图分析:

class Solution {
public:
vector<vector<int>> ret;
vector<int> path;
vector<vector<int>> subsets(vector<int>& nums) {
dfs(nums, 0);
return ret;
}
void dfs(vector<int>& nums, int pos) {
ret.push_back(path);
for (int i = pos; i < nums.size(); i++) {
path.push_back(nums[i]);
dfs(nums, i + 1);
// 回溯 -> 恢复现场
path.pop_back();
}
}
};path 加入结果,需要 O(n) 的时间(复制 path 的内容)。总时间复杂度:
path 加入结果集 ret。path)。总时间复杂度: O(2n⋅n)
。
path 需要存储当前路径,大小为 。
ret 包含个子集,每个子集的平均长度为
,因此结果集的存储空间为:
总空间复杂度:
(递归栈和路径空间较小,可忽略不计)
。
path 需要存储当前路径,大小为 。
总空间复杂度:
(递归栈和路径空间较小,可忽略不计)
题目链接:https://leetcode.cn/problems/sum-of-all-subset-xor-totals/description/
题目要求求出数组中所有子集的异或和的总和。解法采用 回溯法 (Backtracking),枚举所有子集,同时计算每个子集的异或和,并将其累加到结果中。
ret:最终结果,用于存储所有子集的异或和的累加值。path:当前路径变量,表示当前子集的异或值。nums:输入数组。递归函数 dfs(nums, pos):
pos 为起点的所有可能子集。ret。ret: 每次递归进入时,将当前路径的异或值(path)加入到结果中。pos 开始,逐一尝试将后续元素加入路径 path,并递归处理。path 的值)。pos 超出数组范围时,循环自动结束。在主函数 subsetXORSum 中:
ret = 0。dfs(nums, 0),从数组的第 0 个位置开始递归。ret。如图分析:

class Solution {
public:
int ret = 0;
int path = 0;
int subsetXORSum(vector<int>& nums) {
dfs(nums, 0);
return ret;
}
void dfs(vector<int>& nums, int pos){
ret += path;
for(int i = pos; i < nums.size(); i++){
path ^= nums[i];
dfs(nums, i + 1);
// 回溯
path ^= nums[i];
}
}
};,共有
个子集。
。
。
path 是一个整数,占用 的空间。
题目链接:https://leetcode.cn/problems/permutations-ii/description/
利用 回溯法 (Backtracking) 生成所有排列,通过排序和剪枝(跳过重复元素)来避免生成重复排列。
ret:存储所有结果的二维数组。path:当前路径,用于构造一个排列。check:布尔数组,记录每个元素是否已经被使用过。nums 进行排序:sort(nums.begin(), nums.end())。 递归函数 dfs(nums, pos):
path 的大小等于数组 nums 的长度,说明构造了一组完整排列,将其加入结果集 ret。nums 中的每个元素,尝试将未被使用的元素加入当前路径。check[i] == false 判断当前元素是否可用。nums[i] == nums[i - 1]:当前元素与前一个元素相同。check[i - 1] == false:前一个相同的元素尚未被使用,说明该排列已经处理过,直接跳过当前分支。在主函数 permuteUnique 中:
sort(nums.begin(), nums.end())。check 的大小为 nums.size(),初始值为 false。dfs(nums, 0)。ret。如图分析:

class Solution {
public:
vector<vector<int>> ret;
vector<int> path;
bool check[9];
vector<vector<int>> permuteUnique(vector<int>& nums) {
// 先排序
sort(nums.begin(), nums.end());
dfs(nums, 0);
return ret;
}
void dfs(vector<int>& nums, int pos) {
if (path.size() == nums.size()) {
ret.push_back(path);
return;
}
for (int i = 0; i < nums.size(); i++) {
// 剪枝(只关心合法分支)
if (check[i] == false && (i == 0 || nums[i] != nums[i - 1] || check[i - 1] == true)) {
path.push_back(nums[i]);
check[i] = true;
dfs(nums, pos + 1);
// 回溯
path.pop_back();
check[i] = false;
}
}
},排列数量为
。
个排列。
总时间复杂度:
。
path 和布尔数组 check 的大小为 。
总空间复杂度:
回溯法与深度优先搜索(DFS)结合,能有效地解决多种组合问题。通过决策树的结构,回溯法逐步探索解空间,而剪枝则通过减少不必要的计算,显著提高算法的效率。针对特定问题的剪枝优化,可以进一步提升回溯法的性能。
希望通过本文的讲解,您能深入理解回溯法与DFS在解决组合问题中的应用,并通过剪枝技术优化算法效率。如果您对回溯算法或其他算法问题有任何疑问,欢迎交流与讨论!
今天的分享到这里就结束啦!如果觉得文章还不错的话,可以三连支持一下,17的主页还有很多有趣的文章,欢迎小伙伴们前去点评,您的支持就是17前进的动力!