xxleyi / loop_invariants

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

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

leetcode 779. 第K个语法符号

xxleyi opened this issue · comments

题:

在第一行我们写上一个 0。接下来的每一行,将前一行中的0替换为01,1替换为10。

给定行数 N 和序数 K,返回第 N 行中第 K个字符。(K从1开始)

例子:

输入: N = 1, K = 1
输出: 0

输入: N = 2, K = 1
输出: 0

输入: N = 2, K = 2
输出: 1

输入: N = 4, K = 5
输出: 1

解释:
第一行: 0
第二行: 01
第三行: 0110
第四行: 01101001

注意:

N 的范围 [1, 30].
K 的范围 [1, 2^(N-1)].

来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/k-th-symbol-in-grammar
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。


解:

此题的标签是递归和数学,本题解只讨论递归解法。

什么是递归呢?写了递归函数就是递归吗?某种程度上,可以这样说。但还未抵达本质。这其中一个关键概念是尾递归。很多人可能听过尾递归,但对尾递归的把握可能还不到位,我这里指出一点,或许能有所帮助:尾递归的本质是迭代,换句话说:尾递归的本质是使用递归函数的形式写「迭代式」的计算过程,也正因为如此,编译器可以有办法将尾递归优化为循环。

下面这个渐进式的题解,分别使用递归、尾递归和循环,其中尾递归和循环已经非常严格的一一对应,尤其是其中的「循环不变式」。

/**
 * @param {number} N
 * @param {number} K
 * @return {number}
 */
var kthGrammar = function(N, K) {
  function recursive(n, k) {
    if (k === 1) return 0
    const lastLength = 1 << (n - 2)
    const needReverse = k > lastLength
    if (needReverse) k = k - lastLength
    const last = recursive(n - 1, k)
    return needReverse ? (last + 1) % 2 : last
  }
  return recursive(N, K)
};

const recursive = (initial, iter) => iter(initial, cur => recursive(cur, iter))

var kthGrammar = (N, K) => recursive(
  [N, K, false],
  (cur, next) => {
    if (cur[1] === 1) return cur[2] ? 1 : 0
    else {
      const lastLength = 1 << (cur[0] - 2)
      const needReverse = cur[1] > lastLength
      if (needReverse) return next([cur[0] - 1, cur[1] - lastLength, !cur[2]])
      return next([cur[0] - 1, cur[1], cur[2]])
    }
  }
)

var kthGrammar = function(N, K) {
  let flip = false
  while (K !== 1) {
    const lastLength = 1 << (N - 2)
    const needReverse = K > lastLength
    if (needReverse) {
      K -= lastLength
      flip = !flip
    }
    N -= 1
  }
  return flip ? 1 : 0
}

补充一下数学解法:

位计算:

var kthGrammar = function(N, K) {
  return ((K - 1).toString(2).match(/1/g) || []).length % 2
}

这其中貌似没有用到 N,但其实不然,K 已经隐含有 N 的影子,对不对?

面对位计算这种降维打击解法,上面的题解还有什么意义吗?在性能上确实没意义,但是在解题过程的训练,尤其是递归 -> 尾递归 -> 循环的优化过程,这份努力和操练完全是自己的。