本文始發於個人公眾號:TechFlow,原創不易,求個關注
今天是LeetCode專題第55篇文章,我們一起來看看LeetCode中的第89題 Gray Code(格雷碼)。
這題的官方難度是Medium,通過率是48.9%,點贊639,反對1545。又是一道反對比點贊多得多的題目,我個人發現其實這些反對很多的題目都有一個特點,就是題意比較晦澀,出題人的意圖不太容易get到。不知道是不是老外理解能力不太行,所以都給出了這麼多的反對。
我們就來看看這道題的真面目吧。
題意
題目中說gray code,格雷碼是一連串n位二進位制表示的數字。這一串的數字有一個特點就是第一個數字是0,從0開始後面的每一個數字和前一個數字只有一個二進位制位不同。
題目會給定我們一個非負整數n,要求我們生產n位的灰色程式碼,也就是產生這些數字。並且這些數字是以10進位制儲存的。
不知道大家看明白沒有,我們來看一個樣例。
樣例
Input: 2
Output: [0,1,3,2]
在上面這個例子當中,輸入是2,表示這些數字是兩位二進位制位構成的,輸出是[0, 1, 3, 2]。我們把0,1,3,2翻譯成二進位制,0是00,1是01,3是11,2是10。排列在一起的話就是00, 01, 11, 10。我們可以發現每一個數和前一個數相差的都是一個二進位制位。
題目當中相關的描述就這麼多,但其實有很多隱藏的資訊沒有給,要我們自己猜測。比如說每一個數字只能出現一次,不然的話這個序列就是無窮無盡的。另外一個隱藏資訊是,這樣的序列應該也不是唯一的,但是題目並沒有說是否所有合法的序列都可以通過測試,還是說一定要返回字典序最小的結果。
題目比較晦澀也就算了,這些隱藏資訊沒有交代清楚,也難怪大家會費解。
題解
當然以上的問題其實也不是事,我們不確定試一次也就知道了,核心還是怎麼想出解法來。
幹想是沒有結果的,還是要先分析蒐集一些資訊。首先,題目給定的n,限制了每個數能夠使用的二進位制位的數量。n個二進位制位一共能表示的數字有$2^n$種,我們無法得知是否這麼多數字都能串聯起來。假設可行的話,那麼這個問題其實就是這$2^n$個數如何擺放的問題。
所以問題的關鍵就是要尋找這樣一個序列,根據我們之前解全排列以及各種排列的方法,可以聯想得到,這大概率是一個搜尋問題。
順著搜尋的思路繼續往下,剩下的事情就容易了,我們的起始搜尋點是0。題目中要求了每兩個相鄰的數的二進位制位只相差一個,那麼我們可以遍歷這些二進位制位,尋找0的後繼節點。同樣對於每一個後繼節點來說,我們都可以用同樣的方法尋找它的後繼們。再加上gray code不能包含重複的元素,我們可以在搜尋的時候加上剪枝。
這一套其實是一個經典的搜尋問題的流程。
如果我們換個思路,雖然也能得到一樣的解法,但是思考的過程會不太一樣。怎麼換思路呢,其實也簡單,我們把它想象成一個圖論問題。也就是說,每一個數字都是圖中的一個節點。如果兩個數字之間滿足只相差了一個二進位制位,那麼說明它們之間有一條邊相連。整個問題就轉變成了我們從0這個點出發,找出所有連通的節點。
對於圖上的遍歷問題,方法就很固定了就是搜尋。也就是說從這個角度思考的話,更加容易想到搜尋上面了, 整個思考的鏈路會更短。這也是為什麼很多大神建模的時候喜歡從往圖上考慮的原因。
這些都想明白了再來寫程式碼真的就水到渠成了,整個核心程式碼真的不長:
class Solution:
def grayCode(self, n: int) -> List[int]:
ret = [0]
elements = {0}
def dfs(cur):
# 遍歷與cur唯一不同的二進位制位
for i in range(n):
# 針對這一維做亦或,將0變1,1變0
nxt = cur ^ (1 << i)
if nxt in elements:
continue
# 記錄答案,繼續往下遍歷
elements.add(nxt)
ret.append(nxt)
dfs(nxt)
dfs(0)
return ret
總結
單純從思路以及最後的AC程式碼來看的話,這道題難度應該是很低的,實際上也的確如此,這題的通過率接近50%,已經是Medium難度的下屆了。但是相比於做對這題而言,更加重要的是思路。以圖論的思維來抽象建模是演算法題當中一個非常常見的手段,這是比題目本身更加寶貴的東西。
如果你讀過昨天的文章的話,會發現昨天的87題,本質上也是用的一個圖論建模的方法。但是從表現形式上來說,這兩題真的可以說是完全不一樣。建議大家能好好做做這兩題,體會一下其中思維和解法的閃光點。
今天的文章到這裡就結束了,如果喜歡本文的話,請來一波素質三連,給我一點支援吧(關注、轉發、點贊)。
本文使用 mdnice 排版