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