On the first row, we write a 0
. Now in every subsequent row, we look at the previous row and replace each occurrence of 0
with 01
, and each occurrence of 1
with 10
.
Given row N
and index K
, return the K
-th indexed symbol in row N
. (The values of K
are 1-indexed.) (1 indexed).
Examples: Input: N = 1, K = 1 Output: 0 Input: N = 2, K = 1 Output: 0 Input: N = 2, K = 2 Output: 1 Input: N = 4, K = 5 Output: 1 Explanation: row 1: 0 row 2: 01 row 3: 0110 row 4: 01101001
Note:
N
will be an integer in the range[1, 30]
.K
will be an integer in the range[1, 2^(N-1)]
.
這道題說第一行寫上了一個0,然后從第二行開始,遇到0,就變為01,遇到1,則變為10,問我們第N行的第K個數字是啥。這是一道蠻有意思的題目,首先如果沒啥思路的話,按照給定的方法,一行行generate出來,直到生成第N行,那么第K個數字也就知道了。但是這種brute force的方法無法通過OJ,這里就不多說了,需要想一些更高端的解法。我們想啊,遇到0變為01,那么可不可以把0和1看作上一層0的左右子結點呢,同時,把1和0看作上一層1的左右子結點,這樣的話,我們整個結構就可以轉為二叉樹了,那么前四層的二叉樹結構如下所示:
0
/ \
0 1
/ \ / \
0 1 1 0
/ \ / \ / \ / \
0 1 1 0 1 0 0 1
我們仔細觀察上面這棵二叉樹,第四層K=3的那個紅色的左子結點,其父結點的位置是第三層的第 (K+1)/2 = 2個紅色結點,而第四層K=6的那個藍色幽子結點,其父節點的位置是第三層的第 K/2 = 3個藍色結點。那么我們就可以一層一層的往上推,直到到達第一層的那個0。所以我們的思路是根據當前層K的奇偶性來確定上一層中父節點的位置,然后繼續往上一層推,直到推倒第一層的0,然后再返回確定路徑上每一個位置的值,這天然就是遞歸的運行機制啊。我們可以根據K的奇偶性知道其是左結點還是右結點,由於K是從1開始的,所以當K是奇數時,其是左結點,當K是偶數時,其是右結點。而且還能觀察出來的是,左子結點和其父節點的值相同,右子結點和其父節點值相反,這是因為0換成了01,1換成了10,左子結點保持不變,右子結點flip了一下。想通了這些,那么我們的遞歸解法就不難寫出來了,參見代碼如下:
解法一:
class Solution { public: int kthGrammar(int N, int K) { if (N == 1) return 0; if (K % 2 == 0) return (kthGrammar(N - 1, K / 2) == 0) ? 1 : 0; else return (kthGrammar(N - 1, (K + 1) / 2) == 0) ? 0 : 1; } };
我們可以簡化下上面的解法,你們可能會說,納尼?已經三行了還要簡化?沒錯,博主就是這樣一個精益求精的人(此處應有掌聲👏)。我們知道偶數加1除以2,和其本身除以2的值是相同的,那么其實不論K是奇是偶,其父節點的位置都可以用 (K+1)/2 來表示,問題在於K本身的奇偶決定了其左右結點的位置,從而決定要不要flip父節點的值,這才是上面解法中我們要使用 if...else 結構的原因。實際上我們可以通過‘亦或’操作來實現一行搞定,叼不叼。我們來看下變換規則,0換成了01,1換成了10。
0 -> 01
左子結點(0) = 父節點(0) ^ 0
右子結點(1) = 父節點(0) ^ 1
1 -> 10
左子結點(1) = 父節點(1) ^ 0
右子結點(0) = 父節點(1) ^ 1
那么只要我們知道了父結點的值和當前K的奇偶性就可以知道K的值了,因為左子結點就是父結點值‘亦或’0,右子結點就是父結點值‘亦或’1,由於左子結點的K是奇數,我們可以對其取反再‘與’1,所以就是 (~K & 1),再‘亦或’上遞歸函數的返回值即可,參見代碼如下:
解法二:
class Solution { public: int kthGrammar(int N, int K) { if (N == 1) return 0; return (~K & 1) ^ kthGrammar(N - 1, (K + 1) / 2); } };
下面這種解法的思路也十分巧妙,還是從變換規則入手,0換成了01,1換成了10。我們來看解法一講解中的二叉樹示例圖中的藍色路徑,第四層K=6,第三層K=3,第二層K=2,第一層K=1,所以路徑就是 6->3->2->1,如果我們換成0開頭的計數方法,那么路徑就是 5->2->1->0,再換成二進制的表達方式就是 101->10->1->0,我們發現其實就是每次右移一位,直到移動到0,而當最低位是1的時候,表示當前是右子結點,需要flip一下,那么只要我們知道總共flip了多少次,就能推算出第K個位置的值。比如 101->10->1->0,總共flip了兩次變成了0,那么說明K=6的位置值為0。於是問題就轉化為了統計K-1這個數的二進制表示中位1的個數,使用一個while循環統計出來就可以了,然后根據個數的奇偶性返回1或0即可,參見代碼如下:
解法三:
class Solution { public: int kthGrammar(int N, int K) { int cnt = 0; --K; while (K) { cnt += K & 1; K >>= 1; } return cnt % 2; } };
下面這種解法的思路也很清新脫俗啊,一切的一切都是從變換規則入手,0換成了01,1換成了10。那么當K是奇數的時候,我們之前分析了,其一定是左子結點,那么其是01或者10的第一個數字,因為只有這兩種組合方式,所以如果第K個數是0的話,那么第K+1個數就是1,同樣,如果第K個數是1的話,那么第K+1個數就是0,所以此時第K個數和第K+1個數一定相反,那么我們就可以通過‘亦或’1來實現這個一定相反的操作。
當K是偶數的時候,那么其是01或者10的第二個數字,那么根據之前的分析,其是由上一層的第 K/2 位置的數字生成的,上一層的第 K/2 個數字和當前層的第 K/2 個數字是一樣的,如果你仔細觀察題目中的例子或者博主畫的那個二叉樹圖,只要K不越界,每一層的第K個數字都是相等的。所以如果第K個數是0的話,那么第 K/2 個數就是1,同樣,如果第K個數是1的話,那么第 K/2 個數就是0,所以此時第K個數和第 K/2 個數一定相反,那么我們也可以通過‘亦或’1來實現這個一定相反的操作。
於是乎,我們的操作就是,當K是奇數的時候,我們就將其換成K+1,當K是偶數的時候,我們將其換為K/2。然后每次都對結果res(初始化為0)進行‘亦或’1操作,循環的終止條件是當K等於1時,參見代碼如下:
解法四:
class Solution { public: int kthGrammar(int N, int K) { int res = 0; while (K > 1) { K = (K % 2 == 1) ? K + 1 : K / 2; res ^= 1; } return res; } };
下面這種解法跟解法三的思路完全相同,只不過使用了bitset這個內置的數據結構來快速的求出了K-1的二進制表達數中的位1的個數,Java中可以直接使用Integer.bitCount()函數,參見代碼如下:
解法五:
class Solution { public: int kthGrammar(int N, int K) { return bitset<32>(K - 1).count() % 2; } };
參考資料:
https://leetcode.com/problems/k-th-symbol-in-grammar/solution/
https://leetcode.com/problems/k-th-symbol-in-grammar/discuss/113705/JAVA-one-line