回溯法,一般可以解决如下几种问题:
-
组合问题:N个数里面按一定规则找出k个数的集合
-
切割问题:一个字符串按一定规则有几种切割方式
-
子集问题:一个N个数的集合里有多少符合条件的子集
-
排列问题:N个数按一定规则全排列,有几种排列方式
-
棋盘问题:N皇后,解数独等等
因为回溯法解决的都是在集合中递归查找子集,集合的大小就构成了树的宽度,递归的深度,都构成的树的深度。
void backtracking(参数) {
if (终止条件) {
存放结果;
return;
}
for (选择:本层集合中元素(树中节点孩子的数量就是集合的大小)) {
处理节点;
backtracking(路径,选择列表); // 递归
回溯,撤销处理结果
}
}
回溯法四部曲(空参治国)
1、定义解空间(路径和结果)
2 、回溯函数的返回值以及参数
3、 回溯函数终止条件(一般在此加入result,且是array化的数组,终止条件多于path.length相关)
4、 单层搜索的过程(一般在上下是相同的结构,path.push backTracing() path.pop)
解题小技巧
- 组合问题
- 可以用startIndex表示开始下标,只要每个下标都有过那么就全部组合过了
- 分割字符串根组合问题同用startIndex解决,思路殊途同归。(终止条件用startIndex)
- 子集问题
- 唯一不同与组合就是,其是在叶子节点收集
- 排列问题
- 需要额外数组标识是否被占用
组合问题:N个数里面按一定规则找出k个数的集合
给定两个整数 n 和 k,返回 1 ... n 中所有可能的 k 个数的组合。
示例:
输入: n = 4, k = 2
输出:
[
[2,4],
[3,4],
[2,3],
[1,2],
[1,3],
[1,4],
]
// 1. 确定解空间
let result = []
let step = []
//2. 确定参数 n遍历子元素的数量,k能遍历的深度
function backTracing(n, k, startindex){
// 2. 确定回溯终止条件,这里是数组长度,确定是有一个解
if (step.length === k) {
result.push(Array.from(step))
return
}
for (let i = startindex; i <= n; i++) {
// 3. 确定处理节点
step.push(i)
backTracing(n, k, i + 1)
step.pop()
}
}
var combine = function (n, k) {
result = []
step = []
backTracing(n, k, 1)
return result
};
总结:
回溯法首先要考虑树状结构,对于此类的组合问题,
- 注重i=startIndex这个技巧
- 注重终止条件与,step.length的关系
- 具体操作中回溯上下操作一致
- 回溯的startIndex=i+1
相关练习
排列问题:N个数按一定规则全排列,有几种排列方式
给定一个不含重复数字的数组 nums ,返回其 所有可能的全排列 。你可以 按任意顺序 返回答案。
示例 1:
输入:nums = [1,2,3]
输出:[[1,2,3],[1,3,2],[2,1,3],[2,3,1],[3,1,2],[3,2,1]]
示例 2:
输入:nums = [0,1]
输出:[[0,1],[1,0]]
示例 3:
输入:nums = [1]
输出:[[1]]
/**
* @param {number[]} nums
* @return {number[][]}
*/
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++ ) {
if(used[i]) continue;
path.push(n[i]);
used[i] = true; // 同支
backtracking(n, k, used);
path.pop();
used[i] = false;
}
}
};
总结:
排列问题与组合问题最大的区别是元素有顺序
在代码层面,不以sartIndex为限制,而以used[]来限制
子集问题:一个N个数的集合里有多少符合条件的子集
给你一个整数数组 nums ,数组中的元素 互不相同 。返回该数组所有可能的子集(幂集)。
解集 不能 包含重复的子集。你可以按 任意顺序 返回解集。
示例 1:
输入:nums = [1,2,3]
输出:[[],[1],[2],[1,2],[3],[1,3],[2,3],[1,2,3]]
示例 2:
输入:nums = [0]
输出:[[],[0]]
// 1 定义解空间
let result = []
let step = []
// 2 定义回溯函数的参数和反回值
function backTracing(startIndex, nums) {
result.push(Array.from(step))
// 3 定义终止条件
if (startIndex === nums.length) {
return
}
// // 4 定义操作,遍历每一个元素
for(let i=startIndex;i<nums.length;i++){
step.push(nums[i])
backTracing(++i, nums)
step.pop()
}
}
var subsets = function (nums) {
result = []
step = []
backTracing(0, nums)
console.log(result)
};
subsets([1, 2, 3])
总结
与其他类型区别最大的就是,他是在每一个节点收集结果,而非在叶子节点
切割问题:一个字符串按一定规则有几种切割方式
给定一个字符串 s,将 s 分割成一些子串,使每个子串都是回文串。
返回 s 所有可能的分割方案。
示例: 输入: "aab" 输出: [ ["aa","b"], ["a","a","b"] ]
代码
const isPalindrome = (s, l, r) => {
for (let i = l, j = r; i < j; i++, j--) {
if(s[i] !== s[j]) return false;
}
return true;
}
var partition = function(s) {
const res = [], path = [], len = s.length;
backtracking(0);
return res;
function backtracking(i) {
if(i >= len) {
res.push(Array.from(path));
return;
}
for(let j = i; j < len; j++) {
if(!isPalindrome(s, i, j)) continue;
path.push(s.substr(i, j - i + 1));
backtracking(j + 1);
path.pop();
}
}
};
总结:
类似多了条件判断的组合问题,还是有startIndex等
个人总结:
回溯算法不是很难,主要为两种
- 以每个节点为结果集的类型(子集问题)
- 以叶子节点为结果集的类型(排列问题,组合问题,切割问题)