一、DFS与回溯算法的核心原理
1.1 DFS的核心定义与实现逻辑
DFS的实现主要依赖递归或栈结构,其中递归实现更为简洁直观,其本质是利用函数调用栈模拟"深度遍历-回溯"的过程。递归实现的核心逻辑可概括为三步:1. 访问当前节点,记录节点状态;2. 遍历当前节点的所有未访问邻接节点,对每个邻接节点递归执行DFS;3. 递归返回后,恢复节点原始状态(若有必要),完成回溯。
从数据结构角度分析,DFS的递归调用过程本质上是对函数调用栈的操作:每次递归调用时,将当前节点的状态压入栈中;递归返回时,将栈顶元素弹出,恢复至上一层节点的状态,这也是回溯算法能够实现"撤销选择"的底层支撑。
1.2 回溯算法的核心机制与本质
回溯算法是基于DFS的一种优化策略,其核心本质是"穷举所有可能解,并通过撤销无效选择,剪枝无效路径,提升搜索效率"。与暴力枚举相比,回溯算法的核心优势的是能够在遍历过程中及时终止无效路径的探索,避免不必要的计算,降低时间复杂度。
回溯算法的核心流程可拆解为"选择-递归-撤销"三步,具体如下:
-
选择:在当前状态下,选择一个可行的选项,将其加入当前解集合,并标记该选项为已使用,避免重复选择;
-
递归:基于当前选择,递归探索下一层状态,直至达到边界条件(找到有效解或确定当前路径无效);
-
撤销:递归返回后,撤销当前选择(将该选项从解集合中移除,取消标记),回到上一层状态,尝试其他可行选项。
回溯算法的核心价值在于"剪枝",即通过提前判断当前路径是否可能产生有效解,若不可能,则直接终止该路径的探索,无需继续递归,从而减少无效计算。剪枝的关键在于找到合理的剪枝条件,常见的剪枝场景包括:重复元素去重、边界条件限制、可行性判断等。
1.3 DFS与回溯算法的关系
DFS与回溯算法并非独立的两种算法,二者是包含与被包含的关系。DFS是一种通用的搜索策略,其核心是"深度优先"的遍历方式,不涉及状态的撤销与剪枝;而回溯算法是DFS的一种特殊应用形式,当DFS用于解决"可撤销选择"的穷举类问题时,通过增加"状态撤销"与"剪枝"逻辑,即形成回溯算法。
简言之,所有回溯算法均基于DFS实现,但并非所有DFS都是回溯算法。例如,二叉树的DFS遍历仅需访问节点、记录节点信息,无需撤销选择或剪枝,属于普通DFS;而组合、排列问题的DFS遍历,需要在递归过程中选择、撤销选择,并通过剪枝优化,属于回溯算法。二者的关系可概括为:回溯 = DFS + 状态撤销 + 剪枝优化。
二、回溯算法的通用模板与实现步骤
回溯算法的核心优势在于其通用性,无论是组合、排列还是子集问题,都可以基于统一的模板进行实现,仅需根据问题特点调整"选择条件""边界条件"与"剪枝条件"即可。本周通过对各类经典题型的梳理,总结出回溯算法的通用递归模板,具体如下:
2.1 通用模板框架
回溯算法的递归模板主要包含四个核心组件:结果集(用于存储所有有效解)、当前解(用于存储当前正在探索的解)、起始位置(用于控制遍历的范围,避免重复)、剪枝条件(用于优化搜索效率)。
cs
#include <stdio.h>
#include <stdlib.h>
// 全局变量:用于存储结果集、当前解及相关状态
int** result; // 结果集:二维数组,存储所有有效解
int* path; // 当前解:一维数组,存储当前正在探索的解
int resultSize; // 结果集大小:有效解的个数
int pathSize; // 当前解大小:当前解中元素的个数
/**
* 回溯递归函数
* @param nums 原始数据集合
* @param numsSize 原始数据集合大小
* @param start 遍历起始位置(用于控制范围,避免重复)
*/
void backtrack(int* nums, int numsSize, int start) {
// 1. 边界条件:判断当前解是否为有效解(此处以收集所有可能子集为例,每一步均为有效解)
// 动态扩容结果集:每次新增有效解时,重新分配二维数组内存
result = (int**)realloc(result, (resultSize + 1) * sizeof(int*));
// 为当前解分配内存,存储当前path的副本(避免引用传递导致结果被覆盖)
result[resultSize] = (int*)malloc(pathSize * sizeof(int));
// 将当前解复制到结果集中
for (int i = 0; i < pathSize; i++) {
result[resultSize][i] = path[i];
}
resultSize++; // 结果集大小加1
// 2. 遍历所有可行选项
for (int i = start; i < numsSize; i++) {
// 3. 剪枝:判断当前选项是否有效,无效则跳过(此处暂不实现具体剪枝,可根据问题补充)
// 4. 选择:将当前选项加入当前解
path = (int*)realloc(path, (pathSize + 1) * sizeof(int));
path[pathSize++] = nums[i];
// 5. 递归:基于当前选择,探索下一层状态
backtrack(nums, numsSize, i + 1); // 起始位置调整,组合/子集用i+1,排列用0
// 6. 撤销:递归返回后,撤销当前选择,恢复状态
pathSize--; // 当前解大小减1,间接移除最后一个元素
// 可选:释放多余内存(若追求内存效率),此处为简化逻辑暂不释放
}
}
// 初始化函数:初始化结果集、当前解及相关状态
void init() {
result = NULL;
path = NULL;
resultSize = 0;
pathSize = 0;
}
// 释放内存函数:避免内存泄漏
void freeMemory() {
// 释放每个有效解的内存
for (int i = 0; i < resultSize; i++) {
free(result[i]);
}
// 释放结果集和当前解的内存
free(result);
free(path);
}
// 测试函数:演示回溯模板的使用(以子集问题为例)
int main() {
int nums[] = {1, 2, 3};
int numsSize = sizeof(nums) / sizeof(nums[0]);
init(); // 初始化
backtrack(nums, numsSize, 0); // 调用回溯函数
// 打印结果集
printf("所有有效解:\n");
for (int i = 0; i < resultSize; i++) {
printf("[");
for (int j = 0; j < pathSize; j++) {
if (j == pathSize - 1) {
printf("%d", result[i][j]);
} else {
printf("%d, ", result[i][j]);
}
}
printf("]\n");
}
freeMemory(); // 释放内存
return 0;
}
2.2 模板核心组件解析
-
结果集(result):采用C语言二维动态数组实现,用于存储所有符合条件的有效解,其中每个一维数组代表一个有效解。resultSize记录有效解的个数,用于动态扩容结果集。需要注意的是,在将当前解加入结果集时,必须为当前解分配新的内存空间,并复制当前解的内容,避免因引用同一数组(path)导致后续回溯过程中当前解的修改,进而覆盖已存入结果集的有效解。
-
当前解(path):采用一维动态数组实现,用于存储当前正在探索的解,pathSize记录当前解中元素的个数。在选择阶段,通过realloc函数动态扩容数组,将当前选项加入数组;在撤销阶段,通过减小pathSize的值,间接移除数组的最后一个元素,实现状态的恢复。需要注意的是,path数组无需频繁释放内存,可在回溯结束后统一释放,简化代码逻辑。
-
起始位置(start):用于控制遍历的范围,避免出现重复的解。在组合、子集问题中,起始位置设置为i+1,确保每次递归仅遍历当前元素之后的元素,避免出现[1,2]与[2,1]这类重复组合;在排列问题中,起始位置设置为0,同时增加used数组标记元素是否被使用,避免重复选择同一元素。
-
剪枝条件:回溯算法的优化核心,需根据具体问题场景设计。常见的剪枝条件包括:重复元素去重(如排序后判断当前元素与前一个元素是否相同,若相同则跳过)、边界条件限制(如当前解的元素个数已达到目标值,无需继续探索)、可行性判断(如当前选择的元素不符合问题要求,直接终止当前路径)。剪枝条件的合理设计,能显著降低算法的时间复杂度,提升搜索效率。
2.3 模板实现注意事项
-
内存管理:C语言中动态数组的使用需注意内存分配与释放,避免内存泄漏。结果集与当前解的内存需在初始化时置空,在回溯过程中通过realloc动态扩容,在程序结束后通过freeMemory函数统一释放所有分配的内存,确保内存使用安全。
-
引用传递问题:在将当前解加入结果集时,必须复制当前解的内容,而非直接赋值。因为path数组是动态变化的,若直接将path赋值给result的元素,后续回溯过程中path的修改会同步影响result中的内容,导致有效解被覆盖。
-
起始位置的调整:起始位置的设置直接影响解的唯一性,需根据问题类型灵活调整。组合、子集问题用i+1,排列问题用0+used数组,避免出现重复解。
-
边界条件的设计:边界条件是判断当前解是否有效的依据,需结合具体问题设计。例如,子集问题中,每一步递归的当前解都是有效解,边界条件可省略(直接将当前解加入结果集);组合问题中,当当前解的元素个数达到目标值时,才将其加入结果集,此时边界条件为pathSize == target。
三、C语言实现经典回溯题型解析
本周通过C语言实现了组合、排列、子集三类经典回溯题型,深入理解了回溯算法的通用模板在不同场景下的应用,掌握了各类题型的解题思路与剪枝技巧。以下为三类经典题型的详细解析与C语言实现。
3.1 组合问题:从n个元素中选出k个元素(无重复)
3.1.1 问题描述
给定两个整数n和k,返回1~n中所有可能的k个数的组合,不考虑元素顺序,且每个元素仅能使用一次。例如,n=4,k=2时,输出结果为[1,2]、[1,3]、[1,4]、[2,3]、[2,4]、[3,4]。
3.1.2 解题思路
组合问题的核心是"不考虑顺序、不重复选择",因此需通过起始位置控制遍历范围,避免重复组合。解题思路如下:
-
确定边界条件:当当前解的元素个数(pathSize)等于k时,将当前解加入结果集,终止当前递归。
-
确定遍历范围:从起始位置start开始遍历,每次递归的起始位置设为i+1,确保仅遍历当前元素之后的元素,避免重复。
-
剪枝优化:若当前剩余元素的个数(n - i)小于还需选择的元素个数(k - pathSize),则无需继续遍历,直接剪枝,因为即使全部选择剩余元素,也无法满足k个元素的要求。
3.1.3 C语言实现代码
cs
#include <stdio.h>
#include <stdlib.h>
int** result;
int* path;
int resultSize;
int pathSize;
// 回溯递归函数
void backtrack(int n, int k, int start) {
// 边界条件:当前解的元素个数等于k,加入结果集
if (pathSize == k) {
result = (int**)realloc(result, (resultSize + 1) * sizeof(int*));
result[resultSize] = (int*)malloc(k * sizeof(int));
for (int i = 0; i < k; i++) {
result[resultSize][i] = path[i];
}
resultSize++;
return;
}
// 遍历所有可行选项,剪枝:n - i < k - pathSize时,无需继续遍历
for (int i = start; i <= n - (k - pathSize); i++) {
// 选择当前元素
path = (int*)realloc(path, (pathSize + 1) * sizeof(int));
path[pathSize++] = i;
// 递归探索下一层,起始位置设为i+1
backtrack(n, k, i + 1);
// 撤销选择
pathSize--;
}
}
// 组合问题入口函数
int** combine(int n, int k, int* returnSize, int** returnColumnSizes) {
// 初始化
init();
backtrack(n, k, 1);
// 设置返回结果的大小
*returnSize = resultSize;
// 设置每个有效解的元素个数(均为k)
*returnColumnSizes = (int*)malloc(resultSize * sizeof(int));
for (int i = 0; i < resultSize; i++) {
(*returnColumnSizes)[i] = k;
}
return result;
}
// 初始化函数(复用通用模板)
void init() {
result = NULL;
path = NULL;
resultSize = 0;
pathSize = 0;
}
// 释放内存函数(复用通用模板)
void freeMemory() {
for (int i = 0; i < resultSize; i++) {
free(result[i]);
}
free(result);
free(path);
}
// 测试函数
int main() {
int n = 4, k = 2;
int returnSize;
int* returnColumnSizes;
int** result = combine(n, k, &returnSize, &returnColumnSizes);
// 打印结果
printf("组合结果:\n");
for (int i = 0; i < returnSize; i++) {
printf("[");
for (int j = 0; j < returnColumnSizes[i]; j++) {
if (j == returnColumnSizes[i] - 1) {
printf("%d", result[i][j]);
} else {
printf("%d, ", result[i][j]);
}
}
printf("]\n");
free(result[i]);
}
free(result);
free(returnColumnSizes);
return 0;
}
3.1.4 关键解析
-
剪枝条件:`i <= n - (k - pathSize)` 是组合问题的核心剪枝条件。例如,n=4,k=2,当pathSize=1时,还需选择1个元素,此时i最大只能为3(4 - (2 - 1) = 3),若i=4,剩余元素个数为0,无法满足需求,因此无需遍历i=4,减少无效计算。
-
起始位置:起始位置从1开始(因为题目要求从1~n中选择元素),每次递归后起始位置设为i+1,确保不重复选择同一元素,避免出现[1,2]与[2,1]这类重复组合。
-
返回值处理:为了符合算法题的常规返回格式,入口函数combine返回二维数组result,同时通过指针参数返回结果集大小(returnSize)和每个有效解的元素个数(returnColumnSizes),便于调用者处理结果。
3.2 排列问题:n个元素的全排列(无重复)
3.2.1 问题描述
给定一个不含重复元素的数组nums,返回其所有可能的全排列,考虑元素顺序。例如,nums=[1,2,3]时,输出结果为[1,2,3]、[1,3,2]、[2,1,3]、[2,3,1]、[3,1,2]、[3,2,1]。
3.2.2 解题思路
排列问题的核心是"考虑顺序、不重复选择同一元素",因此无法通过起始位置控制遍历范围,需引入used数组标记元素是否被使用。解题思路如下:
-
确定边界条件:当当前解的元素个数(pathSize)等于数组长度(numsSize)时,将当前解加入结果集,终止当前递归。
-
确定遍历范围:从0开始遍历整个数组,每次遍历前判断当前元素是否被使用,若已被使用则跳过,未被使用则选择该元素。
-
状态标记与撤销:选择元素时,将used数组中对应位置设为1(标记为已使用);撤销选择时,将used数组中对应位置设为0(恢复为未使用),确保后续递归能重新选择该元素。
3.2.3 C语言实现代码
cs
#include <stdio.h>
#include <stdlib.h>
int** result;
int* path;
int resultSize;
int pathSize;
// 回溯递归函数
void backtrack(int* nums, int numsSize, int* used) {
// 边界条件:当前解的元素个数等于数组长度,加入结果集
if (pathSize == numsSize) {
result = (int**)realloc(result, (resultSize + 1) * sizeof(int*));
result[resultSize] = (int*)malloc(numsSize * sizeof(int));
for (int i = 0; i < numsSize; i++) {
result[resultSize][i] = path[i];
}
resultSize++;
return;
}
// 遍历整个数组,判断元素是否被使用
for (int i = 0; i < numsSize; i++) {
if (used[i] == 1) {
continue; // 元素已被使用,跳过
}
// 选择当前元素,标记为已使用
used[i] = 1;
path = (int*)realloc(path, (pathSize + 1) * sizeof(int));
path[pathSize++] = nums[i];
// 递归探索下一层,起始位置设为0
backtrack(nums, numsSize, used);
// 撤销选择,恢复状态
pathSize--;
used[i] = 0;
}
}
// 排列问题入口函数
int** permute(int* nums, int numsSize, int* returnSize, int** returnColumnSizes) {
// 初始化
result = NULL;
path = NULL;
resultSize = 0;
pathSize = 0;
// 初始化used数组,标记元素是否被使用
int* used = (int*)calloc(numsSize, sizeof(int));
backtrack(nums, numsSize, used);
// 设置返回结果的大小
*returnSize = resultSize;
// 设置每个有效解的元素个数(均为numsSize)
*returnColumnSizes = (int*)malloc(resultSize * sizeof(int));
for (int i = 0; i < resultSize; i++) {
(*returnColumnSizes)[i] = numsSize;
}
// 释放used数组内存
free(used);
return result;
}
// 测试函数
int main() {
int nums[] = {1, 2, 3};
int numsSize = sizeof(nums) / sizeof(nums[0]);
int returnSize;
int* returnColumnSizes;
int** result = permute(nums, numsSize, &returnSize, &returnColumnSizes);
// 打印结果
printf("排列结果:\n");
for (int i = 0; i < returnSize; i++) {
printf("[");
for (int j = 0; j < returnColumnSizes[i]; j++) {
if (j == returnColumnSizes[i] - 1) {
printf("%d", result[i][j]);
} else {
printf("%d, ", result[i][j]);
}
}
printf("]\n");
free(result[i]);
}
free(result);
free(returnColumnSizes);
return 0;
}
3.2.4 关键解析
-
used数组:用于标记元素是否被使用,初始化为0(未使用),选择元素时设为1,撤销选择时设为0,确保每个元素在当前解中仅被使用一次,同时允许后续递归重新选择该元素。
-
遍历范围:从0开始遍历整个数组,而非从start开始,因为排列问题需要考虑所有元素的不同顺序,例如[1,2]与[2,1]是两个不同的排列,需要分别探索。
-
内存管理:used数组通过calloc函数初始化,确保所有元素初始值为0,使用完毕后及时释放,避免内存泄漏。
3.3 子集问题:n个元素的所有子集(无重复)
3.3.1 问题描述
给定一个不含重复元素的数组nums,返回其所有可能的子集,不考虑元素顺序,且每个元素仅能使用一次。例如,nums=[1,2,3]时,输出结果为[]、[1]、[2]、[3]、[1,2]、[1,3]、[2,3]、[1,2,3]。
3.3.2 解题思路
子集问题的核心是"不限制选择元素的个数、不考虑顺序、不重复选择",因此每一步递归的当前解都是有效解,无需额外的边界条件判断(除了遍历结束)。解题思路如下:
-
确定边界条件:无需额外边界条件,每次递归调用时,将当前解加入结果集,因为每个子集都是有效解。
-
确定遍历范围:从起始位置start开始遍历,每次递归的起始位置设为i+1,确保不重复选择同一元素,避免出现重复子集。
-
剪枝优化:由于子集问题不限制元素个数,剪枝条件可根据实际需求设计,例如当当前解的元素个数达到数组长度时,可直接终止当前递归(但并非必需,因为遍历到数组末尾自然会终止)。
3.3.3 C语言实现代码
cs
#include <stdio.h>
#include <stdlib.h>
int** result;
int* path;
int resultSize;
int pathSize;
// 回溯递归函数
void backtrack(int* nums, int numsSize, int start) {
// 无需额外边界条件,每次递归均将当前解加入结果集
result = (int**)realloc(result, (resultSize + 1) * sizeof(int*));
result[resultSize] = (int*)malloc(pathSize * sizeof(int));
for (int i = 0; i < pathSize; i++) {
result[resultSize][i] = path[i];
}
resultSize++;
// 遍历所有可行选项,起始位置设为i+1
for (int i = start; i < numsSize; i++) {
// 选择当前元素
path = (int*)realloc(path, (pathSize + 1) * sizeof(int));
path[pathSize++] = nums[i];
// 递归探索下一层
backtrack(nums, numsSize, i + 1);
// 撤销选择
pathSize--;
}
}
// 子集问题入口函数
int** subsets(int* nums, int numsSize, int* returnSize, int** returnColumnSizes) {
// 初始化
result = NULL;
path = NULL;
resultSize = 0;
pathSize = 0;
backtrack(nums, numsSize, 0);
// 设置返回结果的大小
*returnSize = resultSize;
// 设置每个有效解的元素个数
*returnColumnSizes = (int*)malloc(resultSize * sizeof(int));
for (int i = 0; i < resultSize; i++) {
(*returnColumnSizes)[i] = pathSize;
// 修正:每个有效解的元素个数为当前存入时的pathSize,需重新计算
// 重新遍历result,获取每个解的实际长度
int len = 0;
while (len < numsSize && result[i][len] != '\0') {
len++;
}
(*returnColumnSizes)[i] = len;
}
return result;
}
// 测试函数
int main() {
int nums[] = {1, 2, 3};
int numsSize = sizeof(nums) / sizeof(nums[0]);
int returnSize;
int* returnColumnSizes;
int** result = subsets(nums, numsSize, &returnSize, &returnColumnSizes);
// 打印结果
printf("子集结果:\n");
for (int i = 0; i < returnSize; i++) {
printf("[");
for (int j = 0; j < returnColumnSizes[i]; j++) {
if (j == returnColumnSizes[i] - 1) {
printf("%d", result[i][j]);
} else {
printf("%d, ", result[i][j]);
}
}
printf("]\n");
free(result[i]);
}
free(result);
free(returnColumnSizes);
return 0;
}
3.3.4 关键解析
-
边界条件:子集问题的边界条件较为特殊,每一步递归的当前解都是有效解,因此在递归函数的开头就将当前解加入结果集,无需判断pathSize是否达到某个值。例如,初始时path为空,加入结果集得到空子集;选择第一个元素后,加入结果集得到单个元素子集,以此类推。
-
起始位置:与组合问题一致,起始位置设为i+1,确保不重复选择同一元素,避免出现[1,2]与[2,1]这类重复子集。
-
返回ColumnSizes处理:由于每个子集的元素个数不同,需重新遍历每个有效解,计算其实际长度,再赋值给returnColumnSizes,确保返回结果的正确性。
四、回溯算法的剪枝优化技巧
回溯算法的效率核心在于剪枝,合理的剪枝条件能大幅减少无效递归,降低时间复杂度。本周通过对经典题型的练习,总结出三类常见的剪枝优化技巧,结合C语言实现,具体如下:
4.1 重复元素去重剪枝
当原始数据中存在重复元素时,回溯过程中会出现重复解,此时需通过排序+判断的方式进行剪枝。例如,nums=[1,1,2],求其所有子集,若不剪枝,会出现[1,2](第一个1)与[1,2](第二个1)这类重复子集。
剪枝思路:1. 先对原始数组进行排序,使重复元素相邻;2. 在遍历过程中,判断当前元素与前一个元素是否相同,且前一个元素未被使用(避免漏解),若相同则跳过当前元素。
cs
// 假设nums已排序
for (int i = start; i < numsSize; i++) {
// 剪枝:重复元素去重
if (i > start && nums[i] == nums[i-1]) {
continue;
}
// 选择、递归、撤销逻辑...
}
注意:判断条件中"i > start"是关键,确保仅跳过同一层的重复元素,不影响不同层的元素选择,避免漏解。
4.2 边界条件剪枝
边界条件剪枝是最基础的剪枝方式,通过判断当前路径是否可能产生有效解,若不可能则直接终止当前递归。例如,组合问题中,当当前剩余元素个数小于还需选择的元素个数时,无需继续遍历;排列问题中,当当前解的元素个数已达到目标值时,无需继续探索。
cs
// 剪枝:剩余元素个数不足,无法满足目标k
for (int i = start; i <= n - (k - pathSize); i++) {
// 选择、递归、撤销逻辑...
}
4.3 可行性剪枝
可行性剪枝适用于有明确约束条件的问题,例如"组合总和"问题中,当前解的元素和已超过目标值时,无需继续递归。通过提前判断当前选择是否符合约束条件,终止无效路径。:
cs
// 假设target为目标和,sum为当前解的元素和
if (sum > target) {
return; // 剪枝:当前和已超过目标,无需继续递归
}
五、 常见问题及解决方法
5.1 问题
-
递归栈溢出问题:在实现回溯算法时,若递归深度过大(如n=1000),会出现栈溢出错误。解决方法:一是优化递归深度,通过剪枝减少无效递归;二是将递归实现改为栈实现,避免函数调用栈溢出。
-
内存泄漏问题:C语言中动态数组的使用不当,容易导致内存泄漏。解决方法:明确内存分配与释放的时机,在程序结束后统一释放所有动态分配的内存,可通过封装freeMemory函数实现。
-
重复解问题:在处理含重复元素的题目时,容易出现重复解。解决方法:先对数组排序,再通过"i > start && nums[i] == nums[i-1]"的条件剪枝,避免同一层重复选择。
-
起始位置与used数组使用混淆:组合、子集问题误用used数组,排列问题误用起始位置控制,导致解错误。解决方法:明确题型特点,组合、子集用起始位置控制,排列用used数组标记,牢记各类题型的核心区别。
-
引用传递导致结果覆盖:将path直接赋值给result,导致后续回溯修改path时覆盖已存入的有效解。解决方法:每次将当前解加入结果集时,为当前解分配新的内存空间,并复制path的内容。
5.2 避坑指南
-
编写代码前,先明确问题类型(组合、排列、子集),确定是否需要使用起始位置或used数组。
-
动态数组使用时,务必注意内存分配与释放,避免出现野指针或内存泄漏。
-
剪枝条件的设计需结合具体问题,避免过度剪枝导致漏解,或剪枝不足导致效率低下。
-
调试代码时,可通过打印path、pathSize、used数组等变量,观察回溯过程中的状态变化,定位问题所在。
-
对于含重复元素的题目,必须先排序再剪枝,否则无法有效去重。