題目描述
給定一個字符串 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
遞歸的代碼總是寫起來很簡潔,但是容易出現重複計算,我們思考如何把重複計算的開銷給省下來,這必然要涉及到空間換時間。
聯想到迴文子串的處理方式,使用動態規劃比較容易的找到字符串的所有迴文子串,一種直接的思路就是先把所有的迴文子串求出來,使得可以在的時間判斷字符串s[i:j]是否是迴文子串,然後根據這個判斷結果去回溯,減少不必要的重複。這個代碼只需要在上述過程加一個求是否是迴文子串即可。求法同樣可以參考上篇文章,動態規劃求解最長迴文串中的迴文串判斷方式。
但是我們這裏思考直接的自底向上的動態規劃方式。可以直接思考後一狀態和前一狀態的關係,字符串s[:j]的所有分割方式就等於s[:i]的分割方式和s[i:j]的所有分割方式的全排列。如果s[i:j]中沒有迴文串,那麼分割方式很單一,反之,纔會對s[:j]的狀態產生影響。
使用來表示到第i個字符爲止,可以產生的所有分割,。這裏的表示笛卡爾積,爲了保持第一個字符的處理和後面的字符處理一致,可以在讓等於一個列表,這樣方便和後面的作加法。
動態規劃代碼
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”] 這樣兩個迴文子串。
表明上看這道題比上面的更簡單,但是從思維方式上來看,上一道題更加符合思維方式一些,而這個問題要求的複雜度優化也就更高,上面的題目,可以讓大家從暴力搜索到動態規劃,這道題就很難直接想到動態規劃了。但是有上面的題做基礎,就比較容易。在這裏就不擴展其他方法,直接基於已做的題目進行改代碼。
首先,我們已經能夠在的時間複雜度求出所有的迴文串的起始位置和終止位置。同樣回到上個問題,如果s[i: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]
動態規劃問題重要的是培養動態規劃的思維方式,如何定義問題,進而化簡問題,動態規劃的問題都是有特徵的,對動態規劃的直覺是可以培養的。
如果真的沒有直覺,建議多花點時間。可以從我這篇文章中學習,對一個問題就按部就班的從暴力,到遞歸再到動態規劃,這也是一個重要的思維途徑。