leetcode 1079. 活字印刷
xxleyi opened this issue · comments
题:
你有一套活字字模 tiles,其中每个字模上都刻有一个字母 tiles[i]。返回你可以印出的非空字母序列的数目。
注意:本题中,每个活字字模只能使用一次。
示例 1:
输入:"AAB"
输出:8
解释:可能的序列为 "A", "B", "AA", "AB", "BA", "AAB", "ABA", "BAA"。
示例 2:
输入:"AAABBC"
输出:188
提示:
1 <= tiles.length <= 7
tiles 由大写英文字母组成
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/letter-tile-possibilities
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。
解:自己只能想出双层暴力回溯,大神的计数器回溯法,巧妙至极。无论如何,回溯的复杂度都比较高,而「循环不变式」隐藏在整个回溯过程的三个关键操作中:
- 有序递归枚举
- 有效剪枝下的选择
- 回退之前恢复现场
// 双层回溯:简单粗暴,复杂度较高
var numTilePossibilities = function(tiles) {
const seqs = new Set()
const used = Array(tiles.length)
// 辅助函数
function parseSeq() {
const seq = []
for (let [i, e] of used.entries()) {
if (e) seq.push(tiles[i])
}
for (let e of permute(seq)) {
seqs.add(e.join(''))
}
}
// 内层回溯
function permute(seq) {
// 退化情况
if (seq.length === 0) return []
// 递归终止
if (seq.length === 1) return [[seq[0]]]
// leap our faith
const res = []
for (let i = 0; i < seq.length; i++) {
let permuteWithOutI = permute(seq.slice(0, i).concat(seq.slice(i + 1, seq.length)))
for (let e of permuteWithOutI) {
res.push([seq[i]].concat(e))
}
}
return res
}
// 外层回溯
function backtrack(i = 0) {
// 递归终止条件
if (i === tiles.length) {
parseSeq()
return
}
// with i
used[i] = true
backtrack(i + 1)
// without i
used[i] = false
backtrack(i + 1)
}
backtrack()
return seqs.size
};
// 大神的回溯:巧妙极了,自己想不出来
var numTilePossibilities = function(tiles) {
// 分字母计数器,用于回溯
const counter = {}
for (let e of tiles) {
counter[e] = (counter[e] || 0) + 1
}
// 总的计数器,用于保存统计结果
let num = 0
function backtrack(counter) {
for (let key in counter) {
// 有效选择
if (counter[key]) {
// 进行一次有效选择
counter[key] -= 1
// 总的计数器加 1
num += 1
// 继续回溯:递归 + 有效选择 + 回退之前恢复现场
backtrack(counter)
// 回退之前恢复现场
counter[key] += 1
}
}
// 递归终止,回退
}
backtrack(counter)
return num
}