背包九讲

· · 个人记录

01背包

例题:采药.

我们可以用搜索完成,\text O(2^n),会T的飞起.

搜索中,一般会带进这些参数: 正在选的数与已选数的背包容积。

我们可以对其记忆化,其时间复杂度达到 O(nm),够快了。

我们继续把它用递推形式写下来。

因为递推习惯从小向大递推状态,所以改一个顺序递推,得到以下代码:

#include<bits/stdc++.h>
using namespace std;
int t,m;
int dp[110][1010],t1[1010],m1[110];
int main() {
    scanf("%d%d",&t,&m);
    for(int i=1;i<=m;i++){
        scanf("%d%d",&t1[i],&m1[i]);
    }
    for(int i=1;i<=m;i++){
        for(int j=1;j<=t;j++){
            dp[i][j]=(j>=t1[i] ? max(dp[i-1][j],dp[i-1][j-t1[i]]+m1[i]) : dp[i-1][j]);
        }
    }
    cout<<dp[m][t];
    return 0;
}

背包问题算一种动态规划(dp),所以数组叫 dp.

但这样,我们就罢休了吗?

不,因为 dp[i][x] 只用到了 dp[i-1] 层的东西,所以我们只需要记录 dp[i-1] 层的数组。

这个叫做滚动数组优化。

因为递推是从下自上推的,所以为了满足 二维数组的递推式,我们写内层循环时需要从大到小枚举。

代码:

#include<bits/stdc++.h>
using namespace std;
int t,m;
int dp[1010],t1[1010],m1[110];
int main() {
    scanf("%d%d",&t,&m);
    for(int i=1;i<=m;i++){
        scanf("%d%d",&t1[i],&m1[i]);
    }
    for(int i=1;i<=m;i++)
        for(int j=t;j>=t1[i];j--)
            dp[j]=max(dp[j],dp[j-t1[i]]+m1[i]);
    cout<<dp[t];
    return 0;
}

完全背包

例题:疯狂的采药.

此题当然可以三层循环高时间复杂度枚举 + 滚动数组优化。

当然,这过不了。

要想过此题,需要用 \text O(mt) 的时间复杂度。

我们其实可以将 01背包 的倒序循环改成正序就行了。

因为 01背包 的倒序循环是为了防止重复选择,但是 完全背包 又 允许重复,所以可以这样做。

代码:

#include<bits/stdc++.h>
using namespace std;
long long dp[int(1e7)+10];
int m,t;
long long w[int(1e4)+10],v[int(1e4)+10];
int main() {
    cin>>t>>m;
    for(int i=1;i<=m;i++)
        cin>>w[i]>>v[i]; 
    for(int i=1;i<=m;i++)
        for(int j=w[i];j<=t;j++)
            dp[j]=max(dp[j-w[i]]+v[i],dp[j]);
    cout<<dp[t];
    return 0;
} 

多重背包

例题:宝物筛选

此题的背包有个特点,每个物品虽然可以重复选择,但是每个物品选择的数是有限的。

此题可以将它拆成 01 背包求解。

通过三层循环,列出状态转移方程 dp_{i,j}=\min(dp_{i-1,j},dp_{i-1,j-k \times w_i}+k \times v_i)

时间复杂度 $\text O(v \sum m)$。 当然,这过不了。 因为 $\{2^0,2^1,2^2...,2^n,k(k \le 2^n)\}$ 这个集合的子集之和是可以表示任何数的。 所以我们可以将每个物品的数量拆分成二进制形式进行枚举。 排除了重复的组合,效率更高了。 时间复杂度 $\text O(v \sum \log m)$。 可以通过。 代码: ```cpp #include<bits/stdc++.h> using namespace std; int n,m; int dp[int(4e4)+10]; int v[110],w[110],num[110]; int main() { cin>>n>>m; for(int i=1;i<=n;i++) cin>>v[i]>>w[i]>>num[i]; for(int i=1;i<=n;i++){ int d=num[i]; for(int j=1;j<=d;j<<=1){ for(int k=m;k>=w[i]*j;k--) dp[k]=max(dp[k],dp[k-w[i]*j]+v[i]*j); d-=j; } for(int k=m;k>=w[i]*d;k--) dp[k]=max(dp[k],dp[k-w[i]*d]+v[i]*d); } cout<<dp[m]; return 0; } ``` # 混合背包 没啥意思,就是混合三种背包来求解。 伪代码: ```cpp for(int i=1;i<=n;i++){ if(完全背包){ for(int j=c[i];j<=V;j++) f[j]=max(f[j],f[j-c[i]]+w[i]); } else if(01背包){ for(int j=V;j>=c[i];j--) f[j]=max(f[j],f[j-c[i]]+w[i]); } else{//否则就是多重背包了 for(int j=V;j>=0;j--) for(int k=1;k<=num[i];k++) if(j-k*c[i]>=0) f[j]=max(f[j],f[j-k*c[i]]+k*w[i]); } } ``` # 分组背包 例题:[通天之分组背包](https://www.luogu.com.cn/problem/P1757). 还是用滚动数组。 此题我们可以将每个组变成一个物品,跑 01 背包,怎么计算每个组的贡献呢。 就是每个物品依次更新一下 dp 数组。 代码。 ```cpp #include<bits/stdc++.h> using namespace std; int n,m; int c[1010]; unordered_map<int,int>mp; struct node{ int a,b; }; vector<node>v[1010]; int cnt; int dp[1010]; int main() { cin>>m>>n; for(int i=1;i<=n;i++){ int a,b,g; cin>>a>>b>>g; if(!mp[g]) mp[g]=++cnt,v[cnt].push_back((node){a,b}); else v[mp[g]].push_back((node){a,b}); } for(int i=1;i<=cnt;i++) for(int k=m;k>=0;k--) for(int j=0;j<v[i].size();j++) if(k>=v[i][j].a) dp[k]=max(dp[k],dp[k-v[i][j].a]+v[i][j].b); cout<<dp[m]; return 0; } ``` # 二维费用背包 例题:[NASA的食物计划](https://www.luogu.com.cn/problem/P1507). 通过类比,既然时两维都满足是吧,那么我们在枚举价值的时候就用两层循环。 这样就可以满足两维的费用了。 代码实现很简单。 ```cpp #include<bits/stdc++.h> using namespace std; int n,h,m; int a[110],b[110],c[110]; int dp[510][510]; int main() { cin>>h>>m>>n; for(int i=1;i<=n;i++) cin>>a[i]>>b[i]>>c[i]; for(int i=1;i<=n;i++) for(int j=h;j>=a[i];j--) for(int k=m;k>=b[i];k--) dp[j][k]=max(dp[j][k],dp[j-a[i]][k-b[i]]+c[i]); cout<<dp[h][m]; return 0; } ``` # 有依赖的背包问题