动态规划之四键键盘
動態規劃之四鍵鍵盤
如何在 N 次敲擊按鈕后得到最多的 A? 我們窮舉唄, 每次有對于每次按鍵, 我們可以窮舉四種可能, 很明顯就是?個動態規劃問題。
第?種思路(超時)
這種思路會很容易理解, 但是效率并不?, 我們直接?流程: 對于動態規劃問題, ?先要明?有哪些「狀態」 , 有哪些「選擇」 。
具體到這個問題, 對于每次敲擊按鍵, 有哪些「選擇」 是很明顯的: 4 種,就是題?中提到的四個按鍵, 分別是 A 、 C-A 、 C-C 、 C-V ( Ctrl 簡寫為 C ) 。
接下來, 思考?下對于這個問題有哪些「狀態」 ? 或者換句話說, 我們需要
知道什么信息, 才能將原問題分解為規模更?的?問題?
你看我這樣定義三個狀態?不?: 第?個狀態是剩余的按鍵次數, ? n 表?; 第?個狀態是當前屏幕上字符 A 的數量, ? a_num 表?; 第三個狀態是剪切板中字符 A 的數量, ? copy 表?。
如此定義「狀態」 , 就可以知道 base case: 當剩余次數 n 為 0 時, a_num
就是我們想要的答案
結合剛才說的 4 種「選擇」 , 我們可以把這?種選擇通過狀態轉移表?出來:
dp(n - 1, a_num + 1, copy), # A 解釋: 按下 A 鍵, 屏幕上加?個字符 同時消耗 1 個操作數dp(n - 1, a_num + copy, copy), # C-V 解釋: 按下 C-V 粘貼, 剪切板中的字符加?屏幕 同時消耗 1 個操作數dp(n - 2, a_num, a_num) # C-A C-C 解釋: 全選和復制必然是聯合使?的, 剪切板中 A 的數量變為屏幕上 A 的數量 同時消耗 2 個操作數這樣可以看到問題的規模 n 在不斷減?, 肯定可以到達 n = 0 的 base case, 所以這個思路是正確的:
def maxA(N: int) -> int:# 對于 (n, a_num, copy) 這個狀態,# 屏幕上能最終最多能有 dp(n, a_num, copy) 個 Adef dp(n, a_num, copy):# base caseif n <= 0: return a_num;# ?種選擇全試?遍, 選擇最?的結果return max(dp(n - 1, a_num + 1, copy), # Adp(n - 1, a_num + copy, copy), # C-Vdp(n - 2, a_num, a_num) # C-A C-C)# 可以按 N 次按鍵, 屏幕和剪切板?都還沒有 Areturn dp(N, 0, 0)這個解法應該很好理解, 因為語義明確。 下?就繼續?流程, ?備忘錄消除?下重疊?問題:
def maxA(N: int) -> int:# 備忘錄memo = dict()def dp(n, a_num, copy):if n <= 0: return a_num;# 避免計算重疊?問題if (n, a_num, copy) in memo:return memo[(n, a_num, copy)]memo[(n, a_num, copy)] = max(# ?種選擇還是?樣的)return memo[(n, a_num, copy)]return dp(N, 0, 0)這個算法的時間復雜度不容易分析。 我們可以把這個 dp 函數寫成 dp 數組:
dp[n][a_num][copy] # 狀態的總數(時空復雜度) 就是這個三維數組的體積我們知道變量 n 最多為 N , 但是 a_num 和 copy 最多為多少我們很難計算, 復雜度起碼也有 O(N^3) 把。 所以這個算法并不好, 復雜度太?, 且已經?法優化了。
這也就說明, 我們這樣定義「狀態」 是不太優秀的, 下?我們換?種定義dp 的思路。
第?種思路
繼續?流程, 「選擇」 還是那 4 個,
但是這次我們只定義?個「狀態」 , 也就是剩余的敲擊次數 n。
這個算法基于這樣?個事實, 最優按鍵序列?定只有兩種情況:
要么?直按 A : A,A,…A(當 N ?較?時) 。
要么是這么?個形式: A,A,…C-A,C-C,C-V,C-V,…C-V(當 N ?較?時) 。
因為字符數量少(N ?較?) 時, C-A C-C C-V 這?套操作的代價相對?較?, 可能不如?個個按 A ; ?當 N ?較?時, 后期 C-V 的收獲肯定很?。
這種情況下整個操作序列?致是: 開頭連按?個 A , 然后 C-A C-C組合再接若? C-V , 然后再 C-A C-C 接著若? C-V , 循環下去。
換句話說, 最后?次按鍵要么是 A 要么是 C-V 。 明確了這?點, 可以通過這兩種情況來設計算法:
int[] dp = new int[N + 1]; // 定義: dp[i] 表? i 次操作后最多能顯?多少個 A for (int i = 0; i <= N; i++)dp[i] = max(這次按 A 鍵,這次按 C-V)對于「按 A 鍵」 這種情況, 就是狀態 i - 1 的屏幕上新增了?個 A ?已, 很容易得到結果:
// 按 A 鍵, 就?上次多?個 A ?已 dp[i] = dp[i - 1] + 1;但是, 如果要按 C-V , 還要考慮之前是在哪? C-A C-C 的。
剛才說了, 最優的操作序列?定是 C-A C-C 接著若? C-V , 所以我們??
個變量 j 作為若? C-V 的起點。 那么 j 之前的 2 個操作就應該是 C-AC-C 了:
其中 j 變量減 2 是給 C-A C-C 留下操作數, 看個圖就明?了:
此算法就完成了, 時間復雜度 O(N2)O(N^2)O(N2), 空間復雜度 O(N)O(N)O(N), 這種解法應該是?較?效的了
最后總結
動態規劃難就難在尋找狀態轉移, 不同的定義可以產?不同的狀態轉移邏輯, 雖然最后都能得到正確的結果, 但是效率可能有巨?的差異。
回顧第?種解法, 重疊?問題已經消除了, 但是效率還是低, 到底低在哪?呢? 抽象出遞歸框架:
def dp(n, a_num, copy):dp(n - 1, a_num + 1, copy), # Adp(n - 1, a_num + copy, copy), # C-Vdp(n - 2, a_num, a_num) # C-A C-C看這個窮舉邏輯, 是有可能出現這樣的操作序列 C-A C-C, C-A C-C... 或者C-V,C-V,... 。 然這種操作序列的結果不是最優的, 但是我們并沒有想辦法規避這些情況的發?, 從?增加了很多沒必要的?問題計算
回顧第?種解法, 我們稍加思考就能想到, 最優的序列應該是這種形式: A,A…C-A,C-C,C-V,C-V…C-A,C-C,C-V… 。
根據這個事實, 我們重新定義了狀態, 重新尋找了狀態轉移, 從邏輯上減少了?效的?問題個數, 從?提?了算法的效率
超強干貨來襲 云風專訪:近40年碼齡,通宵達旦的技術人生總結
- 上一篇: LeetCode 打家劫舍问题
- 下一篇: 动态规划之正则表达