[路飞]算法: 779. 第K个语法符号

230 阅读6分钟

779. 第K个语法符号

##正题

在第一行我们写上一个 0。接下来的每一行,将前一行中的0替换为011替换为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

解析:

首先尝试手动推导了前几个情况

//  第一行: 0
// 第二行: 01
// 第三行: 0110
// 第四行: 01101001
//.       0110011001
    //    01101001011010010110
    //    0110100110010110011010011001011001101001
    //    01101001100101101001011001101001011010011001011010010110011010010110100110010110

后面的实在是因为太多了写不下去了。

一.暴力破解

在推导的过程中可以发现每次当前行的每一个字符都是由上一行字符决定的,并且大小一定是上一行的两倍。既然手动可以推导,那么尝试去用计算机去按照手动推导的思路去完成,理论上就是可行的。

于是写了 transHandle 方法去实现遍历字符串每一个字符,然后将每一个字符按照规则生成2个字符,并且拼接起来,最终 return 拼接后的字符


var transHandle = function (str) {
    let res = ''
    for (let index = 0 ; index < str.length; index++) {
        res += str[index] === '0' ? '01' : '10' 
    }
    return res
}

在最终实现的时候只要吊用该方法:

var kthGrammar = function(n, k) {
    let str = '0'
    for (let index = 0; index < n - 1; index++) {
        str = transHandle(str)
    }
    return str[k - 1]
};

理论上该算法是可行的,于是提交。

image.png

可以发现 Leetcode 最终返回的结果是超出了内存,也就是说当 n 很大的时候,这个计算量是极大的。其实在手动推算的时候推到前几行就已经非常吃力了,可以想象 n 很大的时候是有极大的计算量的,即使这样计算出结果,那对计算机的性能消耗来说也是非常大的。所以该方法不可取。

二.找出底层逻辑

所以要想解决这个问题实际上并没有那么容易,需要找出数字变化的底层逻辑到底是怎样的。

1.尝试推演

当我们并不能一眼看出其中相关性和技巧时,我们可以尝试去从最少的情况开始考虑,一步一步考虑 n 变大的情况。

当 n = 1 时:

n = 1 时是题目给定的数值,为 0,这个也是本题唯一的不用计算得到的数字,所以他才是最底层的数字。

当 n = 2 时:

n = 2 时就变成了 2 个数字,并且这两个数字都是由 n = 1 时的第一个数字演变过来的。

当 n = 3 时:

n = 3 时就变成了 4 个数字,前两个数字是由 n = 2 的第一个数字演变过来, 第三四个数字是由 n = 2 时的第二个数字演变过来。

到此我们可以得出一个结论:

1.k是偶数的话 第 N 行 第 K 个 是 第 N -1 行 第 k / 2 个变来的

2.k是奇数的话 第 N 行 第 K 个 是 第 N -1 行 第 (k + 1) / 2 个变来的

2.确定数值

得出上面结论后,我们要确定第 n 行 第 k 个数字和 第 n - 1 行演变的数字有什么关系。

划重点!! 根据演变规则:

n - 1 演变的数字如果是 0,那么在第 n 行就会产生 01两个数, k是偶数那这个数就是 1, k 是奇数那这个数就是 0

n - 1 演变的数字如果是 1,那么在第 n 行就会产生 10两个数, k是偶数那这个数就是 0, k 是奇数那这个数就是 1

递归逆推

有了以上的结论那么我们采用递归的方式进行逆推,这个问题应该就能够迎刃而解了。

首先确定最终返回的规则,也就是跳出递归的条件:

if (n === 1) {
    if (k === 1) {
        return 0
    } else {
        return 1
    }
}

当行数 n = 1 的时候,我只需要知道 k 是 1 还是 0就能够知道最终答案了。

开始递归:

if (k % 2 === 0) {
// **n - 1 演变的数字如果是 0,那么在第 n 行就会产生 01两个数, k是偶数那这个数就是 1, k 是奇数那这个数就是 0**
// **n - 1 演变的数字如果是 1,那么在第 n 行就会产生 10两个数, k是偶数那这个数就是 0, k 是奇数那这个数就是 1**
       return kthGrammar(n - 1, k / 2) === 1 ? 0 : 1
    } else {
      return kthGrammar(n-1, (k + 1) / 2) === 1 ? 1 : 0
    }

每递归一次,行数都会 -1, k 也会根据奇偶性找出 n -1 对应的k是多少

这样一来,当 n 递归到了 第一行的时候,我们就能够知道前面是怎么一步一步得来的了。

完整代码:

/**
 * @param {number} n
 * @param {number} k
 * @return {number}
 */
//  第一行: 0
// 第二行: 01
// 第三行: 0110
// 第四行: 01101001
//.       0110011001
    //    01101001011010010110
    //    0110100110010110011010011001011001101001
    //    01101001100101101001011001101001011010011001011010010110011010010110100110010110
var kthGrammar = function(n, k) {

   //k是偶数的话 第 N 行 第 K 个 是 第 N -1 行 第 k / 2 个变来的
   //k是奇数的话 第 N 行 第 K 个 是 第 N -1 行 第 (k + 1) / 2 个变来的
   //逆推
   if (n === 1) {
        if (k === 1) {
            return 0
        } else {
            return 1
        }
   }
  
    if (k % 2 === 0) {
// **n - 1 演变的数字如果是 0,那么在第 n 行就会产生 01两个数, k是偶数那这个数就是 1, k 是奇数那这个数就是 0**
// **n - 1 演变的数字如果是 1,那么在第 n 行就会产生 10两个数, k是偶数那这个数就是 0, k 是奇数那这个数就是 1**
       return kthGrammar(n - 1, k / 2) === 1 ? 0 : 1
    } else {
      return kthGrammar(n-1, (k + 1) / 2) === 1 ? 1 : 0
    }
 };

代码其实很简单,但是逻辑是非常的绕人,还是比较复杂的。暴力破解简单但效率低,解题优先考虑找出规律再取巧。事半功倍!

image.png

最后附上提交记录!