回溯思想
回溯的本质就是枚举每一个可能的解,枚举的过程与dfs的路线是一致的,因此最重要的一步就是将问题求解过程抽象出一颗决策树,并给出每个结点上所需要的状态,以及变化。
当一个问题只能通过枚举解决时,可用回溯。
当一个问题使用回溯解决时复杂度过高时,需要找到有效的剪枝方法,才可使用。
以下是在(有重复元素)排列组合子集问题上的应用
1、求全排列
伪代码
1、定义回溯中的状态:结果数组res,可能的排列arr
2、定义搜索终止条件:arr长度等于nums的原始长度时,将arr加入res
3、画出选择树
实现
/**
* @param {number[]} nums
* @return {number[][]}
*/
var permute = function(nums) {
let res = [];
dfs(nums, [], res, new Set());
return res;
};
function dfs(nums, arr, res, set) {
if(arr.length === nums.length) res.push(arr.slice());
for(let i=0;i<nums.length;i++) {
if(!set.has(i)) {
set.add(i);
arr.push(nums[i]);
dfs(nums, arr.slice(), res, set);
arr.pop();
set.delete(i);
}
}
}
2、求子集
代码实现如下:
/**
* @param {number[]} nums
* @return {number[][]}
*/
var subsets = function(nums) {
let res = [[]];
dfs(nums, [], res, 0);
return res;
};
function dfs(nums, arr, res, cur) {
for(let i=cur;i<nums.length;i++) {
arr.push(nums[i]);
res.push(arr.slice());
dfs(nums, arr.slice(), res, i+1);
arr.pop();
}
}
3、求组合
代码实现如下:
/**
* @param {number} n
* @param {number} k
* @return {number[][]}
*/
var combine = function(n, k) {
let nums = [];
for(let i=0;i<n;i++) nums.push(i+1);
let res = [];
dfs(nums, [], res, 0, k);
return res;
};
function dfs(nums, arr, res, cur, k) {
if(arr.length === k) res.push(arr.slice());
for(let i=cur;i<nums.length;i++) {
arr.push(nums[i]);
dfs(nums, arr.slice(), res, i+1, k);
arr.pop();
}
}
可见 求组合是求子集的一个子问题。