01背包
動態規划是一種高效的算法。在數學和計算機科學中,是一種將復雜問題的分成多個簡單的小問題思想 ---- 分而治之。因此我們使用動態規划的時候,原問題必須是重疊的子問題。運用動態規划設計的算法比一般朴素算法高效很多,因為動態規划不會重復計算已經計算過的子問題。因為動態規划又可以稱為“記憶化搜索”。
01背包是介紹動態規划最經典的例子,同時也是最簡單的一個。我們先看看01背包的是什么?
問題(01背包):
有n個重量和價值分別為vi和ci的物品。從這些物品中挑出總重量不超過m的物品,求所有挑選方案中價值總和的最大值。
這就是被稱為01背包的問題。在沒學習動態規划之前,我們看到這個問題第一反應會用dfs搜索一遍。那我們先使用這種方法來求解01背包問題:
#include<cstdio> #include<algorithm> using namespace std; const int N=1e3+1; int n,m,v[N],c[N]; //從第i個物品開始挑選總重量不大於sumv的部分 int dfs(int now,int sumv){ int res; if(now==n+1) return res=0;//已經沒有剩余物品 if(sumv<v[now]) res=dfs(now+1,sumv);//無法挑選第i個物品 else res=max(dfs(now+1,sumv),dfs(now+1,sumv-v[now])+c[now]);//比較挑和不挑的情況,選取最大的情況 return res; } int main(){ scanf("%d%d",&m,&n); for(int i=1;i<=n;i++) scanf("%d%d",&v[i],&c[i]); printf("%d",dfs(1,m)); return 0; } /* 期望得分:30分 */
乍一看dfs好像就可以解決這個問題,那還有動態規划什么事。然而我們仔細分析一下時間復雜度,每一種狀態都用選或者不選兩種可能。所以我們可以得出使用dfs的時間復雜度為O(2^n)。顯然這個方法不是一個很好的方法,因為這個時間復雜度太高了。我們仔細研究可以發現,造成時間復雜度這么高的原因是重復計算。既然我們找到復雜度這么高的原因,那我們就可以想辦法減少它重復計算的次數。仔細分析容易想到,使用一個二維數組來記錄每一次搜索的答案,這樣我們就避免了重復計算。
這樣的小技巧,我們稱之為記憶化搜索。我們只是小小的改變就讓它的時間復雜度降低至O(nW)。
#include<cstdio> #include<cstring> #include<algorithm> using namespace std; const int N=1e3+1; int n,m,v[N],c[N],dp[N][N*10];//保存每一次搜索的答案 int dfs(int now,int sumv){ if(dp[now][sumv]!=-1) return dp[now][sumv]; int res; if(now==n+1) return dp[now][sumv]=0; if(sumv<v[now]) res=dfs(now+1,sumv); else res=max(dfs(now+1,sumv),dfs(now+1,sumv-v[now])+c[now]); return dp[now][sumv]=res; } int main(){ memset(dp,-1,sizeof dp);//初始化dp數組的值,使其全為-1 scanf("%d%d",&m,&n); for(int i=1;i<=n;i++) scanf("%d%d",&v[i],&c[i]); printf("%d",dfs(1,m)); return 0; } /* 期望得分:60-80分 */
仔細分析,可以發現我們還可以有更簡單的寫法(轉成遞推):
#include<cstdio> #include<cstring> #include<algorithm> using namespace std; const int N=1e3+1; int n,m,v[N],c[N],dp[N][N*10]; //dp[i][j] 表示取了i個物品挑選出總重量不超過j的物品時,背包中的最大價值 int main(){ memset(dp,-1,sizeof dp); scanf("%d%d",&m,&n); for(int i=1;i<=n;i++) scanf("%d%d",&v[i],&c[i]); for(int i=0;i<=m+1;i++) dp[n+1][i]=0;//還沒開始挑選的時候,背包里的總價值為0 for(int i=n;i;i--){ for(int j=m;j>=0;j--){ if(j<v[i]) dp[i][j]=dp[i+1][j]; else dp[i][j]=max(dp[i+1][j],dp[i+1][j-v[i]]+c[i]); } } printf("%d",dp[1][m]); return 0; } /* 期望得分:60-80分 */
然后dp[i][j]的僅由dp[i+1][j]||dp[i+1][j-v[i]]轉移而來,於是我們可以滾動第一維:
#include<cstdio> #include<cstring> #include<algorithm> using namespace std; const int N=1e3+1; int n,m,v[N],c[N],dp[2][N*10]; int main(){ memset(dp,-1,sizeof dp); scanf("%d%d",&m,&n); for(int i=1;i<=n;i++) scanf("%d%d",&v[i],&c[i]); int now=0;//開啟滾動 for(int i=0;i<=m;i++) dp[now][i]=0;//當前狀態初始化 for(int i=n;i;i--){ now^=1; for(int j=m;j>=0;j--){ if(j<v[i]) dp[now][j]=dp[now^1][j]; else dp[now][j]=max(dp[now^1][j],dp[now^1][j-v[i]]+c[i]); } } printf("%d",dp[now][m]); return 0; } /* 期望得分:100分 */
凡是可以滾動的,必定可以降維。——shenben
#include<cstdio> #include<cstring> #include<algorithm> using namespace std; const int N=1e3+1; int n,m,v[N],c[N],dp[N*10]; int main(){ scanf("%d%d",&m,&n); for(int i=1;i<=n;i++) scanf("%d%d",&v[i],&c[i]); dp[0]=0; for(int i=1;i<=n;i++){ for(int j=m;j>=v[i];j--){//v[i]以后的j對答案沒有貢獻 dp[j]=max(dp[j],dp[j-v[i]]+c[i]); } } printf("%d",dp[m]); return 0; } /* 期望得分:100分 */
使用遞推方程直接求解的方法,我們稱之為dp。因為他每一次的選取,都在動態的計算最優的情況。當然可能他局部不是最優,但是整體一定是最優解。這就是他和貪心算法最大的不同,貪心算法,每一次都是最優,但是整體不一定不是最優。
多重背包
問題(多重背包):
就是一個0,1,2……k背包(往01背包上想就好了)
有n種重量和價值分別為vi和ci的物品,每種物品有si個。從這些物品中挑出總重量不超過m的物品,求所有挑選方案中價值總和的最大值
搜索
//期望得分:30-70分 #include<cstdio> #include<algorithm> #define N 10100 using namespace std; int v[N],c[N],s[N],n,m; int ans; void dfs(int now,int sumv,int sumc){ if(now==n+1){if(sumv<=m) ans=max(ans,sumc);return ;} dfs(now+1,sumv,sumc); for(int i=1;i<=s[now];i++) dfs(now+1,sumv+i*v[now],sumc+i*c[now]); } int main(){ scanf("%d%d",&m,&n); for(int i=1;i<=n;i++) scanf("%d%d%d",v+i,c+i,s+i); dfs(1,0,0); printf("%d",ans); return 0; }
記憶化搜索
//期望得分:60-100分 #include<cstdio> #include<algorithm> #define N 10100 using namespace std; int v[N],c[N],s[N],n,m; int ans; int dp[101][N]; int dfs(int now,int sumv){ if(now==n+1) return 0; if(dp[now+1][sumv]) dp[now][sumv]=dp[now+1][sumv]; else dp[now][sumv]=dfs(now+1,sumv); for(int i=1;i<=s[now];i++){ if(sumv+i*v[now]>m) break; if(dp[now+1][sumv+i*v[now]]) dp[now][sumv]=max(dp[now][sumv],dp[now+1][sumv+i*v[now]]+i*c[now]); else dp[now][sumv]=max(dp[now][sumv],dfs(now+1,sumv+i*v[now])+i*c[now]); } return dp[now][sumv]; } int main(){ scanf("%d%d",&m,&n); for(int i=1;i<=n;i++) scanf("%d%d%d",v+i,c+i,s+i); dfs(1,0); printf("%d",dp[1][0]); return 0; }
記憶化搜索轉遞推
//期望得分:60-100分 #include<cstdio> #include<algorithm> #define N 10100 using namespace std; int v[N],c[N],s[N],n,m; int ans; int dp[101][N]; int main(){ scanf("%d%d",&m,&n); for(int i=1;i<=n;i++) scanf("%d%d%d",v+i,c+i,s+i); for(int i=n;i;i--){ for(int j=m;j>=0;j--){ for(int k=0;k<=s[i];k++){ if(j-k*v[i]<0) break; dp[i][j]=max(dp[i][j],dp[i+1][j-k*v[i]]+k*c[i]); } } } printf("%d",dp[1][m]); return 0; }
滾動數組(滾動第一維)
//期望得分:100分 #include<cstdio> #include<algorithm> #define N 10100 using namespace std; int v[N],c[N],s[N],n,m; int ans; int dp[2][N]; int main(){ scanf("%d%d",&m,&n); for(int i=1;i<=n;i++) scanf("%d%d%d",v+i,c+i,s+i); int now=0; for(int i=n;i;i--){ now^=1; for(int j=m;j>=0;j--){ for(int k=0;k<=s[i];k++){ if(j-k*v[i]<0) break; dp[now][j]=max(dp[now][j],dp[now^1][j-k*v[i]]+k*c[i]); } } } printf("%d",dp[now][m]); return 0; }
降維(用二進制優化)
//期望得分:100分 #include<cstdio> #include<iostream> using namespace std; inline int read(){ register int x=0,f=1; register char ch=getchar(); while(ch<'0'||ch>'9'){if(ch=='-')f=-1;ch=getchar();} while(ch>='0'&&ch<='9'){x=x*10+ch-'0';ch=getchar();} return x*f; } const int N=1e5+10; int n,m,cnt,xp[30]; int f[N*200],v[N],c[N]; int main(){ for(int i=0;i<=25;i++) xp[i]=1<<i; m=read();n=read(); for(int i=1,x,y,z;i<=n;i++){ x=read();y=read();z=read(); for(int t=0;z>xp[t];t++){ v[++cnt]=x*xp[t]; c[cnt]=y*xp[t]; z-=xp[t]; } if(z>0){ v[++cnt]=x*z; c[cnt]=y*z; } } for(int i=1;i<=cnt;i++){ for(int j=m;j>=v[i];j--){ f[j]=max(f[j],f[j-v[i]]+c[i]); } } printf("%d",f[m]); return 0; }
其他背包dp自行整理