動態規劃
“動態規劃”問題的思考路徑,供大家參考:
1、思考狀態
狀態先嚐試“題目問什麼,就把什麼設置爲狀態”。然後緊接着考慮“狀態該如何轉移?”,如果“狀態轉移方程”不容易得到,就嘗試修改其定義,但目的仍然是爲了方便得到“狀態轉移方程”。
2、思考狀態轉移方程(核心、難點)
狀態轉移方程是非常重要的,是動態規劃的核心,也是難點,起到承上啓下的作用。
技巧是分類討論。對狀態空間進行分類,思考最優子結構到底是什麼。即大問題的最優解如何由小問題的最優解得到。
歸納“狀態轉移方程”是一個很靈活的事情,得具體問題具體分析,除了掌握經典的動態規劃問題以外,還需要多做題。如果是針對面試,請自行把握難度,我個人覺得掌握常見問題的動態規劃解法,明白動態規劃的本質就是打表格,從一個小規模問題出發,逐步得到大問題的解,並記錄過程。動態規劃依然是“空間換時間”思想的體現。
3、思考初始化
初始化是非常重要的,一步錯,步步錯,初始化狀態一定要設置對,纔可能得到正確的結果。
角度 1:直接從狀態的語義出發;
角度 2:如果狀態的語義不好思考,就考慮“狀態轉移方程”的邊界需要什麼樣初始化的條件;
角度 3:從“狀態轉移方程”方程的下標看是否需要多設置一行、一列表示“哨兵”,這樣可以避免一些邊界的討論,使得代碼變得比較短。
4、思考輸出
有些時候是最後一個狀態,有些時候可能會綜合所有計算過的狀態。
5、思考狀態壓縮
“狀態壓縮”會使得代碼難於理解,初學的時候可以不一步到位。先把代碼寫正確,然後再思考狀態壓縮。
狀態壓縮在有一種情況下是很有必要的,那就是狀態空間非常龐大的時候(處理海量數據),此時空間不夠用,就必須狀態壓縮。
這道題比較煩人的是判斷迴文子串。因此需要一種能夠快速判斷原字符串的所有子串是否是迴文子串的方法,於是想到了“動態規劃”。
“動態規劃”最關鍵的步驟是想清楚“狀態如何轉移”,事實上,“迴文”是天然具有“狀態轉移”性質的:
一個迴文去掉兩頭以後,剩下的部分依然是迴文(這裏暫不討論邊界)。
依然從迴文串的定義展開討論:
1、如果一個字符串的頭尾兩個字符都不相等,那麼這個字符串一定不是迴文串;
2、如果一個字符串的頭尾兩個字符相等,纔有必要繼續判斷下去。
(1)如果裏面的子串是迴文,整體就是迴文串;
(2)如果裏面的子串不是迴文串,整體就不是迴文串。
即在頭尾字符相等的情況下,裏面子串的迴文性質據定了整個子串的迴文性質,這就是狀態轉移。因此可以把“狀態”定義爲原字符串的一個子串是否爲迴文子串。
第 1 步:定義狀態
dp[i][j]
表示子串 s[i, j]
是否爲迴文子串。
第 2 步:思考狀態轉移方程
這一步在做分類討論(根據頭尾字符是否相等),根據上面的分析得到:
dp[i][j] = (s[i] == s[j]) and dp[i + 1][j - 1]
分析這個狀態轉移方程:
(1)“動態規劃”事實上是在填一張二維表格,i
和 j
的關係是 i <= j
,因此,只需要填這張表的上半部分;
(2)看到 dp[i + 1][j - 1]
就得考慮邊界情況。
邊界條件是:表達式 [i + 1, j - 1]
不構成區間,即長度嚴格小於 2
,即 j - 1 - (i + 1) + 1 < 2
,整理得 j - i < 3
。
這個結論很顯然:當子串 s[i, j]
的長度等於 2
或者等於 3
的時候,我其實只需要判斷一下頭尾兩個字符是否相等就可以直接下結論了。
如果子串 s[i + 1, j - 1]
只有 1 個字符,即去掉兩頭,剩下中間部分只有 11 個字符,當然是迴文;
如果子串 s[i + 1, j - 1]
爲空串,那麼子串 s[i, j]
一定是迴文子串。
因此,在 s[i] == s[j]
成立和 j - i < 3
的前提下,直接可以下結論,dp[i][j] = true
,否則才執行狀態轉移。
第 3 步:考慮初始化
初始化的時候,單個字符一定是迴文串,因此把對角線先初始化爲 1
,即 dp[i][i] = 1
。
事實上,初始化的部分都可以省去。因爲只有一個字符的時候一定是迴文,dp[i][i]
根本不會被其它狀態值所參考。
第 4 步:考慮輸出
只要一得到 dp[i][j] = true
,就記錄子串的長度和起始位置,沒有必要截取,因爲截取字符串也要消耗性能,記錄此時的迴文子串的“起始位置”和“迴文長度”即可。
第 5 步:考慮狀態是否可以壓縮
因爲在填表的過程中,只參考了左下方的數值。事實上可以壓縮,但會增加一些判斷語句,增加代碼編寫和理解的難度,丟失可讀性。在這裏不做狀態壓縮。
下面是編碼的時候要注意的事項:總是先得到小子串的迴文判定,然後大子串才能參考小子串的判斷結果。
思路是:
1、在子串右邊界 j 逐漸擴大的過程中,枚舉左邊界可能出現的位置;
2、左邊界枚舉的時候可以從小到大,也可以從大到小。
希望大家能夠自己動手,畫一下表格,相信會對“動態規劃”作爲一種“表格法”有一個更好的理解。
class Solution:
def longestPalindrome(self, s: str) -> str:
size = len(s)
if size < 2:
return s
dp = [[False for _ in range(size)] for _ in range(size)]
max_len = 1
start = 0
for i in range(size):
dp[i][i] = True
for j in range(1, size):
for i in range(0, j):
if s[i] == s[j]:
if j - i < 3:
dp[i][j] = True
else:
dp[i][j] = dp[i + 1][j - 1]
else:
dp[i][j] = False
if dp[i][j]:
cur_len = j - i + 1
if cur_len > max_len:
max_len = cur_len
start = i
return s[start:start + max_len]
複雜度分析:
時間複雜度:O(N 2 )。
空間複雜度:O(N 2 ),二維 dp 問題,一個狀態得用二維有序數對錶示,因此空間複雜度是O(N 2)。
本文參考此處。