期末了,通過寫博客的方式復習一下dp,把自己理解的dp思想通過樣例全部說出來
說說我所理解的dp思想
dp一般用於解決多階段決策問題,即每個階段都要做一個決策,全部的決策是一個決策序列,要你求一個
最好的決策序列使得這個問題有最優解
將待求解的問題分為若干個相互聯系的子問題,只在第一次遇到的時候求解,然后將這個子問題的答案保存
下來,下次又遇到的時候直接拿過來用即可
dp和分治的不同之處在於分治分解而成的子問題必須沒有聯系(有聯系的話就包含大量重復的子問題,那
么這個問題就不適宜分治,雖然分治也能解決,但是時間復雜度太大,不划算),所以用dp的問題和用分
治的問題的根本區別在於分解成的子問題之間有沒有聯系,這些子問題有沒有重疊,即有沒有重復子問題
dp和貪心的不同之處在於每一次的貪心都是做出不可撤回的決策(即每次局部最優),而在dp中還有考察
每個最優決策子序列中是否包含最優決策子序列,貪心中每一步都只顧眼前
最優,並且當前的選擇是不會依賴以前的選擇的,而dp,在選擇的時候是從以前求出的若干個與本步驟
相關的子問題中選最優的那個,加上這一步的值來構成這一步那個子問題的最優解
講得再多不如看幾個很經典的樣例,帶你初步入門dp
我不會講很多具體該怎么做,而是剖析這些經典例題中的dp思想,真真正正的懂得了dp思想的話,做題事
半功倍(自己深有體會)
樣例1:數字三角形問題
7
3 8
8 1 0
2 7 4 4
4 5 2 6 5
從頂部向下走,每次只能走下面或者右下,走完全程,問你怎么走使得權值最大(問題描述不是很詳細,關
於數字三角形問題是什么問題請百度)
那么dp的思想到底是怎么體現的呢?
dp是要先分解成很多相互聯系的子問題,要解決一個子問題,依賴於前面和此子問題相關的已經解決的子
問題中選一個最優的加上這個子問題的解,就是這個子問題的最優解
具體做法:
1.分析問題的最優解,找出最優解的性質,並刻畫其結構特征:
問題的最優解:所有走法中最大的權值是多少?
最優解的性質和結構特征:只能向正下或者右下走,每走一行的最大權值等於前面一行的最大權值加上這一
行的走的兩個方向中的最大值
2.遞歸的定義最優值:
要找到從0行出發的最優值,就要找到從第1行出發的最優值
要找到從1行出發的最優值,就要找到從第2行出發的最優值
………………………
要找到第3行出發的最優值,就要找到從最后一行出發的最優值
為什么是這樣呢?我們分析一下
題目要你求從0行出發的最優值,那么我們就是要找到從第一行出發的最優值,加上第0行到第1行的最優值
但是,很重要的一點,我們需要遞歸求解,要先求解從倒數第一行出發的最優值,然后根據從倒數第一行出
發的最優值求出從倒數第二行出發的最優值
3.采用自底向上的方式計算問題的最優值:
這個就是我上面說的,要先求解從倒數第一行出發的最優值,然后根據從倒數第一行出發的最優值求出從倒
數第二行出發的最優值,自底向上的計算,迭代的方式求解子問題
4.根據計算最優值時間得到的信息,構造最優解
這個就是問你具體是怎么走的,我們需要在求解子問題的時候保存一些信息,采用構造出最優解(最優值和
最優解是不同的,最優值在本問題中是一個走法中權值之和最大的那一個,而最優解是具體的走法),這里
題目沒有要求就是不用去構造最優解,構造起來也挺麻煩的。。。。
解法:
dp【i】【j】:代表從第i行第j列出發得到的最優值
dp【i】【j】=max(dp【i+1】【j】,dp【i+1】【j+1】)+a【i】【j】
表示從第i行第j列出發的最優值等於到i+1行的兩種走法中最大的那一個加上出發點的權值
貼個鏈接:
https://www.cnblogs.com/yinbiao/p/8995253.html
貼個代碼:
#include<bits/stdc++.h> using namespace std; int main() { int n; scanf("%d",&n);//n行 int a[n][n]; memset(a,0,sizeof(a)); for(int i=0;i<n;i++) { for(int j=0;j<=i;j++) { scanf("%d",&a[i][j]); } } int dp[n][n]; memset(dp,0,sizeof(dp)); for(int j=0;j<n;j++) { dp[n-1][j]=a[n-1][j]; } for(int i=n-2;i>=0;i--) { for(int j=0;j<=i;j++) { dp[i][j]=max(dp[i+1][j],dp[i+1][j+1])+a[i][j]; } } printf("%d\n",dp[0][0]); return 0; }
經典樣例2:最長公共子序列問題 (LCS問題)
給你兩個序列,問你他們的最長LCS序列的長度是多少?(序列可以是不連續的,只要元素的相對位置一
樣)(不了解LCS問題的自行百度)
那么在LCS問題中dp的思想體現在哪里呢?
重復子問題:(超級容易發現的一個)
我們要求x1~xi,Y1~Yj的LCS,那么是不是要求x1~xi-1,Y1~Yi-1的LCS
我們要求x1~xi-1,y1~yi-1的LCS,那么是不是要求x1~xi-2,Y1~yi-2的LCS
所以我們要求的x1~xi,Y1~Yj的LCS這個大問題中,包含了很多的重復子問題
具體做法:
c【i】【j】表示x1~xi,Y1~Yj的LCS序列長度
x【i】==y【j】 c【i】【j】=c【i-1】【j-1】+1
x【i】!=y【j】 c【i】【j】=max(c【i-1】【j】,c【i】【j-1)
i==0||j==0 c【i】【j】=0
貼個代碼(求最優值和最優解)
#include<bits/stdc++.h> #define max_v 1005 using namespace std; char x[max_v],y[max_v]; int dp[max_v][max_v]; int l1,l2; int dfs(int i,int j) { if(i==-1||j==-1) return 0 ; if(x[i]==y[j])//來自左上角 { dfs(i-1,j-1); cout<<x[i]<<" ";//先遞歸到最后再輸出,,這樣就是順序的 } else { if(dp[i-1][j]>dp[i][j-1])//來自上面 { dfs(i-1,j); } else//來自左邊 { dfs(i,j-1); } } return 0; } int main() { int t; scanf("%d",&t); getchar(); while(t--) { scanf("%s",x); scanf("%s",y); int l1=strlen(x); int l2=strlen(y); memset(dp,0,sizeof(dp)); for(int i=1; i<=l1; i++) { for(int j=1; j<=l2; j++) { if(x[i-1]==y[j-1]) { dp[i][j]=dp[i-1][j-1]+1; } else { dp[i][j]=max(dp[i-1][j],dp[i][j-1]); } } } printf("%d\n",dp[l1][l2]); dfs(l1,l2); cout<<endl; } return 0; } /* 2 ABCBDAB BDCABA */
經典樣例三:矩陣連乘問題,紙牌問題,石頭合並問題(都是一類問題,一起分析)
給定n個矩陣{A1,A2…..An},其中A【i】與A【i+1】是可乘的,如何確定計算的次序,使得乘法的總次數最少
首先我們要明白,計算的次序不同,那么乘法的總次數也不同
類似的問題:給你n張牌,每張排都有一個數字,相鄰的兩張牌的權值可以相乘,相乘的兩張牌可以合並為
一張牌,新牌的權值是原來的兩張牌的乘積
這個問題還有石頭合並問題都是同一類的問題,屬於區間dp問題
石頭合並問題:給你一堆石頭,排成一行,相鄰的兩個石頭可以合並,合並成的石頭的權值為原來兩個石頭
的權值之和
先來分析矩陣連乘問題:
給你一個一維數組
30,35,15,5,10,20,25
只要相鄰的矩陣才可以相乘
思考一下,dp的思想是如何體現的
第一步我們是要把問題分解成很多互相有聯系的子問題(重復子問題是用dp的基礎)
簡單的思考一下,每次矩陣相乘,最簡單的就是兩個可以相乘的矩陣相乘(A1,A2,A3),那最大的乘法次數就是A1*A2*A3
但是如果是多個呢,我們是不是可以簡化成下面這樣
A【i】,A【i+1】………………….A【k】………………A【j-1】,A【j】
講他們分成兩個抽象矩陣
第一個:A【i】….A【k】
第二個:A【k+1】…..A【j】
把大問題抽象成兩個抽象矩陣相乘,那么更加最簡單的那種抽象一下就知道求所有矩陣乘法的最大次數,就
是求第一個抽象矩陣自己內部要乘的次數和第二個抽象矩陣內部自己要求的乘法次數然后加上這這兩個抽象
矩陣合並為一個大的抽象矩陣要乘的次數
那么大問題是這樣的,大問題里面是不是有很多這樣的小問題,而且這些小問題還是重復的,比如A【k】
的選擇不同,那么乘的次序結果也不一樣,A【k】的選擇可以導致很多問題都有重復的部分,如果多次計
算的話,無疑是很不明智的,這樣的話跟分治就是沒有什么區別了,這樣的問題就叫做重復子問題
A【k】的選擇不同的話,會導致子問題有很多重復的部分,前面我們說了的,同時A【k】的選擇不同的話
會導致兩個抽象矩陣相乘的結果也不一樣,所以我們就要在所有的A【k】選擇中找一個最小的
所以我們現在在這個問題里面找到了dp思想的具體體現:大量的重復子問題
具體做法:
dp【i】【j】:代表矩陣i,矩陣i+1………….矩陣j的最少乘法次數
總結上述:
dp【i】【j】=min(dp【i】【k】+dp【k+1】【j】
i<=k<=j-1
貼個代碼:
#include<bits/stdc++.h> using namespace std; #define max_v 1005 int dp[max_v][max_v],a[max_v],s[max_v][max_v]; void f(int i,int j) { if(i==j) return ; f(i,s[i][j]); f(s[i][j]+1,j); printf("A[%d:%d]*A[%d:%d]\n",i,s[i][j],s[i][j]+1,j); } int main() { int n; scanf("%d",&n); for(int i=0;i<=n;i++) { scanf("%d",&a[i]); } for(int i=1;i<=n;i++) { dp[i][i]=0; } for(int r=2;r<=n;r++) { for(int i=1;i<=n-r+1;i++) { int j=i+r-1; dp[i][j]=dp[i+1][j]+a[i-1]*a[i]*a[j]; s[i][j]=i; for(int k=i+1;k<j;k++) { int t=dp[i][k]+dp[k+1][j]+a[i-1]*a[k]*a[j]; if(t<dp[i][j]) { dp[i][j]=t; s[i][j]=k; } } } } f(1,n); } /* 6 30 35 15 5 10 20 25 A[2:2]*A[3:3] A[1:1]*A[2:3] A[4:4]*A[5:5] A[4:5]*A[6:6] A[1:3]*A[4:6] */
分析了矩陣連乘問題,再來分析一下石頭合並問題
石頭合並問題:其實這個問題跟矩陣連乘問題真的是一樣的
非常非常的類似
A1,A2………………….An
也是分解成兩個抽象的石頭
A【i】,A【i+1】………A【k】……….A【j】
第一個抽象石頭:A【i】……..A【k】
第二個抽象石頭:A【k+1】…….A【j】
我們現在把大問題分解成了兩個抽象的石頭合並問題
問的是你合並完成后最小的權值是多少
大問題的最小權值等於第一個抽象石頭合並的權值加上第二個抽象石頭合並的權值,再加上這兩個抽象的石頭合並的權值
我們知道,A【k】的選擇不同,會導致最后權值的不同,也會導致大量重復的子問題(前面在矩陣連乘wen他中具體分析了)
所以我們要在所有的A【k】選擇中,選擇一個合並花費最小的
現在我們把大問題分解成了這樣一個問題,那么每個抽象的石頭也還可以當初一個大問題繼續分解呀,所以
就分解成了很多子問題
具體做法:
dp【i】【j】:代表合並第i到第j個石頭的最小花費
sum【i】:表示1~i個石頭的權值之和
dp【i】【j】=min(dp【i】【k】+dp【k+1】【j】)+sum【j】-sum【i】+a【i】
為什么是sum【j】-sum【i】+a【i】呢?
因為我們要合並從第i個石頭到第j個石頭所需要的花費就是第i個石頭到第j個石頭的權值的和呀
貼個代碼:
#include<bits/stdc++.h> using namespace std; int main() { int n; while(~scanf("%d",&n)) { int a[n+1]; for(int i=1; i<=n; i++) { scanf("%d",&a[i]); } int sum[n+1]; int dp[n+1][n+1]; for(int i=1; i<=n; i++) { int t=0; for(int j=1; j<=i; j++) { t=t+a[j]; } sum[i]=t; } for(int i=1; i<=n; i++) { dp[i][i]=0; } for(int r=2; r<=n; r++) { for(int i=1; i<=n-r+1; i++) { int j=i+r-1; int t=dp[i][i]+dp[i+1][j]+sum[j]-sum[i]+a[i]; for(int k=i; k<=j-1; k++) { if(t>dp[i][k]+dp[k+1][j]+sum[j]-sum[i]+a[i]) { t=dp[i][k]+dp[k+1][j]+sum[j]-sum[i]+a[i]; } } dp[i][j]=t; } } printf("%d\n",dp[1][n]); } return 0; } /* 樣例輸入 3 1 2 3 7 13 7 8 16 21 4 18 樣例輸出 9 239 */
經典樣例四:最長遞增子序列
比如
1,7,3,5,8,4,8
問你最長的遞增的子序列的長度是多少
這個問題的最優解有多個,但是最優值只有一個:長度為4
1,3,5,9
1,3,5,8
1,3,4,8
這三個都是最優解,但是他們長度都是一樣的,長度為4
這些是我們看出來的
那我們如何用dp的思想解題呢
第一步分解成很多互相有聯系的子問題
要求第n個元素結尾的LIS序列的長度,就要求以第n-1個元素結尾的LIS序列的長度
要求第n-1個元素結尾的LIS序列的長度,就要求以第n-2個元素結尾的LIS序列的長度
…………..
假設第n-1個元素結尾的LIS序列的長度為2,且第n個元素是大於第n-1個元素的(遞增的),那么以第n
個元素結尾的LIS序列的長度不就是以第n-1個元素結尾的LIS序列的長度加上1嗎?
再回過頭來看看這些子問題
他們中是不是含有大量重復的子問題
dp【n】:代表以第n個元素結尾的LIS序列的長度
比如我要求dp【n】,就要求dp【n-2】,dp【n-3】
在要求dp【n-1】的時候,也還要求dp【n-2】,dp【n-3】一次
這個就是求了很多次,想想當n足夠大的時候,子問題足夠多的時候,求的重復的子問題是不是很多很多
這樣的話速度太慢
所以這個時候,dp的作用就是體現出來了,保存已經求解過的子問題的值,下次又遇到這個子問題的時
候,直接拿出來用就好啦
做法:
dp【1】=1
dp【i】=max(dp【j】+1) 要求:a【j】<a【i】,j<i
就是在第i個元素的前面找到LIS序列長度最大的,加上1,(先決條件是遞增的)
貼個代碼:(最優值和一個最優解)
#include<bits/stdc++.h> using namespace std; #define max_v 1005 int a[max_v],dp[max_v]; void f(int n,int result) { bool flag=false; if(n<0||result==0) return ; if(dp[n]==result) { flag=true; result--; } f(n-1,result); if(flag) printf("%d ",a[n]); } int main() { int n; while(~scanf("%d",&n)) { for(int i=0;i<n;i++) { scanf("%d",&a[i]); } memset(dp,0,sizeof(dp)); dp[0]=1; for(int i=1;i<n;i++) { int t=0; for(int j=0;j<i;j++) { if(a[j]<a[i]) { if(t<dp[j]) { t=dp[j]; } } } dp[i]=t+1; } int t=0; for(int i=0;i<n;i++) { if(t<dp[i]) { t=dp[i]; } } printf("%d\n",t); f(n,t); printf("\n"); } return 0; } /* 輸入: 7 1 7 3 5 9 4 8 輸出: 4 1 3 4 8*/
經典樣例五:最大子段和問題
比如:
-2,11,-4,13,-5,-2
什么叫最大字段和?就是連續的數字的和最大是多少,注意是段,而不是序列,序列可以是離散的,而段必
須的連續的
所以這個問題dp思想體現在哪里呢?
這個問題其實跟LIS,LCS問題都差不多,都是線性dp問題
第一步:分解成很多有聯系的子問題
要求以第n個元素結尾的最大字段和是多少,就要求以第n-1個元素結尾的最大字段和是多少
要求以第n-1個元素結尾的最大子段和是多少,就要求以第n-2個元素結尾的最大字段和是多少
為什么是這樣呢?
仔細思考一下
以求第n-1個元素的1最大字段和為例
如果我們知道了以第n-2個元素的最大字段和是多少,如果是正的,加上第n個元素值即可,如果是負數,
那還不如不加呢,這樣第n個元素的最大字段和還大一點,因為你加上一個負數肯定比原來的數小了呀
那么dp思想中的重復子問題體現在哪里呢?
體現在第一步,跟LIS問題中的體現是一樣的,這里不再贅述
貼個代碼:(最優解)
#include<bits/stdc++.h> using namespace std; int main() { int t; scanf("%d",&t); while(t--) { int n; scanf("%d",&n); int a[n+1]; for(int i=1;i<=n;i++) { scanf("%d",&a[i]); } int dp[n+1]; dp[1]=a[1]; for(int i=2;i<=n;i++) { int x=dp[i-1]; if(x<0) { x=0; } dp[i]=x+a[i]; } int maxvalue=dp[1]; for(int i=2;i<=n;i++) { if(maxvalue<dp[i]) { maxvalue=dp[i]; } } printf("%d\n",maxvalue); } return 0; } /* 輸入: 2 6 -2 11 -4 13 -5 -2 輸出; 20 */
經典樣例六:01背包問題
背包問題可以說是很經典的問題之一了,01背包問題,就是說每個物品只有兩種選擇,裝還不裝,且物品
不可分割
我先不講01背包問題應該怎么做,講01背包里面蘊含的dp思想
dp適用於多階段決策問題,就是每個階段都要做決策,且你做的決策會影響的最終的結果,導致最終結果的值有所不同
這個決策的概念在01背包里面用的可以說是體現的非常非常的透徹了,因為你每個階段都要做決策呀,這
個物品我到底是選還是不選呢
聲明一個 大小為 m[n][c] 的二維數組,m[ i ][ j ] 表示 在面對第 i 件物品,且背包容量為 j 時所能獲得的最大價值 ,那么我們可以很容易分析得出 m[i][j] 的計算方法,
(1). j < w[i] 的情況,這時候背包容量不足以放下第 i 件物品,只能選擇不拿
m[ i ][ j ] = m[ i-1 ][ j ]
(2). j>=w[i] 的情況,這時背包容量可以放下第 i 件物品,我們就要考慮拿這件物品是否能獲取更大的價值。
如果拿取,m[ i ][ j ]=m[ i-1 ][ j-w[ i ] ] + v[ i ]。 這里的m[ i-1 ][ j-w[ i ] ]指的就是考慮了i-1件物品,背包容量為j-w[i]時的最大價值,也是相當於為第i件物品騰出了w[i]的空間。
如果不拿,m[ i ][ j ] = m[ i-1 ][ j ] , 同(1)
究竟是拿還是不拿,自然是比較這兩種情況那種價值最大。
狀態轉移方程:
if(j>=w[i])
m[i][j]=max(m[i-1][j],m[i-1][j-w[i]]+v[i]);
else
m[i][j]=m[i-1][j];
代碼如下:(二維dp解決01背包問題,一維dp解決01背包問題)
#include<bits/stdc++.h> using namespace std;int ZeroOnePack(int v[],int w[],int n,int c)//v1,v2....vn價值 w1,w2,w3...wn重量 n表示n個物品 c表示背包容量 { int dp[n+1][c+1]; memset(dp,0,sizeof(dp)); for(int i=1; i<=n; i++) { for(int j=0; j<=c; j++) { if(j>=w[i]) { dp[i][j]=max(dp[i-1][j],dp[i-1][j-w[i]]+v[i]);//第i個物品放入之后,那么前面i-1個物品可能會因為剩余空間不夠無法放入 } else { dp[i][j]=dp[i-1][j]; } } } return dp[n][c]; } //空間優化,采用一維數組 int ZeroOnePack_improve(int v[],int w[],int n,int c)//v1,v2....vn價值 w1,w2,w3...wn重量 n表示n個物品 c表示背包容量 { int dp[c+1]; memset(dp,0,sizeof(dp)); for(int i=1; i<=n; i++) { for(int j=c; j>=0; j--) { if(j>=w[i]) dp[j]=max(dp[j],dp[j-w[i]]+v[i]); } } return dp[c]; } int main() { int t; scanf("%d",&t); while(t--) { int n,c; scanf("%d %d",&n,&c); int v[n+1],w[n+1]; for(int i=1; i<=n; i++) { scanf("%d",&v[i]); } for(int i=1; i<=n; i++) { scanf("%d",&w[i]); } // printf("%d\n",ZeroOnePack(v,w,n,c)); printf("%d\n",ZeroOnePack_improve(v,w,n,c)); } return 0; } /* 1 5 10 1 2 3 4 5 5 4 3 2 1 14 */