寫這篇博文主要是為了歸納總結一下dp的有關問題(不定期更新,暑假應該會更的快一些)
會大概講一下思路,不會事無巨細地講
另一篇是平時做過的一些dp題,這篇博客里面提到的題都有題解放在那邊:https://www.cnblogs.com/henry-1202/p/9211398.html
這個玩意更新會有點慢,比較系統的學過一些dp的問題之后才會來寫這個(可能要有人來催更才會寫?)
一.最長上升子序列問題(LIS)
大概意思是給一個序列,按從左到右的順序選出盡可能多的數,組成一個上升子序列(子序列:對於一個序列,在其中刪除1個或多個數,其他數的順序不變)
隨便來個序列:1 8 7 4 8 9
它的最長上升子序列是1 4 8 9(當然也可以是1 7 8 9)
對於這個問題,有O(n2)做法和O(nlogn)做法,這里兩個做法都會講到
1.LIS的O(n2)做法
對於一個簡單的LIS,你是怎么用肉眼判斷的?一個一個往后找,然后數出最長的LIS,是不是?
n2做法就是用這種思路來做的,事實上這個做法就是一個優化后的暴力
設f[i]為以i為結尾的LIS的最大長度(也可以是起點),則有
f[i]=max(f[i],f[j]+1)(a[i]>a[j])
代碼如下:
for(int i=1;i<=n;i++){ f[i]=1;//這里不要忘記初始化,這是每個人都很容易忘記的地方 for(int j=1;j<i;j++){ if(a[i]>a[j])f[i]=max(f[i],f[j]+1); } }
當然第二層循環也可以枚舉j+1到n,都一樣的,if里面的大於號改一下就好(這樣的話就是f數組表示以ai為起點的最長上升子序列的)
這五行代碼是LIS的基本模型,基本所有LIS的題都是對這五行代碼添添改改搞出來的,所以在學習下面的東西之前需要務必確保自己完全搞懂這五行代碼了再繼續看
確保自己搞懂了之后可以先做一下例題中的合唱隊形還有導彈攔截的n2做法
2.LIS的O(nlogn)做法
在學習LIS的nlogn做法之前,你需要兩個前置姿勢:知道什么是貪心,並且會打二分查找
想想LIS:在一個序列中選出盡可能多的數組成一個上升子序列。
讓我們用用貪心的思想:
對於n2做法,我們是在求出以ai為結尾(起點)的LIS,以不斷更新f數組的值的方式來維護LIS,而在這個過程中,f數組的值一定是最優的,而對於LIS來說,最優意味着什么?
對於最優的LIS,它的最后一位一定是盡可能小的,因為這樣在后面更新的過程中,你才有更大的可能性讓這個LIS變得更長
LIS的nlogn做法就是根據這種思想來做的,然后通過二分查找來使效率降到nlogn
相應的,f數組的含義也需要改一下:fi代表長度為i的LIS的最后一位數
在看代碼之前請確保你理解了上面的貪心的思路
int m=1;//m是指LIS的長度,m=1是因為LIS的長度至少為1 memset(f,127,sizeof(f));//初始化,這里的127表示的是無窮大 f[1]=a[1];//初始化 for(int i=2;i<=n;i++){ if(a[i]>f[m])f[++m]=a[i];//如果比當前的LIS的最后一位還大那這個LIS就可以變長了 else {//通過二分查找來更新f數組 int l=0,r=m; while(l<r){ int mid=(l+r)>>1; if(a[i]>f[mid])r=mid; else l=mid+1; } f[l]=a[i];//不要忘記更新 } }
想練一下LIS的nlogn做法的話就去試試洛谷的導彈攔截吧,可以去我的另一篇博客看題解qwq,鏈接在上面
二,最長公共子序列(LCS)
公共子序列是指:一個同時是這兩個序列的子序列的序列
有點繞口...反正就是那個意思
例如:
1 2 3 4 5
2 1 3 5 4
LCS的長度顯然是3(1,3,4/2,3,4)
於是可以設f[i][j]表示第一個序列的前i位與第二個序列的前j位的LCS
答案就是f[len1][len2]
轉移方程其實很好想的:
如果第一個序列的i位與第二個序列的j位相同,那么轉移:f[i][j]=max(f[i-1][j],f[i][j-1])+1
如果不同那么考慮繼承之前的最優答案:f[i][j]=max(f[i-1][j],f[i][j-1])
這里給例題中的 洛谷P1439[模板]最長公共子序列 的朴素LCS做法
#define ll int #define N 1010 ll n,a[N],b[N],f[N][N]; int main(){ scanf("%d",&n); for(ll i=1;i<=n;i++)scanf("%d",&a[i]); for(ll i=1;i<=n;i++)scanf("%d",&b[i]); for(ll i=1;i<=n;i++){ for(ll j=1;j<=n;j++){ if(a[i]==b[j])f[i][j]=max(f[i-1][j],f[i][j-1])+1; else f[i][j]=max(f[i-1][j],f[i][j-1]); } } printf("%d",f[n][n]); return 0; }
然后這道題的100%做法是很妙的,有興趣的話可以去做一下(上面的做法只能50分)
三.區間dp
這個我應該會寫多一點……
區間dp的定義:顧名思義,就是在對區間進行dp,一般是對小區間進行dp得出最優解,然后通過小區間的最優解得出一整個區間的最優解
大概是這樣?反正也差不多,在沒有題的情況下再怎么講也很空泛,上面的定義隨便看看就好,知道區間dp是個什么東西就行,具體看下面的例題來理解。
1.矩陣鏈乘問題
這是紫書和算導都有的東西,不過我沒有在OJ上面找到這道題...
首先在講這道題之前我們要明確矩陣乘法的幾個概念
1.兩個矩陣能夠相乘,當且僅當矩陣A的列數等於矩陣B的行數
2.設矩陣A的規模為n*p,矩陣B的規模為p*m,則這兩個矩陣相乘的運算量為n*m*p
2.矩陣乘法滿足結合律但不滿足分配律
在明白矩陣乘法的概念之后,就可以來看這道題了
題意:給n個矩陣,全部都要乘起來,最后得到一個矩陣,你可以給他們加括號改變運算順序,求最少的運算量,假設第i個矩陣Ai的規模是pi-1*pi
顯然,加不加括號對這道題的運算量的影響是巨大的,舉個例子,對於三個矩陣A,B,C,假設他們分別是2*3,3*4,4*5的,那么(A*B)*C的運算量為64,A*(B*C)的運算量為90,,這兩種運算方式得出的最終的矩陣其實是一樣的,但是運算量差別就很大了。
那么怎么解決這道題?
我們不妨用一般解dp題的思路來看看,把這一整個序列分成多個小的序列(划分子問題)
設l為一個小序列的左端點,r為一個小序列的右端點,那么當這個小序列足夠小(l==r),運算量顯然為0(你沒辦法用它自己來乘它自己),這樣我們就能得出初始化的情況了
然后假設我們現在正在求解某個子問題,這一次乘法是第k個乘號,那么我們一定已經算出A1*A2*···*Ak和Ak+1*Ak+2*···*An的最優結果,因此對於這一次相乘的結果它一定也是最優的(dp的最優子結構性質)
設我們正在求解的這個子問題的運算量為f[i][j],那么可得f[i][j]=min{f[i][k]+f[k+1][j]+pi-1*pj*pk}(k即上文我們提到的“最優的第k個乘號”)
那么有了轉移方程后我們就可以來寫這道題了嗎?不,我們還需要注意到一個特殊的情況,如果采用遞推求解的方式,因為需要滿足dp的無后效性的性質,所以我們不能按照i/j的遞增/遞減順序來進行運算,而是要以j->i遞增的順序來運算。
當然記憶化搜索就沒有這個問題了,不過我不習慣打記憶化搜索,這一方面的話算導有講到
總的時間復雜度為O(n3)
是的沒有代碼我懶得寫,不過反正就是一個引入,應該不需要代碼吧···理解一下思路
真的需要代碼的話可以跟我講下我去搞一下
二.石子合並問題
區間dp的例題,流傳甚廣的一道題,這里只給O(n3)做法,四邊形不等式優化我不會啊>_<
與上一題相同,枚舉斷點划分子問題,通過子問題的最優解得出整個區間的最優解,區間dp的題都是這種套路
顯然對於每個區間f[i][j],一定是由最優斷點的結果轉移過來的(不難證明:如果當前斷點不是最優的,那么如果使用更優的斷點得出的結果,也一定比當前斷點更優)
於是設f[i][j]表示把i~j這個區間的石子合並為一堆石子的最小/最大花費
答案就是所有區間長度為n的區間的最大值/最小值
然后代碼中的具體實現,我個人推薦的做法是枚舉區間長度,左端點和斷點,右端點可以通過左端點和區間長度的值算出來
還有就是對於石子的個數處理一下前綴和存在數組c中,這樣在轉移的時候就可以O(1)得到i~j堆石子的合並花費
轉移方程即為f[i][j]=max(f[i][j],f[i][k]+f[k+1][j]+c[j]-c[i-1])(對於最小值是min)

#include <cstdio> #include <cstring> #define ll long long #define inf 1<<30 #define il inline #define in1(a) read(a) #define in2(a,b) in1(a),in1(b) #define in3(a,b,c) in2(a,b),in1(c) #define in4(a,b,c,d) in2(a,b),in2(c,d) il int max(int x,int y){return x>y?x:y;} il int min(int x,int y){return x<y?x:y;} il int abs(int x){return x>0?x:-x;} il void swap(int &x,int &y){int t=x;x=y;y=t;} il void readl(ll &x){ x=0;ll f=1;char c=getchar(); while(c<'0'||c>'9'){if(c=='-')f=-f;c=getchar();} while(c>='0'&&c<='9'){x=x*10+c-'0';c=getchar();} x*=f; } il void read(int &x){ x=0;int f=1;char c=getchar(); while(c<'0'||c>'9'){if(c=='-')f=-f;c=getchar();} while(c>='0'&&c<='9'){x=x*10+c-'0';c=getchar();} x*=f; } using namespace std; /*===================Header Template=====================*/ #define N 210 int n,f_min[N][N],f_max[N][N],a[N],c[N]; int main(){ in1(n); for(int i=1;i<=n;i++)in1(a[i]),a[i+n]=a[i]; for(int i=1;i<=2*n;i++)c[i]=c[i-1]+a[i]; for(int len=2;len<=n;len++){ for(int i=1;i<=2*n;i++){ int j=i+len-1;f_min[i][j]=inf; for(int k=i;k<j;k++){ f_min[i][j]=min(f_min[i][j],f_min[i][k]+f_min[k+1][j]+c[j]-c[i-1]); f_max[i][j]=max(f_max[i][j],f_max[i][k]+f_max[k+1][j]+c[j]-c[i-1]); } } } int ans_max=0,ans_min=inf; for(int i=1;i<=n;i++){ ans_max=max(ans_max,f_max[i][i+n-1]); ans_min=min(ans_min,f_min[i][i+n-1]); } printf("%d\n%d\n",ans_min,ans_max); return 0; }
四.最大子段和問題
挺簡單的一類dp問題
最大子段和問題是這樣的:
給你一個序列,正負不定,你需要求出$a[i]+a[i+1]+···+a[j-1]+a[j]$的最大值$(1<=i<=j<=n)$
分治和dp的經典題
這里只講dp做法
我們設$f[i]$表示從1到i的最大子段和,那么可以很簡單的推出轉移方程
$f[i]=max(f[i-1]+a[i],a[i])$
也可以換一種寫法
if(f[i-1]>0)f[i]=f[i-1]+a[i];
else f[i]=a[i];
初始化f[1]=a[1]
以上就是最基礎的幾個dp類型啦(完結撒花)