2017清北學堂(提升組精英班)集訓筆記——動態規劃Part1

誒~時間過得真是快,立刻到了第三天的動態規劃了,這是我認爲我最難理解的部分,嗯。。老師特別好——楊樂,講課速度能接受(聲音真的好聽啊。。滑稽臉)。html

經典例題:數字金字塔(Luogu 1216)數組

寫一個程序來查找從最高點到底部任意處結束的路徑,使路徑通過數字的和最大。每一步能夠走到左下方的點也能夠到達右下方的點。 優化

咱們如今這裏討論搜索如何實現:網站

狀態:目前在第x行第y列spa

行動:向左走,向右走設計

例如:一個底邊爲4的三角形共有八種狀態:3d

咱們按照通常的搜索思路,進行深度優先搜索:code

 1 void dfs(int x,int y,int val)
 2 {
 3     val+=a[x][y];//加上權值 
 4     if(x==n-1)
 5     {
 6         if(val>ans) ans=val;//更新更大的ans 
 7         return;
 8     }
 9     dfs(x+1,y,val);//往左邊走 
10     dfs(x+1,y+1,val);//往右邊走 
11 }

考慮時空效率,DFS確實很暴力啊,有沒有什麼優化呢??htm

咱們引入「冗餘搜索」這個概念:無用的,不會改變答案的搜索blog

例子:觀察下面兩個例子。用兩種方式都能到達3 行第 2 列,只是路徑不一樣,同時走到這個點兩條路權值和不同,其中一個總和爲 8,一個總和 12

那麼能夠觀察可得,總和爲 8 的搜索是冗餘(不會改變答案),即便不繼續搜索, 答案也不會改變

由於 12 往下搜索,不管往左往右,都會比 8 對應的路徑大。 

可見,冗餘就是剪枝的「枝」,那麼如何利用冗餘搜索,來優化程序呢?

咱們能夠對於每個位置記錄一個值 F,表明到此位置時,最大的路徑和是多
少 ,這樣若是搜到某一個位置時候,路徑和不大於記錄值F,說明這個搜索是冗餘搜索,直接退出,若是大於,就須要更新F值而且繼續搜索。

咱們就把這種搜索叫作記憶化搜索,根據以前的「記憶」來優化搜索;在這道題中,每一個位置的「記憶」就是最大的路徑和

 1 //T1:數字金字塔(記憶化搜索)
 2 void dfs(int x,int y,int val) 
 3 {
 4     val+=a[x][y];
 5     // 記憶化過程
 6     if(val<=f[x][y]) return;//發現冗餘搜索,退出 
 7     f[x][y]=val;//f[x][y]記錄這個點當前最大權值  8     if(x==n-1)//若是搜到了最後一個點,ans更新保存最大值,退出便可 
 9     {
10         if(val>ans) ans=val;
11         return;
12     }
13     dfs(x+1,y,val);//繼續搜索 
14     dfs(x+1,y+1,val);
15 }

經典例題:採藥(Luogu 1048)

辰辰是個天資聰穎的孩子,他的夢想是成爲世界上最偉大的醫師。爲此,他想拜附近最有威望的醫師爲師。醫師爲了判斷他的資質,給他出了一個難題。醫師把他帶到一個處處都是草藥的山洞裏對他說:「孩子,這個山洞裏有一些不一樣的草藥,採每一株都須要一些時間,每一株也有它自身的價值。我會給你一段時間,在這段時間裏,你能夠採到一些草藥。若是你是一個聰明的孩子,你應該可讓採到的草藥的總價值最大。」

草藥 1 :時間 71;價值 100
草藥 2 :時間 69;價值 1
草藥 3 :時間 1 ;價值 2
@最優選擇:草藥 2+3:總時間 70;總價值 3

這題是經典的揹包問題,和金字塔問題不一樣的地方在於:它是有重量限制的。

咱們仍是先用記憶化搜索來思考這個問題:

狀態:目前已經決定到第x件物品,當前揹包中物品總重量爲w,總價值爲v;

行動:這件物品取仍是不取;

約束:物品總重量不超過w(揹包總重量);

目標:物品總價值最大;

比較下列兩種狀況:

狀態相同x1=x2(當前搜索到同一件物品),w1=w2(當前總重量相)

價值不一樣:但它們的揹包總價值不一樣,其中v1<v2。(通過不一樣的路徑到達同一個點,可是後者的val更大)
則咱們能夠說狀態1冗餘的,由於它確定比狀態2要差。
*記憶化:對於每一個狀態(xw),記錄對應的v的最大值。

 1 //T5:採藥(記憶化搜索)
 2 void dfs(int t,int x,int val)//t爲剩餘時間,x爲當前決定的第幾株草藥,val爲總價值
 3 {
 4     //記憶化
 5     if(val<=f[t][x]) return;
 6     f[t][x]=val;
 7     if(x==n)//把草藥採摘完了,直接返回 
 8     {
 9         if(val>ans) ans=val;//更新最大值ans 
10         return;
11     }
12     dfs(t,x+1,val);
13     if(w[x]<=t) dfs(t-w[x],x+1,val+v[x]);//若是咱們還有時間,繼續採摘! 
14 }

那好的,說完記憶化搜索咱們回到正題:動態規劃啦!記憶化搜索是DP的基礎。

咱們再回到數字金字塔這個問題來,下圖的黑色三角形是咱們記憶化搜索的路徑,咱們想一想,是否是能夠不經過記憶化搜索就能獲得這個黑色三角形??

最優性:設走到某一個位置的時候,它達到了路徑最大值,那麼在這之前,它走的每一步都是最大值。

-考慮這條最優的路徑:每一步均達到了最大值

 

最優性的好處:要達到一個位置的最優值,它的前一步也必定是最優的。

-考慮圖中位置,若是它要到達最優值,有兩個選擇,從左上方或者右上方的最優值獲得:

因此從這裏,定義動態規劃(DP):只記錄狀態的最優值,並用最優值來推導出其餘的最優值。

記錄 F[i][j] 爲第 i 行第 j 列的路徑最大值,有兩種方法能夠推導:(兩個分支兩種狀態,選取最大) 

@順推:F[i][j] 來計算 F[i+1][j],F[i+1][j+1]

@逆推:F[i-1][j],F[i-1][j-1] 來計算 F[i][j]

這兩種思考方法也是動態規劃中最基本的兩種方法,解決絕大部分DP咱們均可以採用這樣的方法。

1 //T2:數字金字塔-順推(有點相似於記憶化搜索的思路)
2 f[0][0]=a[0][0];
3 for(int i=0;i<n-1;++i)
4 for(int j=0;j<=i;++j)//f數組爲最優值路徑(黑色金字塔,a爲源數據數組(紫色金字塔)
5 {
6     //分別用最優值來更新左下方和右下方
7     f[i+1][j]=max(f[i+1][j],f[i][j]+a[i+1][j]);//和當前的f[i+1][j]比較
8     f[i+1][j+1]=max(f[i+1][j+1],f[i][j]+a[i+1][j+1]);//和當前的f[i+1][j+1]比較
9 }
 1 //T4:數字金字塔-逆推(自頂向下) 
 2 f[0][0]=a[0][0];
 3 for(int i=0;i<n;++i)//單獨處理 
 4 {
 5     f[i][0]=f[i-1][0]+a[i][0];//最左的位置沒有左上方
 6     f[i][i]=f[i-1][i-1]+a[i][i];//最右的位置沒有右上方
 7     for(int j=1;j<i;++j)//在左上方和右上方取較大的
 8     f[i][j]=max(f[i-1][j-1],f[i-1][j])+a[i][j];
 9 }
10 //答案多是最後一行的任意一列
11 ans=0;
12 for(int i=0;i<n;++i)
13 ans=max(ans,f[n-1][i]);

*轉移方程:最優值之間的推導公式

@順推:

F[i+1][j] = MAX (F[i][j] + a[i+1][j]);

F[i+1][j+1] = MAX (F[i][j] + a[i+1][j+1]); 

@ 逆推

F[i][j] = MAX (F[i-1][j], F[i-1][j-1]) + a[i][j]; (注意!逆推時要注意邊界狀況! 
順推逆推本質上是同樣的(複雜度一致);順推和搜索的順序相似;而逆推則是將順序反過來;順推考慮的是「我這個狀態的下一步去哪裏」 ,逆推的考慮的是「從什麼狀態能夠到達我這裏」 。 同時在轉移的過程當中咱們要時刻注意邊界狀況。

咱們還能夠改變搜索順序

 1 //T3:數字金字塔-逆推/路徑自底向上
 2 //改變順序:記錄從底部向上走的路徑最優值
 3 for(int i=0;i<n;++i)
 4 f[n-1][i]=a[n-1][i];//備份底部本身這一行
 5 //逆推過程:能夠從左下方或右下方走過來;沒有邊界狀況
 6 for(int i=n-2;i>=0;--i)
 7 for(int j=0;j<=i;++j)
 8 f[i][j]=max(f[i+1][j+1],f[i+1][j])+a[i][j];//當前[i][j]左下方和右下方取較大加上當前的
 9 //答案則是頂端
10 ans=f[0][0];
11 //和以前的逆推區別:這樣較自頂向下不須要判斷邊界,更加簡單

 *轉移順序:最優值之間的推導順序

一個小問題:在數字金字塔中,爲何可以使用動態規劃 呢??答:由於有明確的順序: 自上而下 ,也就是說,能劃分紅不一樣的階段,這個階段是逐步進行這和搜索順序也是相似的,因此,只要劃分好階段從前日後推,與從後往前推都是能夠的

接下來咱們進入重點,仍是回到剛纔的採藥問題,咱們回憶剛纔這題的記憶化搜索。

狀態設計:記錄 F[i][j] 爲, 已經決定前 i 件物品的狀況,在總重量爲 的狀況下,物品總價值的最大值。一樣也是有兩種方法能夠推導: 
@ 順推「我這個狀態的下一步去哪裏
@ 逆推「從什麼狀態能夠到達我這裏

當前狀態F[i][j] 爲, 已經決定前 i 件物品的狀況,在總重量爲 j的狀況下,物品總價值的最大值。
@順推「我這個狀態的下一步去哪裏」 :我如今要決定下一件物品取仍是不取。
> 若是不取的話,能夠達到狀態 F[i+1][j]
> 若是取的話,能夠達到狀態 F[i+1][j+w[i+1]](須要知足重量約束);
@ 逆推「從什麼狀態能夠到達我這裏」 :考慮我這件物品取不取。
> 若是是不取的,那能夠從 F[i-1][j] 推導而來;
> 若是是取的,能夠從 F[i-1][j-w[i]] 推導而來的(一樣須要知足重量約束

 1 //T6:採藥(DP/順推)
 2 for(int i=0;i<n;++i)
 3 for(int j=0;j<=t;++j)
 4 {
 5     //不取
 6     f[i+1][j]=max(f[i+1][j],f[i][j]);
 7     //
 8     if(j+w[i]<=t)//知足重量限制(類比揹包問題) 
 9     f[i+1][j+w[i]]=max(f[i+1][j+w[i]],f[i][j]+v[i]);
10 }
11 //答案
12 ans=0;
13 for(int i=0;i<=t;++i) ans=max(ans,f[n][i]);
 1 //T7:採藥(DP/逆推)
 2 for(int i=1;i<=n;++i)
 3 for(int j=0;j<=t;++j)
 4 {
 5   //不取
 6   f[i][j]=f[i-1][j];
 7   //
 8   if(j>=w[i])f[i][j]=max(f[i][j],f[i-1][j-w[i]]+v[i]);//若是還有時間能夠採摘 
 9 }
10 //答案
11 ans=0;
12 for(int i=0;i<=t;++i) ans=max(ans,f[n][i]);

 學到這裏,咱們大概摸清了動態規劃的輪廓是什麼,使用動態規劃較DFS解決了時間上的問題,那麼咱們可不可考慮解決一下空間上的問題呢?因爲動態規劃知足」無後效性原則「,當前狀態F[i]之和上一個狀態F[i-1]有關,和上個狀態以前的都沒有關係,因此咱們能夠考慮使用滾動數組來保存這兩個狀態,一上一下,互爲先後狀態,節省空間啊!!

這就是——數組壓縮!

因此一個直觀的作法是,記錄兩個數組,分別記錄 F[i-1] F[i] 的值。
*但更進一步,咱們能夠甚至不記錄兩行,只記錄一行的狀態。
-咱們倒着枚舉,在 F[i-1] 數組中逐步更新,讓它逐步變爲 F[i]

由於是倒着枚舉的,先枚舉的位置都已經無用了,能夠直接用 F[i] 的元素來替換。

 1 //T10:採藥(DP/逆推/數組壓縮)
 2 //用一個一維數組來代替二維數組
 3 for(int i=1;i<=n;++i)
 4 for(int j=t;j>=0;--j)//重量:倒着枚舉
 5 {
 6     //不取:對數組沒有影響
 7     //f[i][j]=f[i-1][j];
 8     // 9     //if(j>=w[i])f[i][j]=max(f[i][j],f[i-1][j-w[i]]+v[i]);
10     if(j>=w[i])f[j]=max(f[j],f[j-w[i]]+v[i]);//若是還有采藥時間,執行 
11 }
12 //在枚舉過程當中,大於j的位置等於f[i][j],小於j的位置等於f[i-1][j]

 上題的採藥,就是一類經典的揹包問題——01揹包,下面咱們來講說第二類經典的揹包問題——徹底揹包問題:

經典例題:Piggy-Bank(POJ 1384)

如今有 n 種硬幣, 每種硬幣有特定的重量 cost[i] 和它對應的價值val[i]. 每種硬幣能夠無限使用。已知如今一個儲蓄罐中全部硬幣的總重量正好爲 問你這個儲蓄罐中最少有多少價值的硬幣若是不可能存在 克的狀況那麼就輸出「This is impossible.」 
咱們也把這類問題納入到揹包問題當中: 有物品,重量限制,價值最大
* 但與採藥問題不一樣的是,每件物品能夠無限使用

推導狀態轉移方程:

當前狀態F[i][j] 爲, 已經決定前 i 件物品的狀況,在總重量爲 j的狀況下,物品總價值的最小值。 

@ 順推「我這個狀態的下一步去哪裏」 :考慮我這件物品取多少件
> 若是是不取的,那能夠推導到 F[i+1][j]
> 若是是取一件,那能夠推導到 F[i+1][j+w[i]]
> 若是是取 k 件,那能夠推導到 F[i+1][j+w[i]*k]

 1 //T8:Piggy-Bank(DP/順推)
 2 //初值處理:因爲問題求的是最小值,因此先把全部狀態賦值爲最大值
 3 for(int i=1;i<=n+1;++i)
 4 for(int j=0;j<=m;++j) f[i][j]=INF;
 5 //第一件還沒取,重量爲0
 6 f[1][0]=0;
 7 for(int i=1;i<=n;++i)//i:已經決定的物品
 8 for(int j=0;j<=m;++j)//j:總重量
 9 for(int k=0;j+w[i]*k<=m;++k)//k:這件物品取多少件
10 f[i+1][j+w[i]*k]=min(f[i+1][j+w[i]*k],f[i][j]+p[i]*k);
11 //w重量;p[]價值

當前狀態F[i][j] 爲, 已經決定前 i 件物品的狀況,在總重量爲 j
的狀況下,物品總價值的最大值。
@ 逆推「從什麼狀態能夠到達我這裏」 :考慮我這件物品取多少件
> 若是是不取的,那能夠從 F[i-1][j] 處推導獲得;
> 若是是取一件,那能夠從 F[i-1][j-w[i]] 處推導獲得;
> 若是是取 k 件,那能夠從 F[i-1][j-w[i]*k] 處推導獲得;

1 //T9/work1:Piggy-Bank(DP/逆推)
2 for(int i=0;i<=n;++i)
3 for(int j=0;j<=m;++j) g[i][j]=INF;
4 g[0][0]=0;
5 for(int i=1;i<=n;++i)
6 for(int j=0;j<=m;++j)
7 for(int k=0;j>=w[i]*k;++k)
8 g[i][j]=min(g[i][j],g[i-1][j-w[i]*k]+p[i]*k);//三重循環開銷太大啦!!我須要一個優化QAQ 

,咱們對逆推動行優化:

逆推:觀察和逆推相關的狀態。
假設 w[i]=3,則 F[i][6] F[i-1][0,3,6] 相關; F[i][7] 與F[i-1][1,4,7] 相關;與此同時, F[i][3] 與 F[i-1][0,3] 相關;F[i][4] 與 F[i-1][1,4] 相關。 

則能夠獲得, 實際上與 F[i][j] 相關的狀態只比 F[i][j-w[i]] 多一個。

則因此咱們能夠這樣推導:
> 若是是不取的,那能夠從 F[i-1][j] 處推導獲得;
> 若是是取一件或更多,那能夠從 F[i][j-w[i]] 處推導獲得; (由於是能夠取任意件,因此從 F[i] 中取最優而不是從 F[i-1] 中取

而從這種逆推也能夠方便的寫出數組壓縮。 

 1 //T9/work2:Piggy-Bank(DP/逆推優化)
 2 for(int i=0;i<=n;++i)
 3 for(int j=0;j<=m;++j) g[i][j]=INF;
 4 g[0][0]=0;
 5 for(int i=1;i<=n;++i)
 6 for(int j=0;j<=m;++j)
 7 {
 8 g[i][j]=g[i-1][j];//不取
 9 if(j>=w[i])g[i][j]=min(g[i][j],g[i][j-w[i]]+p[i]);
10 }
1 //T9/work3:Piggy-Bank(DP/逆推優化/數組壓縮)
2 for(int j=0;j<=m;++j)f[j]=INF;
3 f[0]=0;
4 for(int i=1;i<=n;++i)
5 for(int j=0;j<=m;++j)//順着枚舉——徹底揹包(逆着枚舉——01揹包)
6 if(j>=w[i])f[j]=min(f[j],f[j-w[i]]+p[i]);

一樣地,咱們還能夠用位運算對一維進行優化,嗯。。因爲我比較懶,我就直接引了P2O5大佬的代碼:https://zybuluo.com/P2Oileen/note/816892#動態規劃

 1 //原來寫01揹包的時候,循環是這麼寫的:
 2 for(int i=1;i<=n;i++)
 3 {
 4     for(j=m;j>=a[i];j--) if(f[j-a[i]]) f[j]=1;//這個物品被取用了,標記爲1 
 5 }
 6 //能夠改爲位運算版本的,減小一重循環更快~ 
 7 for(int i=1;i<=n;i++)
 8 {
 9     f=f|f<<a[i];//f或f左移a[i]位(×2^a[i]) 
10 }

揹包計數問題:

經典例題:集合(Luogu 1466)

對於從 1 N (1 <= N <= 39) 的連續整數集合,能劃分紅兩個子集合,且保證每一個集合的數字和是相等的。舉個例子,若是 N=3,對於[1, 2, 3] 能劃分紅兩個子集合,每一個子集合的全部數字和是相等的:- [3] 和 [1,2]
這是惟一一種分法(交換集合位置被認爲是同一種劃分方案,所以不會增長劃分方案總數)若是 N=7,有四種方法能劃分集合 [1, 2, 3, 4, 5,6, 7],每一種分法的子集合各數字和是相等的:
[1,6,7] 和 [2,3,4,5] (: 1+6+7=2+3+4+5)
[2,5,7] 和 [1,3,4,6]
[3,4,7] 和 [1,2,5,6]
[1,2,4,7] 和 [3,5,6]
給出 N,你的程序應該輸出劃分方案總數,若是不存在這樣的劃分方案,則輸出 0。程序不能預存結果直接輸出(不能打表)。 

這裏引入揹包模型:

* 物品:能夠把全部的數字看做物品。對於數字 i,其對應的重量爲 i,則咱們須要求出裝滿載重爲 的揹包的方案數(其中 爲全部數總和的一半)
* 狀態:(仿照以前的方法)設 F[i][j] 爲已經考慮完數字 1-i 了,當前數字總和爲 的總方案數。
狀態轉移方程 順推:考慮有沒有取數字 i
沒取: F[i+1][j] += F[i][j]
取了: F[i+1][j+i] += F[i][j] (j+i<=M)

* 狀態轉移方程 - 逆推:考慮有沒有取數字 i
F[i][j] = F[i-1][j] + F[i-1][j-i] (j>=i)

1 //T14/work1:集合(DP/順推)
2 //初值:(什麼都不取)和=0,有一種方案
3 f[1][0]=1;
4 for(int i=1;i<=n;++i)
5 for(int j=0;j<=m;++j)
6 {
7   f[i+1][j]+=f[i][j];
8   if(i+j<=m)f[i+1][i+j]+=f[i][j];
9 }
1 //T14/work2:集合(DP/逆推)
2 f[0][0]=1;
3 for(int i=1;i<=n;++i)
4 for(int j=0;j<=m;++j)
5 {
6     f[i][j]=f[i-1][j];
7     if(j>=i)f[i][j]+=f[i-1][j-i];
8 }
1 //T14/work3:集合(DP/逆推/數組壓縮)
2 g[0]=1;
3 for(int i=1;i<=n;++i)
4 for(int j=m;j>=i;--j)//注意要倒着枚舉
5 g[j]+=g[j-i];

徹底揹包計數問題:

經典例題:貨幣系統(Luogu 1474)

母牛們不但建立了它們本身的政府並且選擇了創建了本身的貨幣系統。因爲它們特殊的思考方式,它們對貨幣的數值感到好奇。
傳統地,一個貨幣系統是由 1,5,10,20 25,50, 100 的單位面值組成的。
母牛想知道有多少種不一樣的方法來用貨幣系統中的貨幣來構造一個肯定的數值。
舉例來講, 使用一個貨幣系統 [1,2,5,10,...] 產生 18 單位面值的一些可能的方法是:18x1, 9x2, 8x2+2x1, 3x5+2+1, 等等其它。寫一個程序來計算有多少種方法用給定的貨幣系統來構造必定數量的面值。
* 物品:能夠把全部的貨幣看做物品。對於每種貨幣,其對應的重量爲它的面值(注意到與前一道題目相比,每一個物品是能夠取任意件的)
* 狀態:(仿照以前的方法)設 F[i][j] 爲已經考慮完前 i 種貨幣了,當前錢的總和爲 的總方案數。
* 狀態轉移方程 - 逆推:考慮貨幣 i 取了多少件。
F[i][j] =
F[i-1][j - w[i]*k] (加法原理噢~)

1 //T15/work1:貨幣系統(DP/逆推)
2 f[0][0]=1;
3 for(int i=1;i<=v;++i)
4 for(int j=0;j<=n;++j)
5 for(int k=0;k<=j/a[i];++k)
6     f[i][j]+=f[i-1][j-a[i]*k];
1 //T15/work2:貨幣系統(DP/逆推/數組壓縮)
2 g[0]=1;
3 for(int i=1;i<=v;++i)
4 for(int j=a[i];j<=n;++j)
5     g[j]+=g[j-a[i]];

總之,咱們在使用動態規劃解決問題的時候,要時刻注意一下:

1.劃分清楚狀態,狀態轉移方程明瞭清晰    

2.注意順序

3.儘可能使用數據壓縮,剪枝之類,好比列出邊界條件,位運算優化,多維變滾動甚至一維等。

最近發現一些網站盜用個人blog,這實在不能忍(™把關於個人名字什麼的所有刪去只保留文本啥意思。。)!!但願各位轉載引用時請註明出處,謝謝配合噢~

原博客惟一地址:http://www.cnblogs.com/geek-007/p/7197045.html

相關文章
相關標籤/搜索