D23-回溯,leetcode39,40,131
- 组合总和
-
给你一个 无重复元素 的整数数组 candidates 和一个目标整数 target ,找出 candidates 中可以使数字和为目标数 target 的 所有 不同组合 ,并以列表形式返回。你可以按 任意顺序 返回这些组合。
-
candidates 中的 同一个 数字可以 无限制重复被选取 。如果至少一个数字的被选数量不同,则两种组合是不同的。
-
对于给定的输入,保证和为 target 的不同组合数少于 150 个。
-
思路
var combinationSum = function(candidates, target) {
const res = [], path = [];
candidates.sort((a,b)=>a-b); // 排序,方便剪枝
backtracking(0, 0);
return res;
function backtracking(j, sum) {
if (sum === target) {
/* Array.from(path) 的作用是创建 path 的一个浅拷贝数组。
在回溯算法中,path 是用来记录当前递归路径(即当前组合)的。
由于 path 数组会不断 push 和 pop(回溯),如果直接 res.push(path),保存的是同一个引用,后续 path 变化会影响结果。
用 Array.from(path) 或 path.slice() 可以拷贝当前 path 的内容,把当前组合的快照保存到结果数组中,后续 path 的变化不会影响已保存的结果。
res.push([...path]);
res.push(path.slice());
res.push(Array.from(path));
*/
res.push(Array.from(path));// 找到一个组合,加入结果
return;
}
for(let i = j; i < candidates.length; i++ ) {
const n = candidates[i];
if(n > target - sum) break; // 剪枝:后面都比target大,直接退出
path.push(n); // 选择当前数字
sum += n;
backtracking(i, sum); // 递归,i而不是i+1,允许重复选取当前数字
path.pop(); // 回溯,撤销选择
sum -= n;
}
}
};
- 组合总和 II
-
给定一个候选人编号的集合 candidates 和一个目标数 target ,找出 candidates 中所有可以使数字和为 target 的组合。
-
candidates 中的每个数字在每个组合中只能使用 一次 。
-
注意:解集不能包含重复的组合。
-
思路
-
数组排序,横向,树层去重,相邻元素相同,前面取过了,后面就不用取了,取了组成的组合也是前面组合过的,重复了
-
定义一个数组used,用来记录同一树枝上的元素是否使用过,方便于集合去重
-
如果candidates[i] == candidates[i - 1] 并且 used[i - 1] == false,就说明:前一个树枝,使用了candidates[i - 1],也就是说同一树层使用过candidates[i - 1]。
/**
1. 排序:先排序,方便后续剪枝和去重。
2. 树层去重:if(j > i && candidates[j] === candidates[j-1]) continue;
保证同一层(横向)不选重复数字,避免结果重复。
3. 剪枝:if(n > target - sum) break;
当前数已经大于剩余目标值,后面更大,直接 break。
4. 递归参数:backtracking(sum, j + 1)
每个数字只能用一次,所以递归下标是 j + 1。
5. 回溯:递归后撤销选择,恢复现场。
*/
var combinationSum2 = function(candidates, target) {
const res = []; path = [], len = candidates.length;
candidates.sort((a,b)=>a-b);// 排序,方便去重和剪枝
backtracking(0, 0);
return res;
function backtracking(sum, i) {
if (sum === target) {
res.push(Array.from(path));// 找到一个组合,加入结果
return;
}
for(let j = i; j < len; j++) {
const n = candidates[j];
// 树层去重:同一层遇到重复元素直接跳过,避免重复组合
if(j > i && candidates[j] === candidates[j-1]){
//若当前元素和前一个元素相等
//则本次循环结束,防止出现重复组合
continue;
}
//// 剪枝:当前元素大于剩余目标值,后面更大,直接退出
//由于数组已排序,那么该元素之后的元素必定不满足条件
//直接终止当前层的递归
if(n > target - sum) break;
path.push(n);// 选择当前数字
sum += n;
backtracking(sum, j + 1);// 递归到下一个位置(每个数只能用一次)
path.pop();// 回溯,撤销选择
sum -= n;
}
}
};
/**
1. 排序:保证相同数字相邻,便于去重和剪枝。
2. 树层去重:(i > 0 && cur === candidates[i - 1] && !used[i - 1])
保证同一层(横向)不选重复数字,避免结果重复。
只有当前一个同值元素在同一树枝未被使用时,才跳过当前元素。
3. 剪枝:cur > target - total,当前数已经大于剩余目标值,后面更大,直接跳过。
4. used 数组:用于区分树层和树枝,防止重复组合。在同一树层(横向)遇到重复元素时,判断前一个相同元素是否在本层被用过,从而避免同一层出现重复组合。
5. 回溯:递归后撤销选择,恢复现场。
*/
var combinationSum2 = function(candidates, target) {
// res:存放所有满足条件的组合结果。
// path:当前递归路径(正在拼接的组合)。
// total:当前组合的和。
// used:布尔数组,记录每个元素是否在当前递归路径中被使用过(用于树层去重)。
// candidates.sort(...):先排序,方便后续剪枝和去重。
let res = [];
let path = [];
let total = 0;
const len = candidates.length;
candidates.sort((a, b) => a - b);
let used = new Array(len).fill(false);
const backtracking = (startIndex) => {
if (total === target) {
res.push([...path]); // 找到一个组合,加入结果
return;
}
for(let i = startIndex; i < len && total < target; i++) {
const cur = candidates[i];
// 剪枝:当前数大于剩余目标值,直接跳过
// 树层去重:同一层遇到重复元素且前一个没用过,跳过
if (cur > target - total || (i > 0 && cur === candidates[i - 1] && !used[i - 1])) continue;
path.push(cur);
total += cur;
used[i] = true;
// 每个数只能用一次,递归下标是 i+1
backtracking(i + 1);
path.pop();
total -= cur;
used[i] = false;
}
}
backtracking(0);
return res;
};
- 分割回文串
-
给你一个字符串 s,请你将 s 分割成一些 子串,使每个子串都是 回文串 。返回 s 所有可能的分割方案。
-
思路
-
判断一个字符串是否是回文。
-
可以使用双指针法,一个指针从前向后,一个指针从后向前,如果前后指针所指向的元素是相等的,就是回文字符串了。
// 判断是否是回文串,双指针,前后一起移动
const isPalindrome = (s, l, r) => {
// 用双指针从两端向中间比较字符,只要有一对不相等就返回 false。
for (let i = l, j = r; i < j; i++, j--) {
if(s[i] !== s[j]) return false;
}
return true;
}
var partition = function(s) {
// res:存放所有分割方案。
// path:当前递归路径(正在拼接的分割子串)。
// backtracking(startIndex):从 startIndex 开始,尝试所有可能的分割。
const res = [], path = [], len = s.length;
backtracking(0);
return res;
/**回溯过程
1. 终止条件
如果 startIndex >= len,说明已经分割到字符串末尾,把当前 path 加入结果。
2. 枚举分割点
枚举区间 [startIndex, i],判断 s[startIndex...i] 是否为回文串。
如果是回文串,就把这个子串加入 path,递归处理剩下的部分。
回溯时撤销选择(path.pop())
*/
function backtracking(startIndex) {
if(startIndex >= len) {
res.push(Array.from(path));
return;
}
for(let i = startIndex; i < len; i++) {
if(!isPalindrome(s, startIndex, i)) continue;
path.push(s.slice(startIndex, i + 1));
backtracking(i + 1);
path.pop();
}
}
};
参考&感谢各路大神
宝剑锋从磨砺出,梅花香自苦寒来。

浙公网安备 33010602011771号