基礎DP(狀態轉移和遞推)
1.硬幣問題
有多個不同面值的硬幣,輸入最少硬幣組合,
這里要是用貪心,所得組合和實際可能不符合,
利用dp法,代碼如下:
#include<bits/stdc++.h>
using namespace std;
const int MONEY=1001; //定義最大金額
const int VALUE=5; //5種硬幣
int type[VALUE]={1,5,10,25,50}; //5種面值
int Min[MONEY]; //每個金額對應最少的硬幣數量
void solve(){
for(int k=0;k<MONEY;k++) //初始值為無窮大
Min[k]=INT_MAX;
Min[0]=0;
for(int j=0;j<VALUE;j++)
for(int i=type[j];i<MONEY;i++)
Min[i]=min(Min[i],Min[i-type[j]]+1); //遞推式
}
int main(){
int s;
solve(); //提前計算出所有金額對應的最少硬幣數量,打表
while(cin>>s)
cout<<Min[s]<<endl;
return 0;
}
拓展問題:要求列印最少硬幣的組合,
解決方法:增加一個記錄表Min_path[i],記錄金額i需要的最后一個硬幣,利用Min_path[]逐步倒推,就能得到所有的硬幣,
如:
Min_path[6]=5:最后一個硬幣是5元的;
Min_path[6-5]=1:1元硬幣;
Min_path[1-1]=0:結束;
代碼如下:
#include<bits/stdc++.h>
using namespace std;
const int MONEY=1001; //定義最大金額
const int VALUE=5; //5種硬幣
int type[VALUE]={1,5,10,25,50}; //5種面值
int Min[MONEY]; //每個金額對應最少的硬幣數量
int Min_path[MONEY]={0}; //記錄最小硬幣的路徑
void solve(){
for(int k=0;k<MONEY;k++)
Min[k]=INT_MAX;
Min[0]=0;
for(int j=0;j<VALUE;j++)
for(int i=type[j];i<MONEY;i++)
if(Min[i]>Min[i-type[j]]+1){
Min_path[i]=type[j]; //在每個金額上記錄路徑,即某個硬幣的面值
Min[i]=Min[i-type[j]]+1; //遞推式
}
}
void print_ans(int *Min_path,int s){ //列印硬幣組合
while(s){
cout<<Min_path[s]<<" ";
s=s-Min_path[s];
}
}
int main(){
int s;
solve();
while(cin>>s){
cout<<Min[s]<<endl; //輸出最少硬幣個數
print_ans(Min_path,s); //列印硬幣組合
}
return 0;
}
拓展問題:硬幣組合方案有幾種
代碼如下:
#include<bits/stdc++.h>
using namespace std;
const int COIN=101; //題目要求不超過100個硬幣
const int MONEY=251; //題目給定的錢數不超過250
int dp[MONEY][COIN]={0}; // DP轉移矩陣
int type[5]={1,5,10,25,50}; //5種面值
void solve(){ // DP
dp[0][0]=1;
for(int i=0;i<5;i++)
for(int j=1;j<COIN;j++)
for(int k=type[i];k<MONEY;k++)
dp[k][j]+=dp[k-type[i]][j-1];
}
int main(){
int s;
int ans[MONEY]={0};
solve(); //用DP計算完整的轉移矩陣
for(int i=0;i<MONEY;i++) //對每個金額,計算有多少種組合方案,打表
for(int j=0;j<COIN;j++) //從0開始,注意 dp[0][0]=1
ans[i]+=dp[i][j];
while(cin>>s)
cout<<ans[s]<<endl;
return 0;
}
2.0/1背包問題
用dp[i][j]陣列,表示將前i個物品放入容量為j的背包中產生的價值,
有 N 件物品和一個容量是 V 的背包,每件物品只能使用一次,
第 i 件物品的體積是 vi,價值是 wi,
求解將哪些物品裝入背包,可使這些物品的總體積不超過背包容量,且總價值最大,
輸出最大價值,
輸入格式
第一行兩個整數,N,V,用空格隔開,分別表示物品數量和背包容積,
接下來有 N 行,每行兩個整數 vi,wi,用空格隔開,分別表示第 i 件物品的體積和價值,
輸出格式
輸出一個整數,表示最大價值,
資料范圍
0<N,V≤1000
0<vi,wi≤1000
輸入樣例
4 5
1 2
2 4
3 4
4 5
輸出樣例:
8
代碼如下:
#include<bits/stdc++.h>
#define ll long long
using namespace std;
typedef struct{
int v;
int w;
}Node;
int dp[1005][1005];
int main()
{
int N,V;
scanf("%d%d",&N,&V);
Node* node=new Node[N+1];
for(int i=1;i<=N;i++)
scanf("%d%d",&node[i].v,&node[i].w);
for(int i=1;i<=N;i++){
for(int j=1;j<=V;j++){
if(node[i].v>j)
dp[i][j]=dp[i-1][j];
else
dp[i][j]=max(dp[i-1][j-node[i].v]+node[i].w,dp[i-1][j]);
}
}
printf("%d\n",dp[N][V]);
}
動態規劃法的設計思想:
如果各個子問題的不是獨立的,如果能夠保存已經解決的子問題的答案,而在需要的時候再找出已求得的答案,這樣就可以避免大量的重復計算,‘
基本思路是,用一個表記錄所有已解決的子問題的答案,不管該問題以后是否被用到,只要它被計算過,就將其結果填入表中,
3.完全背包問題
有 N 種物品和一個容量是 V 的背包,每種物品都有無限件可用,
第 i 種物品的體積是 vi,價值是 wi,
求解將哪些物品裝入背包,可使這些物品的總體積不超過背包容量,且總價值最大,
輸出最大價值,
輸入格式
第一行兩個整數,N,V,用空格隔開,分別表示物品種數和背包容積,
接下來有 N 行,每行兩個整數 vi,wi,用空格隔開,分別表示第 i 種物品的體積和價值,
輸出格式
輸出一個整數,表示最大價值,
資料范圍
0<N,V≤1000
0<vi,wi≤1000
輸入樣例
4 5
1 2
2 4
3 4
4 5
輸出樣例:
10
直接上一段代碼:
#include<bits/stdc++.h>
using namespace std;
typedef struct{
int v,w;
}Node;
int dp[1005][1005];
int main(){
int N,V;
scanf("%d%d",&N,&V);
Node* node=new Node[N+1];
for(int i=1;i<=N;i++)scanf("%d%d",&node[i].v,&node[i].w);
for(int i=1;i<=N;i++)
for(int j=0;j<=V;j++)
for(int k=0;k*node[i].v<=j;k++)
dp[i][j]=max(dp[i][j],dp[i-1][j-k*node[i].v]+k*node[i].w);
printf("%d\n",dp[N][V]);
}
這個代碼和0/1背包有些相似,這里求解dp陣列時,用3個for回圈,遍歷前i個物品在背包容量為j的背包中產生的最大價值,方法為:
取定第i個物品,直接回圈確定從0最大的k個i物品能產生的最大價值,可以理解為:裝入k個i物品,相當于將前i-1個物品選定最大價值裝入容量為j-knode[i].v的包中,那么這樣做的價值為dp[i-1][j-knode[i].v]+k*node[i].w,和原始的dp[i][j]不斷比較,逐漸選擇出最大價值的dp[i][j]
理解完全背包問題時,可以和0/1背包做比較,加深理解,
但是這題如果是這樣做的話,顯得太過暴力,事實上,這樣提交上去是會給超時的!
那么接下來,我們需要對這個問題做個優化:
首先了解dp更新時的內部關系:
dp[i][j]=max(dp[i-1][j],dp[i-1][j-v]+w,dp[i-1][j-2v]+2w+dp[i-1][j-3v]+3w+…)
遞推下去
dp[i][j - v] = max ( ,dp[i-1][j-v],dp[i-1][j-2v]+w+dp[i-1][j-3v]+2*w);
遞推得出:dp[i][j]=max(dp[i-1][j],dp[i][j-v]+w);
有了上面的遞推關系式,可以將代碼優化為:
for(int i=1;i<=N;i++)
for(int j=0;j<=V;j++)
if(j-node[i].v>=0)
dp[i][j]=max(dp[i][j],dp[i][j-node[i].v]+ndoe[i].w);
else dp[i][j]=dp[i-1][j];
至此,完全背包優化代碼如下:
#include<bits/stdc++.h>
using namespace std;
typedef struct{
int v,w;
}Node;
int dp[1005][1005];
int main(){
int N,V;
scanf("%d%d",&N,&V);
Node* node=new Node[N+1];
for(int i=1;i<=N;i++)scanf("%d%d",&node[i].v,&node[i].w);
for(int i=1;i<=N;i++)
for(int j=0;j<=V;j++)
if(j-node[i].v>=0)
dp[i][j]=max(dp[i-1][j],dp[i][j-node[i].v]+node[i].w);
else dp[i][j]=dp[i-1][j];
printf("%d\n",dp[N][V]);
}
4.最長公共子序列
給定兩個長度分別為 N 和 M 的字串 A 和 B,求既是 A 的子序列又是 B 的子序列的字串長度最長是多少,
輸入格式
第一行包含兩個整數 N 和 M,
第二行包含一個長度為 N 的字串,表示字串 A,
第三行包含一個長度為 M 的字串,表示字串 B,
字串均由小寫字母構成,
輸出格式
輸出一個整數,表示最大長度,
資料范圍
1≤N,M≤1000
輸入樣例:
4 5
acbd
abedc
分析:
分別遍歷兩個序列,
若Ai=Bj,求最長公共子序列的長度,則只需要求A的前i-1個和B的前j-1個公共子序列的長度,然后再加1即可,即:
dp[i][j]=dp[i-1][j-1]+1;
若Ai!=Bj,求最長公共子序列的長度,只有兩種可能,一種是A的前i個和B的前j-1的最長公共子序列長度,另一種是A的前i-1個和B的前j個的最長公共子序列長度,即:
dp[i][j]=max(dp[i-1][j],dp[i][j-1]);
知曉了動態方程,代碼就能很容易寫出:
#include<bits/stdc++.h>
using namespace std;
int num[1005][1005];
int main(){
int n,m;
string a,b;
scanf("%d%d",&n,&m);
cin>>a;
cin>>b;
memset(num,0,sizeof(num));
for(int i=1;i<=n;i++){
for(int j=1;j<=m;j++){
if(a[i-1]==b[j-1])num[i][j]=num[i-1][j-1]+1;
else
num[i][j]=max(num[i-1][j],num[i][j-1]);
}
}
printf("%d\n",num[n][m]);
}
5.最長遞增子序列(LIS問題)
給定一個長度為 N 的數列,求數值嚴格單調遞增的子序列的長度最長是多少,
輸入格式
第一行包含整數 N,
第二行包含 N 個整數,表示完整序列,
輸出格式
輸出一個整數,表示最大長度,
資料范圍
1≤N≤1000,
?109≤數列中的數≤109
輸入樣例:
7
3 1 2 1 8 5 6
輸出樣例:
4
分析:
用雙重回圈,第一層回圈遍歷這個數列,第二個回圈借助在第一層回圈遍歷到的數列下標i,再次遍歷陣列從0到i-1,如果遍歷的數列num[i]>num[j],那么最長遞增長度要么是以遍歷到j的最長長度+1,要么就是原先計算出來的最長長度(一開始每個第一層回圈開始時元素的最長遞增長度都設定為1),接著再用一個元素記錄最長的長度即可,
這樣做的復雜度為O(n^2),
代碼如下:
#include<bits/stdc++.h>
using namespace std;
int main(){
int N;
scanf("%d",&N);
int* num=new int[N];
int* dp=new int[N];
int len=1;
for(int i=0;i<N;i++)scanf("%d",&num[i]);
for(int i=0;i<N;i++){
dp[i]=1;
for(int j=0;j<i;j++)
if(num[i]>num[j])
dp[i]=max(dp[i],dp[j]+1);
len=max(len,dp[i]);
}
printf("%d\n",len);
}
該題可以用更高效的演算法,利用dp以及二分法,dp表示規則為:
dp[i]表示長度為i的遞增子序列中最后一個元素最小的值,
狀態計算:對所有num[i],如果大于dp[cnt-1],(下標從0開始,cnt長度的最長上升子序列,末尾最小的數字),那就cnt++,使得最長上升序列長度+1,當前末尾最小元素為num[i];若num[i]小于等于dp[cnt-1],說明不會更新當前的長度,但之前末尾的最小元素要發生變化,找到第一個 大于或等于 (這里不能是大于) num[i],更新以那時候末尾的最小元素,
dp[i]一定是一個單調遞增的陣列,所以可以用二分法查找,復雜度為O(nlogn)
代碼如下:
#include<bits/stdc++.h>
using namespace std;
int main(){
int N,cnt=0;
scanf("%d",&N);
int* num=new int[N];
int* dp=new int[N];
for(int i=0;i<N;i++)scanf("%d",&num[i]);
dp[cnt++]=num[0];
for(int i=1;i<N;i++){
if(num[i]>dp[cnt-1])dp[cnt++]=num[i];
else{
int l=0,r=cnt-1;
while(l<r){
int mid=(l+r)>>1;
if(dp[mid]>=num[i])r=mid;
else l=mid+1;
}
dp[r]=num[i];
}
}
printf("%d\n",cnt);
}
遞推和記憶化搜索
前面dp的狀態轉移,都是用遞推的方法,
有另一種方法,邏輯上的理解更加直接,就是“遞回+記憶化搜索”實作遞回,
經典題:The Triangle
給定一個n層的三角形數塔,從頂部第一個數往下走,每層經過一個數字,直到最底層,只能走斜下方的左邊一個數或右邊一個數,問所有可能走到的路徑最大的數字和為多少?
分析:
本題如果按“從頂往下”的計算方法,有2^n個路徑,導致TLE,
可以使用遞推,即從底往上計算,對數塔上的每個點記錄狀態,dp[i][j]記錄從第i層第j個數開始往下走的數字和,每個節點算一次,復雜度O(n^2),
dp的另一種寫法:遞回+記憶化搜索:
首先,寫遞回程式,暴力搜索所有2^n個路徑
int dfs(int i,int j){
if(i==n)
return a[i][j];//遞回邊界,到達最后一行,回傳
return dp[i][j]=max(dfs(i+1,j),dfs(i+1,j+1))+a[i][j];
//從左邊走上來,或者從右邊走上來,取其中大的,
}
遞回的優化:記憶化搜索
遞回時,可以避免大量的重復計算,如下圖:

觀察第3層的中間數“1”,從第二層的“3”往下走會經過它,從第二層的“8”往下走也會經過它,那么就重復計算了1開始的遞回,只要能避免這些重復計算,就能優化,
int dfs(int i,int j){
if(i==n)
return a[i][j];
if(dp[i][j]>=0)return dp[i][j];//記憶
return dp[i][j]=max(dfs(i+1,j),dfs(i+1,j+1))+a[i][j];
}
區間dp
步驟:(1)先在小區間進行dp得到最優解;
(2)合并小區間的最優解,求大區間的最優解
經典題1:石子合并
設有 N 堆石子排成一排,其編號為 1,2,3,…,N,
每堆石子有一定的質量,可以用一個整數來描述,現在要將這 N 堆石子合并成為一堆,
每次只能合并相鄰的兩堆,合并的代價為這兩堆石子的質量之和,合并后與這兩堆石子相鄰的石子將和新堆相鄰,合并時由于選擇的順序不同,合并的總代價也不相同,
例如有 4 堆石子分別為 1 3 5 2, 我們可以先合并 1、2 堆,代價為 4,得到 4 5 2, 又合并 1,2 堆,代價為 9,得到 9 2 ,再合并得到 11,總代價為 4+9+11=24;
如果第二步是先合并 2,3 堆,則代價為 7,得到 4 7,最后一次合并代價為 11,總代價為 4+7+11=22,
問題是:找出一種合理的方法,使總的代價最小,輸出最小代價,
輸入格式
第一行一個數 N 表示石子的堆數 N,
第二行 N 個數,表示每堆石子的質量(均不超過 1000),
輸出格式
輸出一個整數,表示最小代價,
資料范圍
1≤N≤300
輸入樣例:
4
1 3 5 2
輸出樣例:
22
分析:
核心:最后一次合并一定是左邊連續的一部分和右邊連續的一部分進行合并
狀態表示:dp[i][j]表示把第i堆到第j堆合并的方案集合中的最小代價,
區間dp的常用模板:
第一維列舉區間長度,從1到n-1;第二維列舉起點i,從i到n-len;則終點就為j=i+len(自動生成)
代碼如下:
#include<bits/stdc++.h>
using namespace std;
const int INF=1<<30;
const int N=305;
int sum[N],n;
int Minval(){
int dp[N][N];
for(int i=1;i<=n;i++)
dp[i][i]=0;
for(int len=1;len<n;len++) //len是i和j之間的距離
for(int i=1;i<=n-len;i++){ //從第i堆開始
int j=i+len; //自動得到右端點
dp[i][j]=INF;
for(int k=i;k<j;k++) //i和j之間,用k進行分割
dp[i][j]=min(dp[i][j],dp[i][k]+dp[k+1][j]+sum[j]-sum[i-1]);
}
return dp[1][n];
}
int main(){
cin>>n;
sum[0]=0;
for(int i=1;i<=n;i++){
int x;
cin>>x;
sum[i]=sum[i-1]+x; //sum[i,j]的值等于 sum[j]-sum[i-1]
}
cout<<Minval()<<endl;
return 0;
}
經典題2:回文串(暫略)
樹形dp
在樹這種資料結構上進行的dp:給出一棵樹,要求以最少的代價(或取得最大收益)完成給定的操作,
在樹上做動態規劃很適合,因為樹本身有“子結構”性質,具有遞回性,符合dp的性質,
轉載請註明出處,本文鏈接:https://www.uj5u.com/ruanti/377152.html
標籤:其他
上一篇:基于java Springboot+Vue+shiro前后端分離疫情防疫管理系統設計和實作2.0
下一篇:回文串問題一網打盡
