『基礎DP專題:LIS,LCS和揹包九講(不包括泛化物品)及實現』
<前言>
<更新提示>
<第一次更新>重點揹包
<正文>
序列dp問題
LIS問題(最長上升子序列)
求長度為n的序列A中最長上升子序列的長度。
分析
狀態:f[i]代表以a[i]結尾的最長上升子序列長度。
方程:
邊界:f[i]=1(i=1~n)。
優化:暴力轉移。
程式碼實現:
#include<bits/stdc++.h>
using namespace std;
int n,a[1000080]={},f[100080]={};
int main()
{
cin>>n;
for(int i=1;i<=n;i++)cin>>a[i],f[i]=1;
for(int i=1;i<=n;i++)
{
for(int j=1;j<i;j++)
{
if(a[j]<a[i])f[i]=max(f[i],f[j]+1);
}
}
cout<<f[n]<<endl;
return 0;
}
LCS問題(最長公共子序列)
題目
求兩個長度為n的序列A,B的最長公共子序列長度。
分析
狀態:f[i][j]表示序列字串A[1~i],B[1~j]的最長公共字串。
方程:
邊界:f[i][0]=f[j][0]=0。
優化:暴力轉移
程式碼實現:
#include<bits/stdc++.h>
using namespace std;
int n,a[10080]={},f[1080][1080]={},b[1080]={};
int main()
{
cin>>n;
for(int i=1;i<=n;i++)
{
cin>>a[i];
}
for(int i=1;i<=n;i++)
{
cin>>b[i];
}
for(int i=1;i<=n;i++)f[i][0]=0;
for(int i=1;i<=n;i++)
{
for(int j=1;j<=n;j++)
{
if(a[i]==b[j])f[i][j]=f[i-1][j-1]+1;
else f[i][j]=max(f[i-1][j],f[i][j-1]);
}
}
cout<<f[n][n]<<endl;
return 0;
}
揹包問題
01揹包
題目
有N件物品和一個容量為V的揹包。第i件物品的費用是cost[i],價值是value[i]。求解將哪些物品裝入揹包可使價值總和最大。
分析
這是最基礎的揹包問題,其特徵為每一樣物品只能取一件或不取。我們用子問題定義狀態,其狀態如下:
狀態:f[i][j]代表已經對i件物品進行決策,使用揹包容量為j時的最大價值。
我們每一次作得決策為放第i件物品或不放第i件物品,所以我們可以得出狀態轉移方程如下:
方程:
當狀態為二維時,其程式碼實現如下:
#include<bits/stdc++.h>
using namespace std;
int n,v,cost[100080]={},value[100080]={},f[1080][1080]={};
int main()
{
cin>>n>>v;
for(int i=1;i<=n;i++)cin>>cost[i]>>value[i];
for(int i=1;i<=n;i++)
{
for(int j=cost[i];j<=v;j++)
{
f[i][j]=max(f[i-1][j],f[i-1][j-cost[i]]+value[i]);
}
}
cout<<f[n][v]<<endl;
return 0;
}
優化:空間複雜度的優化。
其優化方式如下:注意到我們每一次決策時,狀態f[i][j]都由f[i-1]這一維轉移而來,考慮推掉陣列的第一維。我們繼續定義f[i]代表揹包容量為i時的最大價值,考慮到其由上一次的決策轉移而來,而新的狀態轉移方程
需要呼叫
,為了避免在呼叫時
已經被本個物品的決策更新,我們在列舉容量j時採用倒敘列舉。感性的理解,如果我們呼叫已經決策過的
,相當於揹包中裝了多個相同的物品,是不可取的。
當狀態為一維時,其程式碼實現如下:
#include<bits/stdc++.h>
using namespace std;
int n,v,cost[100080]={},value[100080]={},f[100080]={};
int main()
{
cin>>n>>v;
for(int i=1;i<=n;i++)cin>>cost[i]>>value[i];
for(int i=1;i<=n;i++)
{
for(int j=v;j>=cost[i];j--)
{
f[j]=max(f[j],f[j-cost[i]]+value[i]);
}
}
cout<<f[v]<<endl;
return 0;
}
邊界及初始化問題:
我們看到的求最優解的揹包問題題目中,事實上有兩種不太相同的問法。有的題目要求“恰好裝滿揹包”時的最優解,有的題目則並沒有要求必須把揹包裝滿。一種區別這兩種問法的實現方法是在初始化的時候有所不同。
如果是第一種問法,要求恰好裝滿揹包,那麼在初始化時除了f[0]為0其它f[1..V]均設為-∞,這樣就可以保證最終得到的f[N]是一種恰好裝滿揹包的最優解。
如果並沒有要求必須把揹包裝滿,而是隻希望價格儘量大,初始化時應該將f[0..V]全部設為0。
為什麼呢?可以這樣理解:初始化的f陣列事實上就是在沒有任何物品可以放入揹包時的合法狀態。如果要求揹包恰好裝滿,那麼此時只有容量為0的揹包可能被價值為0的nothing“恰好裝滿”,其它容量的揹包均沒有合法的解,屬於未定義的狀態,它們的值就都應該是-∞了。如果揹包並非必須被裝滿,那麼任何容量的揹包都有一個合法解“什麼都不裝”,這個解的價值為0,所以初始時狀態的值也就全部為0了。
這個初始化的講解可以適用於每一類的揹包問題,之後就不再贅述了。
完全揹包
題目
有N種物品和一個容量為V的揹包,每種物品都有無限件可用。第i種物品的費用是cost[i],價值是vaule[i]。求解將哪些物品裝入揹包可使這些物品的費用總和不超過揹包容量,且價值總和最大。
分析
我們考慮利用01揹包的思路,那麼仍然可以類似的定義狀態。
狀態:f[i][j]代表已經對i件物品進行決策,使用揹包容量為j時的最大價值。
但是,不一樣的是,完全揹包中每一樣物品都可以取無限次,所以我們的狀態轉移方程有所改變。
方程:
對於這個方程,我們所需要求解的狀態數是不變的,但相比01揹包問題,由於需要列舉k,時間複雜度將幅增加,趨近於