回溯三问
- 当前操作是什么?当前操作使从子问题i转化成i+1
- 子问题:>=i
- 下一个子问题 >= i+1
一个示例:电话号码的字母组合
给定一个仅包含数字 2-9 的字符串,返回所有它能表示的字母组合。答案可以按 任意顺序 返回。
给出数字到字母的映射如下(与电话按键相同)。注意 1 不对应任何字母。
var letterCombinations = function(digits) {
if (digits.length === 0) return []
const num2char = ['', '', 'abc', 'def', 'ghi', 'jkl', 'mno', 'pqrs', 'tuv', 'wxyz']
// 最终结果
let res = [], len = digits.length
// 某个回溯分支的结果
let cur = new Array(digits.length).fill('')
// 表示当前遍历到digits的第i位
const dfs = i => {
// 递归结束,将回溯分支结果记录
if (i === len) {
res.push(cur.join(''))
return
}
const chars = num2char[Number(digits[i])]
// 第i位依次枚举
for (const char of chars) {
cur[i] = char
// 遍历digits的第i+1位
dfs(i+1)
}
}
dfs(0)
return res
};
子集型回溯
关键问题:每个元素可以选择 / 不选择
子集
给你一个整数数组 nums ,数组中的元素 互不相同 。返回该数组所有可能的子集(幂集)。
解集 不能 包含重复的子集。你可以按 任意顺序 返回解集。
有两个思路:
- 从输入的角度思考:对于数组中每个元素,可以选择,也可以不选择,依次枚举
- 从输出的角度思考:枚举第一个数选谁,枚举第二个数选谁...
// 从输入的角度思考
var subsets = function(nums) {
let res = [], cur = []
const len = nums.length
const dfs = i => {
// 递归技术,记录回溯分支结果
if (i === len) {
res.push([...cur])
return
}
// 不选择第i个数字
dfs(i+1)
// 选择第i个数字,需要pop恢复现场
cur.push(nums[i])
dfs(i+1)
cur.pop()
}
dfs(0)
return res
};
// 从输出的角度思考
var subsets = function(nums) {
let res = [], cur = []
const len = nums.length
const dfs = i => {
// 每个回溯节点都需要记录答案
res.push([...cur])
for (let k = i; k < len; k++) {
// 答案的第i个数字的枚举
cur.push(nums[k])
dfs(k+1)
cur.pop()
}
}
dfs(0)
return res
};
分割回文串
给你一个字符串 s,请你将 **s **分割成一些子串,使每个子串都是 回文串 。返回 s 所有可能的分割方案。
回文串 是正着读和反着读都一样的字符串。
分析:假设每个字符间有一个逗号,对每个逗号考虑选它/不选它,通过逗号将字符串分割
// 从输入的角度考虑,对每个逗号,选或不选
var partition = function(s) {
let res = [], cur = []
const len = s.length
// start 表示当前这段回文子串的开始位置
const dfs = (i, start) => {
if (i === len) {
res.push([...cur])
return
}
// 不选 i 和 i+1 之间的逗号(i=n-1 时右边没有逗号)
if (i < len - 1) {
dfs(i+1, start)
}
// 选 i 和 i+1 之间的逗号
const str = s.slice(start, i + 1)
if (str === str.split('').reverse().join('')) {
cur.push(str)
dfs(i+1, i+1)
cur.pop()
}
}
dfs(0, 0)
return res
};
// 从答案的角度考虑,枚举第一个逗号的位置,枚举第二个逗号的位置...
var partition = function(s) {
let res = [], cur = []
const len = s.length
const dfs = i => {
if (i === len) {
res.push([...cur])
return
}
for (let k = i; k < len; k++) {
const str = s.slice(i, k + 1)
if (str === str.split('').reverse().join('')) {
cur.push(str)
dfs(k+1)
cur.pop()
}
}
}
dfs(0)
return res
};
组合型回溯
枚举当前位置选哪个?
组合
给定两个整数 n 和 k,返回范围 [1, n] 中所有可能的 k 个数的组合。
你可以按 任何顺序 返回答案。
var combine = function (n, k) {
const res = [],
cur = []
// dfs(i)表示从[1, ..., i]中选择数
const dfs = (i) => {
if (cur.length === k) {
res.push([...cur])
// 可以直接返回,剪枝
return
}
// [1, ..., i]最多有i个数,当小于剩余未选数的个数时,可以提前返回
// 因为即使全选也满足不了条件
if (i < k - cur.length) return
// 倒序枚举
for (let j = i; j > 0; j--) {
cur.push(j)
dfs(j - 1)
cur.pop()
}
}
dfs(n)
return res
}
// 整体框架上是对数的子集回溯,只是需要判断cur,是否满足条件
var combine = function (n, k) {
const res = [],
cur = []
// dfs(i)表示从[i, ..., n]中选择数
const dfs = i => {
if (cur.length === k) {
res.push([...cur])
return
}
// 元素个数肯定不满足条件,提前返回
if (n - i + 1 < k - cur.length) return
// 正序枚举
for (let j = i; j <= n; j++) {
cur.push(j)
dfs(j + 1)
cur.pop()
}
}
dfs(1)
return res
}
// 选/不选
var combine = function (n, k) {
const res = [],
cur = []
const dfs = i => {
if (cur.length === k) {
res.push([...cur])
return
}
// 提前结束-剪枝
if (n - i + 1 < k - cur.length) return
// 选i
cur.push(i)
dfs(i + 1)
cur.pop()
// 不选i
dfs(i + 1)
}
dfs(1)
return res
}
组合总和-III
找出所有相加之和为 n **的 k ****个数的组合,且满足下列条件:
- 只使用数字1到9
- 每个数字 最多使用一次
返回 所有可能的有效组合的列表 。该列表不能包含相同的组合两次,组合可以以任何顺序返回。
// 选/不选
var combinationSum3 = function(k, n) {
let res = [], cur = []
const dfs = i => {
if (i > 10) return
if (cur.reduce((prev, val) => {
return prev + val
}, 0) === n && cur.length === k) {
res.push([...cur])
return
}
// 选i
cur.push(i)
dfs(i + 1)
cur.pop()
// 不选i
dfs(i + 1)
}
dfs(1)
return res
};
排列型回溯
关键问题:不再区分元素的选择顺序,1,2和2,1是两个答案
全排列
var permute = function(nums) {
const n = nums.length, res = [], cur = []
// 也可以使用一个数组表示
const selected = {}
const dfs = (i) => {
if (i === n) {
res.push([...cur])
return
}
for (let j = 0; j < nums.length; j++) {
const element = nums[j];
if (selected[element]) continue
selected[element] = 1
cur.push(element)
dfs(i + 1)
cur.pop()
selected[element] = 0
}
}
dfs(0)
return res
};
N皇后
function canPut(i, cur, n) {
// 不在同一列
if (cur.indexOf(i) > -1) return false
let res = true
for (let k = 0; k < cur.length; k++) {
const element = cur[k];
// 不在左上角
if (cur.length + i === k + element) {
res = false
break
}
// 不在右上角
if (cur.length - i === k - element) {
res = false
break
}
}
return res
}
var solveNQueens = function(n) {
// cur表示[1,3,0,2],每一行上皇后所在的列
const res = [], cur = []
const dfs = i => {
if (i === n) {
res.push([...cur])
return
}
for (let k = 0; k < n; k++) {
// 也可以使用数组,快速进行判断
if (!canPut(k, cur, n)) continue
cur.push(k)
dfs(i + 1)
cur.pop()
}
}
dfs(0)
return res.map(item => {
return item.map(item2 => {
const arr = new Array(n).fill('.')
arr[item2] = 'Q'
return arr.join('')
})
})
};