常用 dp 状态:\(dp_i\) 表示以 \(i\) 结尾的 XXX / 前 \(i\) 个元素的 XXX。
涉及的类型(由易到难):线性 dp,背包,区间 dp,树形 dp(换根 dp),状压 dp,dp 的各类优化(数据结构优化、斜率优化、四边形不等式优化......)。
背包问题:01 背包、完全背包、分组背包、多重背包、树上背包。
P1455
并查集维护配套,按照所有套餐进行 01 背包即可。
总结:本题是并查集与 01 背包的结合,具有综合性。
code
#include<bits/stdc++.h>
using namespace std;
const int N=1e4+5;
int n,m,w;
int c[N],d[N];
int p[N],fa[N],dp[N];
int fnd(int x){
return (fa[x]==x?x:fa[x]=fnd(fa[x]));
}
void uni(int x,int y){
x=fnd(x),y=fnd(y);
if(x!=y){
fa[x]=y;
c[y]+=c[x];
d[y]+=d[x];
}
}
int main(){
ios::sync_with_stdio(0);
cin.tie(0);
cin>>n>>m>>w;
for(int i=1;i<=n;i++)
cin>>c[i]>>d[i],fa[i]=i;
for(int i=1,u,v;i<=m;i++)
cin>>u>>v,uni(u,v);
int cnt=0;
for(int i=1;i<=n;i++){
if(fa[i]==i)
p[++cnt]=i;
}
for(int i=1;i<=cnt;i++)
for(int j=w;j>=c[p[i]];j--)
dp[j]=max(dp[j],dp[j-c[p[i]]]+d[p[i]]);
cout<<dp[w];
return 0;
}
CF507A
定义 \(path_{i,j}\) 记录背包容量为 \(j\) 时物品 \(i\) 是否转移成功,每次转移时标记即可。
最后递归地寻找答案,具体见代码。
总结:本题是记录决策点的 01 背包,以后要求记录决策点的 dp 都可以仿照着这样写。
code
#include<bits/stdc++.h>
using namespace std;
const int N=1e2+5,K=1e4+5;
int n,k,cnt;
int c[N],dp[K],ans[N];
bool path[N][K];
void getans(int cur,int l){
if(!cur)
return;
if(path[cur][l]){
ans[++cnt]=cur;
getans(cur-1,l-c[cur]);
}
else
getans(cur-1,l);
}
int main(){
ios::sync_with_stdio(0);
cin.tie(0);
cin>>n>>k;
for(int i=1;i<=n;i++)
cin>>c[i];
for(int i=1;i<=n;i++){
for(int j=k;j>=c[i];j--){
if(dp[j]<dp[j-c[i]]+1){
dp[j]=dp[j-c[i]]+1;
path[i][j]=1;
}
}
}
getans(n,k);
cout<<cnt<<'\n';
for(int i=1;i<=cnt;i++)
cout<<ans[i]<<' ';
return 0;
}
AT_abc054_d
既然有两种类型的物品,我们就设两维,令 \(dp_{i,j}\) 表示选重量为 \(i\) 的 A 元素且选重量为 \(j\) 的 B 元素所能获得的最大价值。仍然像 01 背包一样转移即可。
最后,我们枚举所有状态,对于所有满足要求的状态取最小值即可。
总结:本题是具有多个维度的 01 背包。谨记:当存在多个物品时,考虑增设维度。
code
#include<bits/stdc++.h>
using namespace std;
const int N=5e2+5;
int n,ma,mb,sa,sb;
int a[N],b[N],c[N];
int dp[N][N];
int main(){
ios::sync_with_stdio(0);
cin.tie(0);
cin>>n>>ma>>mb;
for(int i=1;i<=n;i++){
cin>>a[i]>>b[i]>>c[i];
sa+=a[i],sb+=b[i];
}
memset(dp,0x3f,sizeof dp);
dp[0][0]=0;
for(int i=1;i<=n;i++)
for(int j=sa;j>=a[i];j--)
for(int k=sb;k>=b[i];k--)
dp[j][k]=min(dp[j][k],dp[j-a[i]][k-b[i]]+c[i]);
int ans=1e9;
for(int i=1;i<=sa;i++)
for(int j=1;j<=sb;j++)
if(j*ma==i*mb)
ans=min(ans,dp[i][j]);
cout<<(ans==1e9?-1:ans);
return 0;
}
AT_abc118_d
首先这显然是个完全背包,我们先按照完全背包的方式做,令 \(dp_i\) 表示用 \(i\) 根火柴能拼成的最大位数。
显然,位数越大越好,位数相同的情况下数码越大越好,所以我们将 \(a\) 先排个序。
然后从 \(n\) 开始(这样保证了位数最大),每次在 \(m\) 个数码中选取能拼成 \(dp_n\) 这么多位且当前这一位为 \(a_i\) 的最大的那个 \(a_i\),记录下它即可。
总结:本题是完全背包与贪心的结合,这启发我们可以在 dp 之后贪心选取最优方案,这两个东西并不是水火不容。
code
#include<bits/stdc++.h>
using namespace std;
const int N=1e4+5;
const int num[]={0,2,5,5,4,5,6,3,7,6};
int n,m,tot;
int a[N],dp[N],ans[N];
int main(){
ios::sync_with_stdio(0);
cin.tie(0);
cin>>n>>m;
for(int i=1;i<=m;i++)
cin>>a[i];
memset(dp,0xcf,sizeof dp);
dp[0]=0;
for(int i=1;i<=m;i++)
for(int j=num[a[i]];j<=n;j++)
dp[j]=max(dp[j],dp[j-num[a[i]]]+1);
//int cur=dp[n];
sort(a+1,a+m+1);
while(n){
//cout<<cur<<'\n';
int p=0;
for(int i=1;i<=m;i++)
if(n>=num[a[i]]&&dp[n-num[a[i]]]+1==dp[n])
p=i;
n-=num[a[p]];
ans[++tot]=a[p];
}
for(int i=1;i<=tot;i++)
cout<<ans[i];
return 0;
}
CF632E
正解是 FFT / NTT,但是朴素完全背包也可以过。
这题并不要求最大价值,于是我们换一种状态定义:令 \(dp_i\) 表示凑成价值为 \(i\) 需要的最少商品数,转移 \(dp_i=\min(dp_i,dp_{i-a_j}+1)\),这样方便统计答案。
但是题目需要恰好取 \(k\) 件商品,于是我们可以将每个物品的价值全都减去最小的那个商品的价值,这样任意商品数 \(\le k\) 的方案都可以通过不断加上最小的那个商品从而凑成 \(k\) 个,之后在加回去就可以了。
总结:本题是完全背包的变种,且用到了一个小技巧。我们必须要依据题目关心的东西来设计状态。
code
#include<bits/stdc++.h>
using namespace std;
const int N=1e6+5;
int n,k,mx,mn=1e9;
int a[N],dp[N];
int main(){
ios::sync_with_stdio(0);
cin.tie(0);
cin>>n>>k;
for(int i=1;i<=n;i++){
cin>>a[i];
mn=min(mn,a[i]);
mx=max(mx,a[i]);
}
mx-=mn;
for(int i=1;i<=n;i++)
a[i]-=mn;
memset(dp,0x3f,sizeof dp);
dp[0]=0;
for(int i=0;i<=k*mx;i++)
for(int j=1;j<=n;j++)
if(i>=a[j])
dp[i]=min(dp[i],dp[i-a[j]]+1);
for(int i=0;i<=k*mx;i++)
if(dp[i]<=k)
cout<<i+k*mn<<' ';
return 0;
}