彻底掌握动态规划,第一节
Hello,我是 Alex 007,一個熱愛計算機編程和硬件設計的小白,為啥是007呢?因為叫 Alex 的人太多了,再加上每天007的生活,Alex 007就誕生了。
動態規劃-諾基亞耐摔性測試
老師說藍橋杯被列為A類競賽,讓我再把算法好好看看,我一想我那可憐的算法,當時就想跟老師說:
碰巧今天LeetCode每日一題考到了動態規劃,問了工作室的幾個學弟學妹,對動態規劃也掌握的也不是很好,覺得要寫一篇文章好好講講。
一、引子(諾基亞耐摔性測試)
1.題目描述
我以前啊,特別喜歡收藏一些東西,其中就包括那個從樓上掉下來還能砸穿地板的神機——諾基亞。
當然這些都只是吹噓啊,有一天呢我就突然想測試一下諾基亞手機的耐摔性到底怎么樣,于是就找了棟100層的大樓,看看能摔倒幾層,當手機在低樓層往下扔的時候,到地上都不會碎,而在高樓層的時候往下扔手機才會碎。
所以大樓中間必定存在一個臨界的樓層,在臨界樓層以下怎么扔,手機都是不碎的,并且這個手機還可以繼續做測試,但如果超過了臨界樓層,手機就會被摔碎,并且碎了之后手機就不能再用了。
假設我手里有N個諾基亞可以用來做檢測,問最少要扔多少次才能找到這個臨界樓層。
2.題目分析
剛看到這個題的時候我腦子里冒出來好多想法,什么迭代、遞歸、二分、中值定理,一大堆,但是都一一排除了,一時間也沒有什么好想法。
那就只能從最簡單的情況開始一步一步的分析:
1個手機:
如果只有一個手機的話,肯定不能采用二分法上來就從50層開始扔,要是直接碎了就沒得玩了,雖然確定了范圍在0~50層之間,但是手機摔碎了,不能繼續再測試了。
所以,當只有一個手機的時候,我們只能用最笨的方法,從第一層開始,一層一層的扔,直到摔碎才能找到臨界樓層。
這種前提下,最差的情況要扔100次,最好的情況只需要扔1次就可以了。
無限個手機:
如果我收藏了很多諾基亞手機,扔完了還有,這種情況下,我們就可以采用二分法了,首先在50層開始扔,如果手機碎了那么臨界樓層就在1 ~ 50層之間,否則就在51 ~ 100層之間,然后繼續用二分法測試。
這種情況下,我們假設最多要扔M次,滿足條件2M>=100,解得M>=log2100,而log2100=2log210,我們可以估計一下log210的大小:log28=3<log210<log216=4,所以可以計算出M>6,因此要扔7次就可以找到臨界樓層。
2個手機:
接下來我們就要分析一個比較復雜的情況,只有兩個手機,別看就這兩個手機,情況還挺復雜,我們假設第一個手機為A,第二個手機為B。
A手機分別嘗試10層、20層、30層、……100層,如果A手機在某次測試的時候碎了,則用B嘗試A-9、A-8、A-7、……A-1層。
舉個例子,假設A手機在嘗試第10層的時候沒碎,但是在嘗試第20層的時候碎了,說明臨界樓層在10 ~ 20層之間,接下來用B手機依次嘗試11層、12層、13層、……19層,總有一層會碎的,不碎的話說明20層就是臨界樓層。
此時,A最多扔10次,B最多扔9次,因此等間隔測試最多嘗試19次即可找到臨界樓層。
我們讓A每次跨越的距離縮小一層,這樣B所要測試的樓層也會相應的減少一層。
假設A手機第一次測試的樓層在第n層,第二次測試的樓層要跨越n-1層在第2n-1層,第三次測試的樓層跨越n-2層在第3n-3層,以此類推,假設我們最后可以讓A只跨越1層,那么可以列出計算式:
n+(n?1)+(n?2)+...+3+2+1=n(n?1)2>=100n + (n - 1) + (n - 2) + ... + 3 + 2 + 1 = \frac{n(n - 1)}{2}>=100 n+(n?1)+(n?2)+...+3+2+1=2n(n?1)?>=100
解得n>=13.65,我們取n=14,那么A的測試樓層就是:14、27、39、50、60、69、77、84、90、95、99、100,所以A最多測試12次。
假設A手機在14層的時候碎了,B手機要測試1 ~ 13層,加上A手機的一次要測試14次。
假設A手機在27層的時候碎了,A手機測試了2次,B手機要測試15 ~ 26層,也就是12次,加起來還是要測試14次。
所以,第二種方案要測試12 ~ 14次即可找到臨界樓層,最多就需要14次。
N個手機:
現在我們再來討論一下有N個手機的情況,為了讓問題的普適性再增強一點,再假設有T層樓,這樣問題就變成了,有T層樓和N個手機,求出最壞情況下經過多少次測試能找到臨界樓層。
如果把它用數學語言來描述的話,就是求一個函數:
M(T,N)=?M(T,N)=? M(T,N)=?
這里我們還是先從最簡單的情況開始分析,可以畫一個表格,行表示樓層T,列表示手機數N:
| 1 | 1 | 1 | 1 | 1 | 1 |
| 2 | 2 | ||||
| 3 | 3 | ||||
| … | … | ||||
| n | n |
我們先把比較簡單的N層樓1個手機和N個手機1層樓的情況分析完,并且填入到表格中,接下來分析其它的情況。
假設第1次扔手機在第k層,如果手機碎了,那么臨界樓層在1 ~ k層之間,接下來的問題就變成了在1 ~ k層之間用N - 1個手機測試:
M(k?1,N?1)=?M(k-1,N-1)=? M(k?1,N?1)=?
如果A手機在第k層沒碎,接下來的問題就變成了在k+1 ~ T層之間用N個手機測試:
M(T?k,N)=?M(T-k,N)=? M(T?k,N)=?
我們的目標是求出最壞的情況需要測試多少次,因此需要對兩種情況的測試取最大值,同時還要加上第1次測試的次數:
Mk(T,N)=1+max{Mk(k?1,N?1),Mk(T?k,N)}M_k(T,N)=1+max\{M_k(k-1,N-1),M_k(T-k,N)\} Mk?(T,N)=1+max{Mk?(k?1,N?1),Mk?(T?k,N)}
現在還有個k沒有確定值,不過可以確定其范圍是:1<=k<=T,可以通過循環來計算,我們是想求最優情況下扔多少次手機才能找到臨界樓層,因此可以列出下式:
M(T,N)=min{M1(T,N),M2(T,N),M3(T,N),...,Mk(T,N),...,MT(T,N)}M(T,N)=min\{M_1(T,N),M_2(T,N),M_3(T,N),...,M_k(T,N),...,M_T(T,N)\} M(T,N)=min{M1?(T,N),M2?(T,N),M3?(T,N),...,Mk?(T,N),...,MT?(T,N)}
3.Code
有了這個公式,并且公式里的值都確定了,我們就可以開始敲代碼解決問題了:
import sysif __name__ == '__main__':floors, phones = 100, 10# 1.創建dp數組dp = [[0 for _ in range(phones)] for _ in range(floors)]# 2.填充第一列for t in range(floors):dp[t][0] = t + 1# 3.填充第一行for n in range(phones):dp[0][n] = 1# 4.根據公式求解其它值for t in range(1, floors):for n in range(1, phones):minNum = sys.maxsizefor k in range(1, t + 1):minNum = min(minNum, 1 + max(dp[k - 1][n - 1], dp[t - k - 1][n]))dp[t][n] = minNumfor floor in range(10):print(f"Floor = {floor + 1}:\t", end="")for num in range(10):print(dp[floor][num], end="\t")print()下面奉上標準答案:
二、定義
如果你能夠完全理解上面講的內容并且能夠自己實現,那么恭喜你,通過這么一個簡單的扔手機的問題,我們就把無數人頭疼的動態規劃初步理解了。
可能你現在還有點懵,不過沒關系,動態規劃是一種用途很廣的問題求解方法,它本身并不是一個特定的算法,而是一種思想,一種手段。對動態規劃的掌握情況很大程度上能直接影響一個選手的分析和建模能力。
我們先來看看動態規劃的幾個核心概念。
1.階段和階段變量
用動態規劃求解一個問題時,需要將問題的全部過程恰當的分成若干個相互聯系的階段,以便按照一定的次序去求解。
描述階段的變量稱為階段變量,通常用k表示,階段的劃分一般是根據時間和空間的自然特征來劃分,同時階段的劃分要便于把問題轉換成多階段決策過程。
Mk(T,N)
2.狀態和狀態變量
某一階段的出發位置稱為狀態,通常一個階段包含若干狀態。一般的,狀態可由變量來描述,用來描述狀態的變量稱為狀態變量。
M50(T,N)
3.狀態轉移方程
前一階段的終點是后一階段的起點,對前一階段的狀態做出某種決策產生后一階段的狀態,這種關系描述了由k階段到k+1階段狀態的演變規律,稱為狀態轉移方程。
Mk(T,N)=1+max{Mk(k-1,N-1),Mk(T-k,N)}
4.總結
動態規劃最重要的是掌握它的核心思想:將原問題分解成子問題進行求解。
解決動態規劃問題大致有以下四步:
第1次扔手機在第k層
- 手機碎了 =》 在1 ~ k層之間用N - 1個手機測試
- 手機沒碎 =》 在k+1 ~ T層之間用N個手機測試
子問題的求解方程:
- 手機碎了 =》 M(k-1,N-1)
- 手機沒碎 =》 M(T-k,N)
狀態轉移方程:Mk(T,N)=1+max{Mk(k-1,N-1),Mk(T-k,N)}
三、最長公共子序列Logest Common Subsequence
最長公共序列問題是動態規劃的經典例題,在做題之前首先要明確幾個概念:
一個序列X=x1x2x3…xn,刪除其中任意若干項,剩余的序列叫做X的一個子序列。
如果序列Z既是序列X的子序列,同時也是Y的子序列,則稱序列Z為序列X和序列Y的公共子序列,空序列是任何兩個序列的公共子序列。
序列X和序列Y的公共子序列中長度最長的叫做序列X和序列Y的最長公共子序列。
1.劃分狀態
設序列X=x1x2x3…xm,序列Y=y1y2y3…yn,序列Z=z1z2z3…zk是序列X和序列Y的最長公共子序列。
2.狀態表示
從劃分子狀態可以看出,如果Xm=Yn,那么我們應該求解Xm-1,Yn-1的一個LCS,并且將Xm=Yn加入到這個LCS的末尾,這樣得到的一個新的LCS就是所求最長公共子序列。
如果Xm!=Yn,需要求解兩個子問題,分別是Xm-1,Y的LCS和X,Yn-1的LCS,兩個LCS中較長者就是X和Y的一個LCS。
可以看出LCS問題具有重疊子問題性質,為了求X和Y的LCS,需要分別求出Xm-1,Y的LCS和X,Yn-1的LCS,子問題中又包含了求出Xm-1,Yn-1的LCS的子問題的子問題。
3.狀態轉移
根據上面的分析,我們可以得出下面的公式:
LCS(i,j)={0,i=0,j=01+LCS(i?1,j?1),i,j>0,xi=yimax{LCS(i,j?1),LCS(i?1,j)},i,j>0,xi!=yiLCS(i,j)=\left\{ \begin{aligned} 0,& {i=0,j=0}\\ 1+LCS(i-1,j-1),& i,j>0,x_i=y_i\\ max\{LCS(i,j-1),LCS(i-1,j)\},& i,j>0,x_i!=y_i\\ \end{aligned} \right. LCS(i,j)=??????0,1+LCS(i?1,j?1),max{LCS(i,j?1),LCS(i?1,j)},?i=0,j=0i,j>0,xi?=yi?i,j>0,xi?!=yi??
接下來就可以直接根據這個公式coding了。
4.Code
def LcsTraceBack(dp, directions, string, length1, length2):s = []# dp數組不為None時while dp[length1][length2]:char = directions[length1][length2]# 匹配成功,插入該字符,并向左上角找下一個if char == 'ok':s.append(string[length1 - 1])length1 -= 1length2 -= 1# 根據標記,向左找下一個if char == 'left':length2 -= 1# 根據標記,向上找下一個if char == 'up':length1 -= 1s.reverse()return ''.join(s)def Lcs(str1, str2):length1, length2 = len(str1), len(str2)# 生成字符串長度加1的0矩陣DP用來保存對應位置匹配的結果DP = [[0 for _ in range(length2 + 1)] for _ in range(length1 + 1)]# direction用來記錄轉移方向directions = [['' for _ in range(length2 + 1)] for _ in range(length1 + 1)]for p1 in range(length1):for p2 in range(length2):# 字符匹配成功,則該位置的值為左上方的值加1if str1[p1] == str2[p2]:DP[p1 + 1][p2 + 1] = DP[p1][p2] + 1directions[p1 + 1][p2 + 1] = 'ok'# 左值大于上值,則該位置的值為左值,并標記回溯時的方向為leftelif DP[p1 + 1][p2] > DP[p1][p2 + 1]:DP[p1 + 1][p2 + 1] = DP[p1 + 1][p2]directions[p1 + 1][p2 + 1] = 'left'# 上值大于左值,則該位置的值為上值,并標記回溯時的方向為upelse:DP[p1 + 1][p2 + 1] = DP[p1][p2 + 1]directions[p1 + 1][p2 + 1] = 'up'return LcsTraceBack(DP, directions, str1, length1, length2), directionsif __name__ == '__main__':answer, direction = Lcs('ABCBDAB', 'BDCABA')print(answer)5.練習題
最長公共子序列Lcs 51Nod - 1006
總結
以上是生活随笔為你收集整理的彻底掌握动态规划,第一节的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 309. Best Time to Bu
- 下一篇: 做项目开发你必须得掌握的知识:设计模式