持續更新。。。。。。
2.1斐波那契系列問題
2.2矩陣系列問題
2.3跳躍系列問題
3.1 01揹包
3.2 完全揹包
3.3多重揹包
3.4 一些變形選講
2.1斐波那契系列問題
在數學上,斐波納契數列以如下被以遞歸的方法定義:F(0)=0,F(1)=1, F(n)=F(n-1)+F(n-2)(n>=2,n∈N*)根據定義,前十項爲1, 1, 2, 3, 5, 8, 13, 21, 34, 55
例1:給定一個正整數n,求出斐波那契數列第n項(這時n較小)
解法一:完全抄定義
def f(n):
if n==1 or n==2:
return 1
return f(n-1)+f(n-2)
分析一下,爲什麼說遞歸效率很低呢?咱們來試着運行一下就知道了:
比如想求f(10),計算機裏怎麼運行的?
想算出f(10),就要先算出F(9),
想算出f(9),就要先算出F(8),
想算出f(8),就要先算出F(7),
想算出f(7),就要先算出F(6)……
兜了一圈,我們發現,有相當多的計算都重複了,比如紅框部分:
那如何解決這個問題呢?問題的原因就在於,我們算出來某個結果,並沒有記錄下來,導致了重複計算。那很容易想到如果我們把計算的結果全都保存下來,按照一定的順序推出n項,就可以提升效率
解法2:
def f1(n):
if n==1 or n==2:
return 1
l=[0]*n #保存結果
l[0],l[1]=1,1 #賦初值
for i in range(2,n):
l[i]=l[i-1]+l[i-2] #直接利用之前結果
return l[-1]
可以看出,時間o(n),空間o(n)。
繼續思考,既然只求第n項,而斐波那契又嚴格依賴前兩項,那我們何必記錄那麼多值浪費空間呢?只記錄前兩項就可以了。
解法3:
def f2(n):
a,b=1,1
for i in range(n-1):
a,b=b,a+b
return a
補充:
- pat、藍橋杯等比賽原題:求的n很大,F(N)模一個數。應每個結果都對這個數取模,否則:第一,計算量巨大,浪費時間;第二,數據太大,爆內存,
- 對於有多組輸入並且所求結果類似的題,可以先求出所有結果存起來,然後直接接受每一個元素,在表中查找相應答案
- 此題有快速冪算法,但是礙於篇幅和同學們水平有限,不再敘述,可以自行學習。
例2:一隻青蛙一次可以跳上1級臺階,也可以跳上2級。求該青蛙跳上一個n級的臺階總共有多少種跳法。
依舊是找遞推關係:
1)跳一階,就一種方法
2)跳兩階,它可以一次跳兩個,也可以一個一個跳,所以有兩種
3)三個及三個以上,假設爲n階,青蛙可以是跳一階來到這裏,或者跳兩階來到這裏,只有這兩種方法。
它跳一階來到這裏,說明它上一次跳到n-1階,
同理,它也可以從n-2跳過來
f(n)爲跳到n的方法數,所以,f(n)=f(n-1)+f(n-2)
優化思路與例1類似,請自行思考。
例3:我們可以用2*1的小矩形橫着或者豎着去覆蓋更大的矩形。請問用n個2*1的小矩形無重疊地覆蓋一個2*n的大矩形,總共有多少種方法?
N=1: 只有一種
N=2,兩種:
N=3:
讀到這裏,你們應該能很快想到,依舊是斐波那契式遞歸啊。
對於n>=3:怎麼能覆蓋到三?
只有兩種辦法,從n-1的地方豎着放了一塊,或者從n-2的位置橫着放了兩塊
例4:給定一個由0-9組成的字符串,1可以轉化成A,2可以轉化成B。依此類推。。25可以轉化成Y,26可以轉化成z,給一個字符串,返回能轉化的字母串的有幾種?
比如:123,可以轉化成
1 、2 、3變成ABC,
12 、3變成LC,
1 、23變成AW
三種,返回三,
比如99999,就一種:iiiii,返回一。
分析:求i位置及之前字符能轉化多少種。
兩種轉化方法
1)字符i自己轉換成自己對應的字母
2)和前面那個數組成兩位數,然後轉換成對應的字母
假設遍歷到i位置,判斷i-1位置和i位置組成的兩位數是否大於26,大於就沒有第二種方法,f(i)=f(i-1),如果小於26, f(i)=f(i-1)+f(i-2)
2.2矩陣系列問題
例5:給一個由數字組成的矩陣,初始在左上角,要求每次只能向下或向右移動,路徑和就是經過的數字全部加起來,求可能的最小路徑和。
1 3 5 9
8 1 3 4
5 0 6 1
8 8 4 0
路徑:1 3 1 0 6 1 0路徑和最小,返回12
分析:我們可以像之前一樣,暴力的把每一種情況都試一次,但是依舊會造成過多的重複計算,以本題爲例子最後解釋一下暴力慢在哪裏,以後不再敘述了。
比如本題來講,我們嘗試如下路徑:
有很多路是重複走過的一遍。
再進一步說:
從1到6位置,有很多路可以走,直觀感受一下:
所有路中,一定會有和最小的,但是我們並不知道,每次嘗試一次1->6->終點的路線時,我們把所有的情況都算了一遍,這過程中我們浪費了相當多的有效信息。
這就是暴力的結果。
優化做法:生成和矩陣相同大小的二維表,用來記錄到起點每個位置的最小路徑和
接下來帶着大家真正進入動態規劃;
第一步:初始化(對於本題來說,第一列和第一行,我們別無選擇,就一條路,因此,我們可以直接確定答案)
第二步:確定其餘位置如何推出(我們稱爲狀態轉移方程)
直觀來說,每個位置只可能是從上面,或者左邊走來的:
對於普遍的位置i,j,只有i-1,j和i,j-1這兩個位置可以一步走到這裏,所以
DP[i,j]=min(DP[i,j-1],DP[i-1,j])+L[i,j](之前的最優解加上本位置的數字)
繼續優化:和之前一樣,這個式子實際上也是嚴格依賴兩個值,一個是左邊的值,一個是上面的值,所以,我們按之前的思路,應該可以想到可以壓縮空間。
我們嘗試用一維的空間來解題:
想象這是我們的第一行答案:
我們如何利用僅有的一維空間來更新出下一行呢?
我們要想:
- 我們需要左面的數字,所以,本位置的左邊必須是更新過的數字(否則就是左上的位置了),所以應該從左往右更新。
- 我們需要上面的數字,這個不需要更新,本來就需要本位置的舊數字。
本題第二行爲:8,1,3,4
第一行答案爲
依次更新:
更新A:
(只能向下走)
更新B:
(比較從左邊來和從上面來哪裏比較小)
更新C:
更新D:
最後我們可以發現,僞代碼是這樣的:
For i 0 -> 高度:
For j 0 -> 寬度
DP[j]=min(DP[j-1],DP[j])+L[i,j]
時間不變,空間優化到o(min(高,寬))
例6:給一個由數字組成的矩陣,初始在左上角,要求每次只能向下或向右移動,路徑和就是經過的數字全部加起來,求可能的最大路徑和。
和例5只差一個“大”字,請自己思考
例7:一個矩陣,初始在左上角,要求每次只能向下或向右移動,求到終點的方法數。
和例5,6類似,只是方法數應該等於,左邊的方法數加上上面的方法數
第二章末練習
1
一個只包含'A'、'B'和'C'的字符串,如果存在某一段長度爲3的連續子串中恰好'A'、'B'和'C'各有一個,那麼這個字符串就是純淨的,否則這個字符串就是暗黑的。例如:
BAACAACCBAAA 連續子串"CBA"中包含了'A','B','C'各一個,所以是純淨的字符串
AABBCCAABB 不存在一個長度爲3的連續子串包含'A','B','C',所以是暗黑的字符串
你的任務就是計算出長度爲n的字符串(只包含'A'、'B'和'C'),有多少個是暗黑的字符串。(網易17校招原題)
2、X國的一段古城牆的頂端可以看成 2*N個格子組成的矩形(如下圖所示),現需要把這些格子刷上保護漆。
你可以從任意一個格子刷起,刷完一格,可以移動到和它相鄰的格子(對角相鄰也算數),但不能移動到較遠的格子(因爲油漆未乾不能踩!)
比如:a d b c e f 就是合格的刷漆順序。
c e f d a b 是另一種合適的方案。
當已知 N 時,求總的方案數。當N較大時,結果會迅速增大,請把結果對 1000000007 (十億零七) 取模。
3.1 01揹包
入門了動態規劃之後,我們來看一個經典系列問題:揹包問題
這是最基礎的揹包問題,特點是:每種物品僅有一件,可以選擇放或不放。
用子問題定義狀態:
f[i][j]表示前i件物品恰放入一個容量爲j的揹包可以獲得的最大價值。則其狀態轉移方程爲:
“將前i件物品放入容量爲j的揹包中”這個子問題,若只考慮第i件物品的策略(放或不放),那麼就可以轉化爲一個只牽扯前i−1件物品的問題。
如果不放第i件物品,那麼問題就轉化爲“前i−1件物品放入容量爲j的揹包中”,價值爲f[i−1][j];
如果放第i件物品,那麼問題就轉化爲“前i−1件物品放入剩下的容量爲j−c[i]的揹包中”,此時能獲得的最大價值就是f[i−1][j−w[i]],再加上通過放入第i件物品獲得的價值v[i]。
因此得出上面的式子。
繼續優化空間(利用之前提到的知識):
如果我們壓縮到一維空間解題,這次我們需要的是上面的位置和左上的位置,也就是說,我們需要左邊的位置是沒被更新過的,得出更新順序應該從右往左:
for i in range(1,n+1):
for j in range(v,-1,-1)
f[j] = max(f[j], f[j - w[i]] + v[i]);
3.2 完全揹包
這個問題非常類似於01揹包問題,所不同的是每種物品有無限件。也就是從每種物品的角度考慮,與它相關的策略已並非取或不取兩種,而是有取0件、取1件、取2件……等很多種。如果仍然按照解01揹包時的思路,很容易得出:
這跟01揹包問題一樣有O(VN)個狀態需要求解,但求解每個狀態的時間已經不是常數了
而是,總的複雜度可以認爲是,將01揹包問題的基本思路加以改進,得到了這樣一個清晰的方法。這說明01揹包問題的方程的確是很重要,可以推及其它類型的揹包問題。但我們還是試圖改進這個複雜度。
我們可以知道,對於一個普遍位置w,當前物品代價爲2的話,下圖中紅色區域就是和位置w的取值相關的一些數值:
對當前物品的決策就依次是:不拿、拿一個、拿兩個、拿三個(對應上面式子中的k)
我們算法優化的思路就是不斷去除重複計算,顯然我們可以繼續優化這個式子。
請思考:我們的E3位置是如何得出的?其實是根據三個紅色區域得出的,但是我們算位置w時又算了一遍,顯然是重複了。而E3其實包含了不拿、拿一個、拿兩個這些情況中的最優解,我們算w時直接用就可以了。
給出模板代碼:
for (int i = 1; i <= n; i++)
for (int j = w[i]; j <= V; j++)
f[j] = max(f[j], f[j - w[i]] + v[i]);
對比兩種揹包:
這個代碼與01揹包的代碼只有j的循環次序不同而已。爲什麼這樣一改就可行呢?
首先想想爲什麼01揹包中要按照j=V...0 j=V...0j=V...0的逆序來循環。這是因爲要保證第i次循環中的狀態f[i][j]是由狀態f[i−1][j−w[i]]遞推而來。換句話說,這正是爲了保證每件物品只選一次,保證在考慮“選入第i件物品”這件策略時,依據的是一個絕無已經選入第i件物品的子結果f[i−1][j−w[i]]。
而現在完全揹包的特點恰是每種物品可選無限件,所以在考慮“加選一件第i ii種物品”這種策略時,卻正需要一個可能已選入第i種物品的子結果f[i][j−w[i]],所以就可以並且必須採用j=0...V j=0...Vj=0...V的順序循環。這就是這個簡單的程序爲何成立的道理。
最終給出狀態轉移方程給不明白的同學看:
(也可以通過數學導出此式)
3.3多重揹包
和之前的揹包不同,每種物品不是隻有一件,也不是有無限件,這次的每種物品的數量都是有限制的,我們對於每種物品,可以選擇拿一件、兩件……p[i]件。
我們借用上一種問題的圖:
看起來是類似的,位置w依舊和紅色區域相關,但是我們可以直接根據E3來求出位置w嗎?是不能的,因爲條件變了,每種物品不是無限的,可能在w位置,圖中橢圓圈出的位置代表着需要拿三個,但是如果規定最多拿兩個,我們這種算法就出問題了。
一種做題思路:把每個物品都按01揹包做:比如第i種物品,我們就按有p[i]件相同的物品。每一種物品都是如此,按01揹包做就可以了。(但是顯然很蠢)
改進:
我們平時買東西時,難道帶的全是一元的硬幣嗎?當然不是,只要手中的錢可以湊出商品的價格即可,比如9元的東西,我不一定用九個硬幣(揹包問題的物品)來付錢,可以5元+4個1元。
揹包問題也一樣,我們不一定要全部拆成1的物品,只要我們的物品可以代表0——>p[i]的所有情況,我們就認爲這種策略是正確的。
那如何拆p[i]個物品可以保證我們的物品可以代表0——>p[i]的所有情況呢?這裏要藉助2進制思想。
一個n位的二進制數可以取0到2的n次方-1,第i位代表的是2的i-1次方。
對應到物品:
我們的p[i]=15,我們怎樣拆呢?
1+2+4+8即可,這四個數一定可以組合出0-15的任何一個數。
二進制拆分代碼如下:
for (int i = 1; i <= n; i++) {
int num = min(p[i], V / w[i]);
for (int k = 1; num > 0; k <<= 1) {
if (k > num) k = num;
num -= k;
for (int j = V; j >= w[i] * k; j--)
f[j] = max(f[j], f[j - w[i] * k] + v[i] * k);
}
}
3.4 一些變形選講
1)最常見的一些變形,甚至不能說是變形,上面也提到過,但是怕同學們不知道:
我們常見的問題中,一般是問最優解,可能是最大,或者最小,但是,問題也可能是方法的數量,這個時候,一般把狀態轉移方程中的max(min)改爲sum(求和)即可,當然,壓縮空間後的樣子還是需要自己寫。
2)初始化的細節問題
我們看到的求最優解的揹包問題題目中,事實上有兩種不太相同的問法。有的題目要求"恰好裝滿揹包"時的最優解,有的題目則並沒有要求必須把揹包裝滿。這兩種問法的區別是在初始化的時候有所不同。
如果是第一種問法,要求恰好裝滿揹包,那麼在初始化時除了f[0]爲0其它f[1...V]均設爲−∞,這樣就可以保證最終得到的f[N]是一種恰好裝滿揹包的最優解。
如果並沒有要求必須把揹包裝滿,而是隻希望價格儘量大,初始化時應該將f[0...V]全部設爲0。
爲什麼呢?可以這樣理解:初始化的f數組事實上就是在沒有任何物品可以放入揹包時的合法狀態。如果要求揹包恰好裝滿,那麼此時只有容量爲0的揹包可能被價值爲0的nothing “恰好裝滿”,其它容量的揹包均沒有合法的解,屬於未定義的狀態,它們的值就都應該是
−∞了。如果揹包並非必須被裝滿,那麼任何容量的揹包都有一個合法解“什麼都不裝”,這個解的價值爲0,所以初始時狀態的值也就全部爲0了。
3)常數優化
前面的代碼中有for(j=V...w[i]),還可以將這個循環的下限進行改進。
由於只需要最後f[j]的值,倒推前一個物品,其實只要知道f[j−w[n]]即可。以此類推,對以第j個揹包,其實只需要知道到f[j−sumw[j...n]]即可,代碼自行修改。
4)其實拆解二進制物品並不是多重揹包的最優解,但是最優的單調隊列思想寫起來有些繁瑣,可能以後會寫。
可以刷的題
鑑於有一些同學說簡單,我把去年寫的一些題解放在這裏:
皇后問題(位運算)
旅行商問題(認識狀態壓縮)
HDU1029 HDU1087 HDU1176 HDU1257 POJ1458
POJ2533 HDU1114 HDU1260 HDU1160
POJ1189 UVA12511 HDU2845 HBCPC2018 K