xxleyi / loop_invariants

记录以及整理「循环不变式」视角下的算法题解

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

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
}