log:
点击查看日志
(25.1.12 开始编写)
.....
(25.1.22 加入完全和多重背包)
(25.1.23 更新例题)
动态规划
对于一个能用动态规划解决的问题,一般采用如下思路解决:
1.将原问题划分为若干 阶段,每个阶段对应若干个子问题,提取这些子问题的特征(称之为 状态);
2.寻找每一个状态的可能 决策,或者说是各状态间的相互转移方式(用数学的语言描述就是 状态转移方程)。
3.按顺序求解每一个阶段的问题。
4.能用dp
解决的问题需要有三个要素:最优子结构,无后效性和子问题重叠。
1.背包问题
例1.1 01背包之:[NOIP2005 普及组] 采药
问题:总共有 \(M\) 个草药,每个草药得价值为 \(v\) ,每采一个草药需要花 \(t\) 个时间,要求要在一个固定时间 \(T\) 内使采得的药物总价值最大。
方法1:dfs
暴力搜索
解法:一个一个往前摸
完整代码:
#include <bits/stdc++.h>
using namespace std;const int N=1e3+10;
int t[N],v[N],ans;
int T,M;void dfs(int sumv,int pos,int sumt){if(sumt>T) return ;if(pos==M) {ans=max(ans,sumv);return ;}dfs(sumv+v[pos],pos+1,sumt+t[pos]);dfs(sumv,pos+1,sumt);//回溯
}int main(){cin>>T>>M;for(int i=0;i<M;i++){cin>>t[i]>>v[i];}dfs(0,0,0);cout<<ans;return 0;
}
结过:超时,时间复杂度为 \(O(2^N)\) 级别。
方法2:dp
(动态规划)
解法:
考虑这几种情况:
1.不摘草药
时间不变,把前面得状态复制过来,
dp[i][j]=dp[i-1][j];
2.采摘草药
当前时间减去该草药所需得时间,并且加上该草药的价值,
dp[i][j]=dp[i-1][j-t[i]]+v[i];
然后进行比较
dp[i][j]=max(dp[i-1][j],dp[i-1][j-t[i]]+v[i]);
完整代码:
#include <bits/stdc++.h>
using namespace std;int t[1001],v[101],dp[1001][1001]; int main(){int T,M;cin>>T>>M;for(int i=1;i<=M;i++){cin>>t[i]>>v[i];} for(int i=1;i<=M;i++){ for(int j=1;j<=T;j++){ if(j>=t[i]){dp[i][j]=max(dp[i-1][j-t[i]]+v[i],dp[i-1][j]);}else{dp[i][j]=dp[i-1][j];}}}cout<<dp[M][T];return 0;
}
结过:AC
例1.2 :[NOIP2001 普及组] 装箱问题
问题:有一个箱子容量为 \(V\),同时有 \(n\) 个物品,每个物品有一个体积。现在从 \(n\) 个物品中,任取若干个装入箱内(也可以不取),使箱子的剩余空间最小。输出这个最小值。
方法1:dfs
无剪枝暴力
解法:题目就是求背包最多能装多少,只不过是换个问法而已,暴搜挨个试,
void dfs(int pos,int sumw){if(sumw>v) return ;if(pos>n){ans=max(sumw,ans);return ;}dfs(pos+1,sumw+w[pos]);dfs(pos+1,sumw);
}
数据水所以能ac。
方法2:dp
(动态规划)
解法:推动态转移方程,有两种情况,装和不装,不装为 \(dp[i][j]=dp[i-1][j]\),装为 \(dp[i][j]=dp[i-1][j]+w[i]\),代码懒得黏了。
1.3空间优化
见此处
1.4 二维01背包
二维01背包就是有两个性质。
题目:NASA的食物计划`
很轻松的推出状态转移方程,几乎和模板没区别,只是多了一维。
1.拿: \(dp[i][j][z]=dp[i-1][j][z]\)
2.不拿:\(dp[i][j][z]=dp[i-1][j-h[i]][z-t[i]]+k[i]\), 减去损耗再加上卡路里
得到状态转移方程: \(dp[i][j][z]=max(dp[i-1][j][z],dp[i-1][j-h[i]][z-t[i]]+k[i])\)
代码:
for(int i=1;i<=n;i++){for(int j=1;j<=h;j++){for(int z=1;z<=t;z++){if(j>=hs[i]&&z>=ts[i]){dp[i][j][z]=max(dp[i-1][j][z],dp[i-1][j-hs[i]][z-ts[i]]+k[i]);}else dp[i][j][z]=dp[i-1][j][z];} }}
当然还可已进行优化,把之前的滚动数组变成二维:
\(dp[j][z]=max(dp[j][z],dp[j-h[i]][z-t[i]]+k[i])\)
for(int i=1;i<=n;i++){for(int j=h;j>=hs[i];j++){for(int z=t;z>=ts[i];z++){dp[j][z]=max(dp[j][z],dp[j-h[i]][z-t[i]]+k[i])} }}
1.5 变形题
例:P1509 找啊找啊找GF
多维dp+次要性动态规划,既要保证两个最优,可以设置两个dp来操作,一个人数一个时间,状态转移方程还是很好退的,就怕写的时候漏条件了。
无优化的代码:
#include <bits/stdc++.h>
using namespace std;
const int N=1e2+10;
int n,rmb[N],rp[N],t[N],dpn[N][N][N],dpt[N][N][N];
int main(){int n;cin>>n;for(int i=1;i<=n;i++){cin>>rmb[i]>>rp[i]>>t[i];}int m,r;cin>>m>>r;for(int i=1;i<=n;i++){for(int j=1;j<=m;j++){for(int k=1;k<=r;k++){if(rmb[i]<=j&&rp[i]<=k){if(dpn[i-1][j][k]<dpn[i-1][j-rmb[i]][k-rp[i]]+1){dpn[i][j][k]=dpn[i-1][j-rmb[i]][k-rp[i]]+1;dpt[i][j][k]=dpt[i-1][j-rmb[i]][k-rp[i]]+t[i];}else {if(dpn[i-1][j][k]==dpn[i-1][j-rmb[i]][k-rp[i]]+1){dpn[i][j][k]=dpn[i-1][j][k];dpt[i][j][k]=min(dpt[i-1][j][k],dpt[i-1][j-rmb[i]][k-rp[i]]+t[i]);}else {dpn[i][j][k]=dpn[i-1][j][k];dpt[i][j][k]=dpt[i-1][j][k];}}}else {dpn[i][j][k]=dpn[i-1][j][k];dpt[i][j][k]=dpt[i-1][j][k];}}}}cout<<dpt[n][m][r];return 0;
}
进行优化后的代码:
#include <bits/stdc++.h>
using namespace std;
int rmb[1001],rp[1001],t[1001],dpn[1001][1001],dpt[1001][1001];
int main(){int n,m,r;cin>>n;for(int i=1;i<=n;i++) cin>>rmb[i]>>rp[i]>>t[i];cin>>m>>r;for(int i=1;i<=n;i++){for(int j=m;j>=rmb[i];j--){for(int k=r;k>=rp[i];k--){if(dpn[j][k]<dpn[j-rmb[i]][k-rp[i]]+1){dpn[j][k]=dpn[j-rmb[i]][k-rp[i]]+1;dpt[j][k]=dpt[j-rmb[i]][k-rp[i]]+t[i];}else {if(dpn[j][k]==dpn[k-rmb[i]][j-t[i]]+1) dpt[j][k]=min(dpt[j][k],dpt[j-rmb[i]][k-rp[i]]+t[i]);}}}}cout<<dpt[m][r];return 0;
}
状态转移方程
\(dpnum[j][k]=max(dpnum[j][k],dpnum[j-rmb[i]][k-rp[i]]+1)\)
\(dptime[j][k]=min(dptime[j][k],dptime[j-rmb[i]][k-rp[i]]+time[i])\)
1.6 完全背包
不同于01背包,完全背包可以无限次拿取物品,开了。
解法:
01背包压维优化可知如果二层循环是正序的话,就会多次拿取物品,符合完全背包的要求。
模板
for(int i=1;i<=n;i++){for(int j=w[i];j<=w;j++){dp[j]=max(dp[j],dp[j-w[i]]+v[i]);}
}
1.7 多重背包
和完全背包不同的是,它拿物品的次数是有限制的,如只能拿 \(num\) 次。
解法和模板:
1.像01背包一样当成单个物品使用。
for(int i=1;i<=n;i++){for(int l=1;l<=num[i];l++){//使用num次for(int j=w;j>=w[i],j--){dp[j]=max(dp[j],dp[j-w[i]]+v[i]);}}
}
for(int i=1;i<=mxx;i++){mx=max(mx,dp[i]);
}
2.考虑使用情况,也就是使用次数。
for(int i=1;i<=n;i++){for(int j=0;j<=w;j++){for(int l=1;l<=num[i];l++){if(l*w[i]<j){dp[j]=max(dp[j],dp[j-l*w[i]]+l*v[i]);}}}
}for(int i=1;i<=mxx;i++){mx=max(mx,dp[i]);
}
1.8 一些例题
1.P6771 [USACO05MAR] Space Elevator 太空电梯
解法:
套多重背包的板子,把价值和重量都变成高度,背包的容积为限制高度,次数为材料可用次数,注意高度限制越低的越往下垒。可得动态转移方程为 \(dp[j]=max(dp[j],dp[j-h[i]]+h[i])\) 。题目整体难度不大,但要注意细节的把控。
代码:
#include <bits/stdc++.h>
using namespace std;
const int N=4e4+10;
int dp[N];
struct Build{int h,a,c;
}b[N];
bool cmp(Build aa,Build b){return aa.a<b.a;
}
int main(){int n,mxx=0;cin>>n;for(int i=1;i<=n;i++){cin>>b[i].h>>b[i].a>>b[i].c;mxx=max(mxx,b[i].a);}sort(b+1,b+n+1,cmp);for(int i=1;i<=n;i++){for(int l=1;l<=b[i].c;l++){for(int j=b[i].a;j>=b[i].h;j--){dp[j]=max(dp[j],dp[j-b[i].h]+b[i].h);}}}int mx=INT_MIN;for(int i=1;i<=mxx;i++){mx=max(mx,dp[i]);}cout<<mx;return 0;
}
2.P5662 [CSP-J2019] 纪念品
解法:
考虑明天剩余的钱,今天买了明天再买,得出动态转移方程 \(dp[k-a[i][j]]=max(dp[k-a[i][j]],dp[k]-a[i][j]+a[i-1][j])\) 。dp[k]相当于还剩 \(k\) 钱的可获得的最大价值,太几把复杂了,当然还有第二种方法。
代码:
#include <bits/stdc++.h>
using namespace std;
const int N=1e4+10;
int a[N][N];
int dp[N];
int main(){int t,n,m;cin>>t>>n>>m;int tmp=1;for(int i=1;i<=t;i++){for(int j=1;j<=n;j++){cin>>a[i][j];}}//int mx=0;int ans=m;for(int i=1;i<t;i++){memset(dp,0,sizeof(dp));//初始化dp[ans]=ans;//不买不买时候的价值for(int j=1;j<=n;j++){for(int k=ans;k>=a[i][j];k--){//明天的的钱数dp[k-a[i][j]]=max(dp[k-a[i][j]],dp[k]+a[i+1][j]-a[i][j]);//如果买了,且明天的价值更大就直接卖了}}int mx=0;for(int j=0;j<=ans;j++){mx=max(mx,dp[j]);}ans=mx;}cout<<ans<<endl;return 0;
}