DP(動態規划)學習心得


 

              動態規划學習心得

  說實話吧,動態規划(DP)確實是一個比較難的知識點,對於初學者來說,是一個難過的坎(筆者的臉呢?開玩笑。)。動態規划就是我從初學開始遇到的最神奇的解法,它不同於暴力搜索,也不同於一般的貪心,能夠以出乎人意料的時間復雜度(近似於O(n^2))解決一些難題,算法遠遠優於一般的深搜(O(2^n))。不過,動態規划的思維性比較強,必須會設好狀態,正確寫出狀態轉移方程,並且能夠准確判斷有無最優子結構。

  其實有點像貪心,但是它有局部最優解推導向整體最優解的過程,形象一點說,動態規划的“眼光”比貪心更長遠,有一個更新最優解的過程,發現問題了可以“反悔”。它還有一點分治的味道,通過對問題划分各個階段,對各個階段分別求解,最后推向整體的過程。與分治法不同的是,適合於用動態規划求解的問題,經分解得到子問題往往不是互相獨立的。若用分治法來解這類問題,則分解得到的子問題數目太多,有些子問題被重復計算了很多次。如果我們能夠保存已解決的子問題的答案,而在需要時再找出已求得的答案,這樣就可以避免大量的重復計算,節省時間。我們可以用一個表來記錄所有已解的子問題的答案。不管該子問題以后是否被用到,只要它被計算過,就將其結果填入表中。這就是動態規划法的基本思路。具體的動態規划算法多種多樣,但它們具有相同的填表格式。

  學習動態規划是一個比較漫長的過程,需要慢慢領悟,去體會動態規划的奧義。顯然,多做題,多思考是必需的,堅持下去,慢慢就能學會了。

  下面詳細地描述一下:

  一.動態規划的表示方法:

  一般地,動態規划有兩種表示方法,分別是:1.遞推  2.記憶化搜索。

   這兩種方法各有優缺點,遞推的效率更高,可以降維節省空間,能使用滾動數組,但思維性強,難度高。而記憶化搜索更好寫,更便於理解,不容易出錯,但容易超空間。有時候狀態數目多,記憶化搜索就不行了,會超空間。但是遞推是絕對沒有問題的,只要會滾動數組或者降維。所以,我比較推薦遞推的方法,更能夠鍛煉我們的算法能力。所以我們一般用遞推的方法解決動態規划的問題。

    例如:下面的代碼就是一種遞推:

f[i][j]=max(f[i+1][j],f[i+1][j+1])+a[i][j];

   至於記憶化搜索,也上一波代碼:

int dfs(int k,int j) 
{
	if(f[k][j]) return f[k][j];
	if(k == n) return a[k][j];
	temp1 = dfs(k+1 , j);
	temp2 = dfs(k+1 , j+1);
	f[k][j] = max(temp1 , temp2)+a[k][j];
}

  (以上代碼以題目數字三角形為例)。

  二.動態規划的條件:

  動態規划有兩個必要條件:

  1.無后效性.

  2.最優子結構.

  無后效性:

  標准定義是這樣的:將各階段按照一定的次序排列好之后,對於某個給定的階段狀態,它以前各階段的狀態無法直接影響它未來的決策,而只能通過當前的這個狀態。換句話說,每個狀態都是過去歷史的一個完整總結。這就是無后向性,又稱為無后效性。

  最優子結構:

  標准定義是這樣的:一個最優化策略具有這樣的性質,不論過去狀態和決策如何,對前面的決策所形成的狀態而言,余下的諸決策必須構成最優策略。簡而言之,一個最優化策略的子策略總是最優的。一個問題滿足最優化原理又稱其具有最優子結構性質。

  三.動態規划的分類及解決過程:

    分類:

  動態規划分為:

  1.基本線性動規: 比較基礎的DP入門

  2.背包動規  背包問題,見某大佬著作《背包九講》

  3.區間動規  區間型的動態規划

  4.雙進程動規  分為兩個進程,一般只需要增加一個維度表示狀態就可以了。

  5.樹狀動規      在樹上做動態規划,較為高級。

  6.各種優化...............等等

   解決過程:

 

   第一步:讀懂題意,看看題目是否可以滿足動態規划的條件(及是否可以用動態規划解決)。

   第二步:根據題目所給的條件划分階段,可以是題目給定的順序,或者是貪心的順序,或者是特殊的順序。

   第三步:根據階段設置狀態,一般用f數組表示,最基本規則:求什么設什么,必須滿足無后效性 。當感覺是dp,但是當前狀態不滿足必要條件的時候,狀態+維。

   第四步:推出狀態轉移方程式,能夠表示當前最優值和前面最優值的關系。

   第五步:代碼實現,檢查前面的步驟是否正確。  

  四.動態規划經典例題詳解:

  1.基本線性動規:

    hloj#402護衛隊

     見鏈接:https://www.cnblogs.com/smilke/p/10502784.html(本蒟蒻的一篇博客題解,如有不當之處歡迎指出)

  2.背包動規:                     

                【背包】采葯

題目描述

寧智賢是個天資聰穎的孩子,他的夢想是成為世界上最偉大的醫師。為此,他想拜附近最有威望的醫師為師。

醫師為了判斷他的資質,給他出了一個難題。

醫師把他帶到一個到處都是草葯的山洞里對他說:“孩子,這個山洞里有一些不同的草葯,采每一株都需要一些時間,每一株也有它自身的價值。我會給你一段時間,在這段時間里,你可以采到一些草葯。

如果你是一個聰明的孩子,你應該可以讓采到的草葯的總價值最大。”

  如果你是寧智賢,你能完成這個任務嗎?

輸入格式

輸入的第一行有兩個整數T(1 <= T <= 1000)和M(1 <= M <= 100),用一個空格隔開,T代表總共能夠用來采葯的時間,M代表山洞里的草葯的數目。接下來的M行每行包括兩個在1到100之間(包括1和100)的整數,分別表示采摘某株草葯的時間和這株草葯的價值。

輸出格式

輸出包括一行,這一行只包含一個整數,表示在規定的時間內,可以采到的草葯的最大總價值。

樣例數據

input

70 3
71 100
69 1
1 2

output

3

數據規模與約定

時間限制:1s

空間限制:256MB

----------------------------------------我是美美的分割線-------------------------------------------------------

   這道題就是最基本的01背包,對於每件物品,我們有取和不取兩種選擇.

  首先定義狀態f[i][j]以j為容量為放入前i個物品(按i從小到大的順序)的最大價值,那么i=1的時候,放入的是物品1,這時候肯定是最優的.

  由此,我們推出狀態轉移方程:f[i][j] = max(f[i-1][j-w[i]])+v[i],f[i-1][j]);

   其實,這道題還有一個滾動數組優化,可以優化第一維的空間。

  優化后的狀態轉移方程:f[j]=max(f[j-w[i]]+v[i],f[j]);

       下面是代碼:

  

#include<bits/stdc++.h>
using namespace std; int t,m; int w[100010],v[100010]; int f[100010]; int main() { freopen("input.in","r",stdin); freopen("output.out","w",stdout);
    cin>>m>>t; for(int i=1;i<=t;i++) cin>>w[i]>>v[i]; for(int i=1;i<=m;i++) for(int j=m;j>=w[i];j--) { f[j]=max(f[j],f[j-w[i]]+v[i]); } cout<<f[m]; return 0; }

  3.區間動規:

                             【區間動規】石子合並

題目描述

在操場上沿一直線排列着n堆石子。現要將石子有次序地合並成一堆。

規定每次只能選相鄰的兩堆石子合並成新的一堆,並將新的一堆石子數計為該次合並的得分。

我們希望這n1 次合並后得到的得分總和最小。

輸入格式

第一行有一個正整數nn<=300),表示石子的堆數; 第二行有n個正整數,表示每一堆石子的石子數,每兩個數之間用一個空格隔開。它們都不大於10000

輸出格式

一行,一個整數,表示答案。

樣例數據

input

3
1 2 9

output

15

數據規模與約定

區間dp第一題

時間限制:1s

空間限制:256MB

--------------------------------------------我是華麗的分割線-----------------------------------------------------

   

  這樣我們可以定義狀態f[i][j],表示i到j合並后的最大得分。其中1<=i<=j<=N。

  既然這樣,我們就需要將這一圈石子分割。很顯然,我們需要枚舉一個k,來作為這一圈石子的分割線。

  這樣我們就能得到狀態轉移方程:

  f[i][j] = max(f[i][k] + f[k+1][j] + d(i,j));    

  其中,1<=i<=<=k<j<=N。d(i,j)表示從i到j石子個數的和。

  下面是代碼:

  

#include<bits/stdc++.h>

#define din(a) (scanf("%d",&a));
#define dout(a) (printf("%d\n",a));
#define ll long long

using namespace std; int m,k; int n; int a[101000]; int f[1001][1001]; int sumn[1001]; int cost[1001][1001]; void work_cost()//計算合並的代價/得分.
{ for(int i=1;i<=n;i++) for(int j=i;j<=n;j++) cost[i][j]=sumn[j]-sumn[i-1]; } void init() { din(n);//初始化
    memset(f,0,sizeof(f)); memset(sumn,0,sizeof(sumn)); sumn[0]=0;//計算石子總數,方便累加得分.
    for(int i=1;i<=n;i++){ din(a[i]); sumn[i]=sumn[i-1]+a[i]; } work_cost(); } void work() //區間動規
{ for(int p=1;p<=n;p++) for(int i=1;i<=n;i++){ int j=i+p-1; if(j>n) break; for(int k=i;k<j;k++) if((f[i][j]>f[i][k]+f[k+1][j]+cost[i][j]||(f[i][j]==0))) f[i][j]=f[i][k]+f[k+1][j]+cost[i][j]; } } int main() { freopen("Stone.in","r",stdin); freopen("Stone.out","w",stdout); init(); work(); dout(f[1][n]); return 0; }

  4.雙進程DP

                                構建雙塔

題目描述

  2001年9月11日,一場突發的災難將紐約世界貿易中心大廈夷為平地,Mr. F曾親眼目睹了這次災難。為了紀念“911”事件,Mr. F決定自己用水晶來搭建一座雙塔。

  Mr. F有N 塊水晶,每塊水晶有一個高度,他想用這N塊水晶搭建兩座有同樣高度的塔,使他們成為一座雙塔,Mr. F可以從這N塊水晶中任取M1MN)塊來搭建。但是他不知道能否使兩座塔有同樣的高度,也不知道如果能搭建成一座雙塔,這座雙塔的最大高度是多少。所以他來請你幫忙。

  給定水晶的數量NN(1N100)和每塊水晶的高度HiN塊水晶高度的總和不超過2000),你的任務是判斷Mr. F能否用這些水晶搭建成一座雙塔(兩座塔有同樣的高度),如果能,則輸出所能搭建的雙塔的最大高度,否則輸出“ImpossibleImpossible”。

輸入格式

輸入的第一行為一個數N,表示水晶的數量。

第二行為N個數,第i個數表示第i個水晶的高度。

輸出格式

 輸出僅包含一行,如果能搭成一座雙塔,則輸出雙塔的最大高度,否則輸出一個字符串“Impossible”。

樣例數據

input

5
1 3 4 5 2

output

7

數據規模與約定

時間限制:1s

空間限制:256MB

 

-------------------我還是華麗的分割線-------------------------------

 

水晶放置在任意一座塔上都會對另一座塔產生影響,故屬於雙進程問題。

f[i][j]表示取前i塊水晶、兩塔差為j時較高塔的最大高度。

注意,這里的f[i][j]都是從上一階段推得的。我們在面對第i塊水晶時,它可能是從以下四種決策得來的:

f[i][j]=max(f[i1][j])f[i][j]=max(f[i−1][j]) . 這塊水晶被丟掉了。

f[i][j]=max(f[i1][j+h[i]])f[i][j]=max(f[i−1][j+h[i]]) . 這塊水晶被給了上一個狀態中較低的那座塔,且它未超過較高的塔,由圖可知較高塔的最大高度是不變的。 

f[i[][j]=max(f[i1][jh[i]]+h[i])f[i[][j]=max(f[i−1][j−h[i]]+h[i]) .這塊水晶被給了上一個狀態中較高的塔,由圖可知,較高塔的值增加了h[i]h[i]。

當然,此時我們要保證j>h[i]j>h[i]。f[i][j]=max(f[i1][h[i]j]+j)f[i][j]=max(f[i−1][h[i]−j]+j) .這塊水晶被給了上一階段較低的塔,且它超過了較高塔。由圖可知,較高塔的值增加了jj。

(感謝hh大佬提供思路)。

以下為代碼

#include<bits/stdc++.h>
using namespace std; int h[10010]; int f[1010][1010]; int n,sum=0; int main() { freopen("input.in","r",stdin); freopen("output.out","w",stdout); memset(f,-10,sizeof(f)); f[0][0]=0; scanf("%d",&n); for(int i=1;i<=n;i++) { scanf("%d",&h[i]); sum+=h[i]; } for(int i=1;i<=n;i++) for(int j=0;j<=sum;j++)//f[i][j]表示前i個水晶選擇完后,落差為j時的最優值 
 { f[i][j]=max(f[i-1][j],f[i-1][j+h[i]]);//不要水晶和要水晶的最優。
            if(j>=h[i]) f[i][j]=max(f[i-1][j-h[i]]+h[i],f[i][j]); else f[i][j]=max(f[i-1][h[i]-j]+j,f[i][j]);//狀態轉移 
 } if(f[n][0]) printf("%d\n",f[n][0]); else printf("Impossible"); return 0; }

  5.樹形動規

                  樹形DP例題1

題目描述

給定一棵n個點的無權樹,問樹中每個節點的深度和每個子樹的大小? (以1號點為根節且深度為0)

輸入格式

第1行:n。

第2~n行:每行兩個數x,y,表示x,y之間有一條邊。

輸出格式

n行,每行輸出格式為:#節點編號 deep:深度 count:子樹節點數(詳見樣例)

樣例數據

input

7
1 2
2 3
1 4
3 5
1 6
3 7

output

#1 deep:0 count:7
#2 deep:1 count:4
#3 deep:2 count:3
#4 deep:1 count:1
#5 deep:3 count:1
#6 deep:1 count:1
#7 deep:3 count:1

數據規模與約定

15% n<=10;

40% n<=1000;

100% n<=100000;

------------------------------我又是美美的分割線-------------------------------

  基本的樹形,建立一個領接表就OK了。

  話不多說,直接上代碼:

#include<bits/stdc++.h>
using namespace std; int n,p; int head[1001000],size[1001000],dep[1001000]; int cnt=0; int x,y; struct node { int to,next; }e[1001000]; void add(int x,int y) { cnt++; e[cnt].to=y; e[cnt].next=head[x]; head[x]=cnt; } void dfs(int x,int fa,int depth) { size[x]=1; dep[x]=depth; for(int i=head[x];i;i=e[i].next) { int v=e[i].to; if(v==fa) continue; dfs(v,x,depth+1); size[x]+=size[v]; } } int main() { freopen("tree.in","r",stdin); freopen("tree.out","w",stdout); memset(dep,0,sizeof(dep)); scanf("%d",&n); for(int i=1;i<=n-1;i++) { scanf("%d%d",&x,&y); add(x,y); add(y,x); } dfs(1,0,0); for(int i=1;i<=n;i++) { printf("#%d deep:%d count:%d\n",i,dep[i],size[i]); } return 0; }

  五.動態規划的意義:

  動態規划程序設計是對解最優化問題的一種途徑、一種方法,而不是一種特殊算法。不像搜索或數值計算那樣,具有一個標准的數學表達式和明確清晰的解題方法。動態規划程序設計往往是針對一種最優化問題,由於各種問題的性質不同,確定最優解的條件也互不相同,因而動態規划的設計方法對不同的問題,有各具特色的解題方法,而不存在一種萬能的動態規划算法,可以解決各類最優化問題。因此讀者在學習時,除了要對基本概念和方法正確理解外,必須具體問題具體分析處理,以豐富的想象力去建立模型,用創造性的技巧去求解。我們也可以通過對若干有代表性的問題的動態規划算法進行分析、討論,逐漸學會並掌握這一設計方法。

 

 

好好理解動態規划吧!

 

 

 

 

 

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM