LeetCode 87,遠看是字串其實是搜尋,你能做出來嗎?

TechFlow2019 發表於 2020-08-01

本文始發於個人公眾號:TechFlow,原創不易,求個關注


今天是LeetCode專題第54篇文章,我們一起來看LeetCode 87題,Scramble String(爬行字串)。

這題的官方難度是Hard,通過率33%,點贊506,反對702。看起來這題難度還可以,但是反對比點贊多,其實這題質量還不錯,反對比較多我猜可能是因為題意稍稍有些複雜,理解起來不太容易,編碼也偏難。但是這題如果是放在正式比賽中出現的話,都不叫事。

下面我們來看下題意。

題意

這題的題目叫做爬取字串,看起來有些費解,其實這個爬取是題目中定義出來的一種操作,我們稍候結合樣例來看很容易理解。首先,我們先把一個字串拆分成二叉樹的形式。

   great
   /    \
  gr    eat
 / \    /  \
g   r  e   at
           / \
          a   t

也就是我們隨機的選擇分段點,每次都將字串分割成兩個部分。有了這棵二叉樹之後,我們就可以進行爬取操作了。所謂的爬取操作,也就是調換這棵二叉樹當中某一個節點的左右孩子的順序。比如假設我們選擇了對gr這個節點進行爬取,那麼得到的結果如下:

    rgeat
   /    \
  rg    eat
 / \    /  \
r   g  e   at
           / \
          a   t

我們還可以多次執行爬取,比如我們多次爬取操作之後可以得到一個全新的字串rgtae.

    rgtae
   /    \
  rg    tae
 / \    /  \
r   g  ta  e
       / \
      t   a

rgeat和rgtae都是從原字串great進行一系列爬取操作之後得到的,題目會給定兩個字串s1和s2,要求我們給出能否通過對s1爬取操作得到字串s2?

題解

不知道大家看完題意是什麼感覺,是否覺得有些棘手呢?

棘手歸棘手,但題目的要求還是很明確的。還是老規矩,我們一點點來分析問題。首先,那個花裡胡哨的爬取操作是一個可逆操作,也就是說如果字串s1能夠通過這些操作變成s2,那麼同樣s2也可以通過同樣的操作變回s1。從更高的層面來說,它們其實是一樣的,是同一個存在的兩個狀態。

進一步,如果大家學過圖論相關的演算法,對這塊有所瞭解的話,那麼這個問題還可以進一步變形。

假設我們最初的字串是s,它通過一步爬取操作可以變成s1,s2和s3。那麼我們可以把這些字串都抽象成一張無向圖當中的節點。可以看成是s和s1,s2和s3之間有一條邊相連。所以字串之間能否通過爬取轉化的關係就變成了在圖上是否聯通的關係,這個問題也就變成了在一張無向圖當中已知兩點,請問這兩點是否聯通。這個問題就簡單多了,我們遍歷整張圖就好了。

縮小到了圖上遍歷之後,整個問題其實已經出來了,遍歷圖無非兩種方法,一種是深度優先搜尋,一種是寬度優先搜尋。這兩種都是老掉牙的演算法了,實在沒什麼稀奇的。在這題當中深搜寬搜都差不多,看你的喜好了。我個人是選擇的深搜實現的。

對於字串的爬取操作而言,一共有兩種可能,一種是s1拆分之後的兩個部分分別和s2同樣位置的兩個部分的字串進行比較。還有一種可能是s1的前半部分和s2的後半部分,s1的後半部分和s2的前半部分判斷。這兩種情況其實是同一個節點在搜尋樹上的兩個支路,相當於我們提前剪枝了,剪掉了不可能存在解的搜尋子樹,這個也是剪枝的常規做法。

大家可能感覺這個題意比較複雜,但是最後的程式碼也許要比大家想的要簡單:

class Solution:
    def isScramble(self, s1: str, s2: str) -> bool:
        from collections import Counter
        
        def determine(s1, s2):
            # 如果s1和s2構成的字元不同,那麼直接排除
            c1 = Counter(list(s1))
            c2 = Counter(list(s2))
            return c1 == c2

        
        def dfs(s1, s2):
            # 如果要判斷的s1和s2相等,返回True
            if s1 == s2:
                return True
            if not determine(s1, s2):
                return False
            n = len(s1)
            # 列舉拆分的位置將字串拆分成兩個部分
            for i in range(1, n):
                if dfs(s1[:i], s2[:i]) and dfs(s1[i:], s2[i:]) or dfs(s1[:i], s2[n-i:]) and dfs(s1[i:], s2[:n-i]):
                    return True
            return False
        
            
        if len(s1) != len(s2):
            return False
        if len(s1) == 0:
            return True
        return dfs(s1, s2)

總結

今天的這道題就算是講完了,雖然看起來涉及到各種字串的操作,又是建樹又是顛倒順序什麼的,但這題本質上其實是一道搜尋題。只要對搜尋問題稍微熟悉一點,做出這道題並不困難,這也是本題通過率其實不算低的原因。

在之前的文章當中也曾經提到過,不管是在LeetCode上也好,還是在acm賽場上也罷,一道看似是字串的問題最後通過建模轉化成其他的演算法模型是家常便飯的事情。大家做題的時候一定要思維靈活,如果鑽了牛角尖可能就解不出來了。

今天的文章到這裡就結束了,如果喜歡本文的話,請來一波素質三連,給我一點支援吧(關注、轉發、點贊)。

本文使用 mdnice 排版

LeetCode 87,遠看是字串其實是搜尋,你能做出來嗎?