將字符串分割爲迴文子串

題目描述

給定一個字符串 s,將 s 分割成一些子串,使每個子串都是迴文串。

返回 s 所有可能的分割方案。

示例:

輸入: "aab"
輸出:
[
  ["aa","b"],
  ["a","a","b"]
]

  題目要找到所有的迴文子串組合方式,看到這個問題很容易就想到了暴力的處理方式,就是從每一個迴文子串處深度遍歷。因爲深度遍歷可以保證不重不漏。再說的具體一些,遍歷的時候就是判斷字符串的第i個字符到第j個字符是否是迴文串,如果是,此時會產生分支,可以逐一字符的往前走,也可以把i到j的字符串當成一個整體往下走,這個過程很好寫代碼。
  而且python很方便判斷字符串i到j是否是迴文串,只需要s[i:j+1] == s[i:j+1][::-1]即可,如果其他語言沒有這個特性,可以參考之前我上一篇文章,動態規劃求解最長迴文串中的迴文串判斷方式。深度優先遍歷代碼如下。

DFS解法代碼

 def partition_dfs(s: str) -> List[List[str]]:
     res = []

     def dfs(start, tmp):
         if start == len(s):
             res.append(tmp)
         for i in range(start+1, len(s) + 1):
             if s[start:i] == s[start:i][::-1]:  # 如果start 到 i 是迴文串,則進行深度遍歷
                 dfs(i, tmp + [s[start:i]])
     dfs(0, [])
     return res

  遞歸的代碼總是寫起來很簡潔,但是容易出現重複計算,我們思考如何把重複計算的開銷給省下來,這必然要涉及到空間換時間。
  聯想到迴文子串的處理方式,使用動態規劃比較容易的找到字符串的所有迴文子串,一種直接的思路就是先把所有的迴文子串求出來,使得可以在O(1)O(1)的時間判斷字符串s[i:j]是否是迴文子串,然後根據這個判斷結果去回溯,減少不必要的重複。這個代碼只需要在上述過程加一個求是否是迴文子串即可。求法同樣可以參考上篇文章,動態規劃求解最長迴文串中的迴文串判斷方式。
  但是我們這裏思考直接的自底向上的動態規劃方式。可以直接思考後一狀態和前一狀態的關係,字符串s[:j]的所有分割方式就等於s[:i]的分割方式和s[i:j]的所有分割方式的全排列。如果s[i:j]中沒有迴文串,那麼分割方式很單一,反之,纔會對s[:j]的狀態產生影響。
  使用dpidp_i來表示到第i個字符爲止,可以產生的所有分割,dpj=dpis[i:j]if s[i:j]dp_j=dp_i * s[i:j]\quad if\ s[i:j]是迴文串。這裏的*表示笛卡爾積,爲了保持第一個字符的處理和後面的字符處理一致,可以在讓dp1dp_-1等於一個列表,這樣方便和後面的作加法。

動態規劃代碼

    def partition(s: str) -> List[List[str]]:
        dp = [[] for _ in range(len(s) + 1)]
        dp[-1] = [[]]
        for end in range(1,len(s)+1):
            for start in range(end):
                if s[start:end] == s[start:end][::-1]:
                    for each in dp[start-1]: # 這裏就是做笛卡爾積的過程
                        dp[end-1].append(each+[s[start:end]])
        return dp[len(s)-1]

  這裏需要注意的就是,如果使用動態規劃,在處理字符串的時候,會導致下標因爲平移的問題,下標處理起來不是很容易,推薦在寫代碼的時候從後往前計算,因爲動態規劃從兩端哪一端開始,在這裏是不影響結果的(大部分不影響)。
  完成了這個問題,其他類似的問題也就很好處理了。

題目描述

給定一個字符串 s,將 s 分割成一些子串,使每個子串都是迴文串。

返回符合要求的最少分割次數。

示例:
輸入: “aab”
輸出: 1
解釋: 進行一次分割就可將 s 分割成 [“aa”,“b”] 這樣兩個迴文子串。

  表明上看這道題比上面的更簡單,但是從思維方式上來看,上一道題更加符合思維方式一些,而這個問題要求的複雜度優化也就更高,上面的題目,可以讓大家從暴力搜索到動態規劃,這道題就很難直接想到動態規劃了。但是有上面的題做基礎,就比較容易。在這裏就不擴展其他方法,直接基於已做的題目進行改代碼。
  首先,我們已經能夠在O(n2)O(n^2)的時間複雜度求出所有的迴文串的起始位置和終止位置。同樣回到上個問題,如果s[i:j]是一個迴文串,那麼顯然dpjdp_j的最小分割次數,就等於dpj=min(dpi+1)if s[i:j]dp_j=min(dp_i + 1)\quad if \ s[i:j]是迴文串。到這裏代碼就很好寫了。可以採取一種思路就是先求出所有的迴文串,然後使用遞推式來求結果。
  這裏介紹一種很取巧的方式,直接記錄所有的迴文串的起始和終止位置,然後對所有的位置使用遞推式進行更改,這裏要注意的就是更改的順序需要按照結束的位置排序之後的順序,否則dpidp_i所求的還不是最終結果,這裏就已經使用dpidp_i計算 dpjdp_j,就會產生錯誤。
  當然也可以在求是否是迴文串的同時,直接求算出dpjdp_j。代碼如下。

求解代碼

def minCut(s: str) -> int:
    # 直接求以第j個字符結尾的字符串,並計算dp
    length = len(s)
    dp = list(range(-1,len(s)))
    pre = [True] + [False]*(length-1)
    for end in range(1,length):
        keys = [False if i!=end else True for i in range(length) ]
        # 記錄當前以end結尾的,以i起始的位置是否是迴文串
        dp[end+1] = dp[end] +1
        for beg in range(end):
            if s[beg] == s[end] and  (pre[beg+1] or beg+1==end):# 判斷是否是迴文串
                dp[end+1] = min(dp[beg]+1,dp[end+1])
                keys[beg] = True
        pre = keys # 保存以end-1爲止第i個位置起始的字符串是否是迴文串

    return dp[-1]

  動態規劃問題重要的是培養動態規劃的思維方式,如何定義問題,進而化簡問題,動態規劃的問題都是有特徵的,對動態規劃的直覺是可以培養的。
  如果真的沒有直覺,建議多花點時間。可以從我這篇文章中學習,對一個問題就按部就班的從暴力,到遞歸再到動態規劃,這也是一個重要的思維途徑。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章