Day25-回溯,leetcode491,46,47
- 非递减子序列
-
给你一个整数数组 nums ,找出并返回所有该数组中不同的递增子序列,递增子序列中 至少有两个元素 。你可以按 任意顺序 返回答案。
-
数组中可能含有重复元素,如出现两个整数相等,也可以视作递增序列的一种特殊情况。
-
思路
-
一个元素不能重复使用,startIndex,调整下一层递归的起始位置。
/**
1. 递增性判断
nums[i] < path[path.length - 1]:如果当前数字比上一个小,跳过,保证递增(等于也可以)。
2. 本层去重
uset[nums[i] + 100]:本层递归中,遇到相同数字只选一次,防止重复子序列。
+100 是为了防止负数下标。
. 3递归与回溯
backtracing(i + 1):每次递归从下一个元素开始,保证每个元素最多用一次。
path.pop():回溯,撤销选择。
4. 收集结果
只有当 path.length > 1 时才收集,保证子序列至少有两个元素。
*/
var findSubsequences = function(nums) {
let result = []
let path = []
function backtracing(startIndex) {
if(path.length > 1) {
result.push(path.slice()) // 收集长度大于1的子序列
}
let uset = [] // 本层去重,防止同一层重复选相同数字
for(let i = startIndex; i < nums.length; i++) {
// 1. 保证递增(等于也可以)
// 2. 本层不能选重复数字
if((path.length > 0 && nums[i] < path[path.length - 1]) || uset[nums[i] + 100]) {
continue
}
// uset[nums[i] + 100] 这样写的原因是:为了防止数组下标为负数。
uset[nums[i] + 100] = true // 标记本层已选
path.push(nums[i])
backtracing(i + 1) // 递归,i+1保证每个元素最多用一次
path.pop() // 回溯
}
}
backtracing(0)
return result
};
- 全排列
-
给定一个不含重复数字的数组 nums ,返回其 所有可能的全排列 。你可以 按任意顺序 返回答案。
-
思路
-
排列问题需要一个used数组,标记已经选择的元素
/**
res:存放所有排列结果的二维数组。
path:当前递归路径,表示一个排列。
used:布尔数组,标记每个元素是否已被选用,防止重复选取。
backtracking(n, k, used):
n 是原数组,k 是排列长度(等于 nums.length),used 是标记数组。
当 path.length === k 时,说明收集到一个完整排列,加入结果。
for 循环遍历所有元素,未被使用的元素才可加入 path。
递归后回溯,撤销选择和标记
假设 nums = [1, 2, 3],在递归过程中,used 可能的状态如下:
初始:used = [false, false, false](都没用过)
选了第一个元素 1:used = [true, false, false]
选了 1 和 2:used = [true, true, false]
回溯撤销 2,再选 3:used = [true, false, true]
全部回溯后又选 2:used = [false, true, false]
依此类推...
每次递归时,used[i] 为 true 表示 nums[i] 已经在当前排列中,不能再选。
回溯时要把 used[i] 恢复为 false,以便后续递归可以重新选择。
*/
var permute = function(nums) {
const res = [], path = [];
backtracking(nums, nums.length, []);
return res;
function backtracking(n, k, used) {
if(path.length === k) {
res.push(Array.from(path)); // 收集一个完整排列
return;
}
for (let i = 0; i < k; i++ ) {
// used是一个和nums长度相同的布尔数组(数组元素全是true或false),用来标记每个数字是否已经被选过。
if(used[i]) continue; // 跳过已使用的元素
path.push(n[i]);
used[i] = true; // 同支 // 标记已使用
backtracking(n, k, used);// 递归
path.pop();// 回溯,撤销选择
used[i] = false;// 撤销标记
}
}
};
- 全排列 II
-
给定一个可包含重复数字的序列 nums ,按任意顺序 返回所有不重复的全排列。
-
思路
-
数层去重
-
一般来说:组合问题和排列问题是在树形结构的叶子节点上收集结果,而子集问题就是取树上所有节点的结果。
/**
1. 排序
先对 nums 排序,使得相同元素相邻,便于后续去重。
2. used 数组
used 是一个布尔数组,标记每个元素是否已被选用,防止重复选取。
3. 树层去重
if (i > 0 && nums[i] === nums[i - 1] && !used[i - 1]) continue
只有当前一个相同元素在同一树层未被使用时,才跳过,避免生成重复排列。
4. 回溯过程
每次递归都尝试选择未被使用的元素,递归后撤销选择和标记。
5. 收集结果
当 path.length === nums.length 时,收集一个完整排列。
*/
var permuteUnique = function (nums) {
// 先排序,方便去重
nums.sort((a, b) => {
return a - b
})
let result = []
let path = []
function backtracing( used) {
if (path.length === nums.length) {
result.push([...path]) // 收集一个完整排列
return
}
for (let i = 0; i < nums.length; i++) {
// 树层去重:同一层遇到重复元素只选第一个
if (i > 0 && nums[i] === nums[i - 1] && !used[i - 1]) {
continue
}
if (!used[i]) {
used[i] = true
path.push(nums[i])
backtracing(used)
path.pop()
used[i] = false
}
}
}
backtracing([])
return result
};
参考&感谢各路大神
宝剑锋从磨砺出,梅花香自苦寒来。