背包问题学习指南
前置芝士
01背包
朴素法
时间复杂度:O(n*m)
[数组定义]
f[i][w]的定义如下:对于前i个物品,当前背包的容量为w,这种情况下可以装的最大价值是f[i][w]。
int w[110],c[110],f[110][110];
int n,m;
int dp(){
for(int i=1;i<=n;i++)
for(int j=1;j<=m;j++){
if(j<w[i])
f[i][j]=f[i-1][j];
else{
f[i][j]=max(f[i-1][j],f[i-1][j-w[i]]+c[i]);
}
}
return f[n][m];
}
状态压缩(滚动数组)
01背包内嵌的循环是从⼤到⼩遍历,为了保证每个物品仅被添加⼀次
int w[110],c[110],f[110];
int n,m;
int dp(){
for(int i=1;i<=n;i++)
for(int j=m;j>=w[i];j--){
f[j]=max(f[j],f[j-w[i]]+c[i]);
}
return f[m];
}
二维01背包
完全背包
完全背包的物品是可以添加多次的,所以可以而且需要从⼩到⼤去遍历
多重背包
朴素算法
时间复杂度:\(O(W\sum_{i=1}^{n}k_{i})\)
将多重背包转化为01背包进行求解,把第i种物品转换成\(s_i\)件01背包中的物品,每件物品的体积为\(k*v_i\),价值为\(k*w_i\)\((0<=k<=s_i)\)
const int N=110;
int v[N],w[N],s[N];//体积,价值,数量
void solve() {
for(int i=1;i<=n;i++){
for(int j=m;j>=v[i];j--){
for(int k=0;k<=s[i]&&k*v[i]<=j;k++){
f[j]=max(f[j],f[j-k*v[i]]+k*w[i]);
}
}
}
}
二进制优化
时间复杂度:\(O(W\sum_{i=1}^n\log_2k_i)\)
将第i种物品拆分成若干件物品,每件物品的体积和价值乘以一个拆分系数\((1,2^1,2^2...,2^{k-1},s_i-2^{k}+1)\),就可以转化成01背包的物品求解了。
const int N=110;
int v[N],w[N],s[N];//体积,价值,数量
int vv[N],ww[N],num=1;
void solve() {
for(int i=1;i<=n;i++){
cin>>v>>w>>s;
for(int j=1;j<=s;j<<=1){
vv[num]=j*v;
ww[num++]=j*w;
s-=j;
}
if(s){
vv[num]=s*v;
ww[num++]=s*w;
}
}
}
单调队列优化
时间复杂度:O(mn) [m:总容量,n为商品种类]
可以通过n=1000,m=1000的数据量
const int N = 110;
const int M = 40010;
int f[M];
int g[M];
int n, m;
int v, w, s;
int q[M*2];
void solve() {
cin >> n >> m;
for (int i = 1; i <= n; i++) {
memcpy(g, f, sizeof(f));//f备份到g中
cin >> w>>v >> s;//价值,体积,数量
for (int j = 0; j < v; j++) {//拆分成v个类
int h = 0, t = -1;
for (int k = j; k <= m; k += v) {//对每个类使用单调队列
if (h <= t && q[h] < k - s * v) h++;//q[h]不在窗口[k-s*v,s-v]内,队头出队
if (h <= t) f[k] = max(g[k], g[q[h]] + (k - q[h]) / v * w);
//(k-q[h])/v:表示还能放入的物品个数
//使用对头最大值跟新f
while (h <= t && g[k] >= g[q[t]] + (k - q[t]) / v * w) t--;
//如果g[k]比用g[q[t]]更新f[x],获得更大的价值,则头出队
//下标入队
q[++t] = k;
}
}
}
cout << f[m] << endl;
}
分组背包
朴素法
物品-体积-决策
int f[110][110],v[110][110],s[110],w[110][110];
int n,V;
int solve(){
for(int i=1;i<=n;i++)
for(int j=1;j<=V;j++)
for(int k=0;k<=s[i];k++){
if(j>=v[i][k])
f[i][j]=max(f[i][j],f[i-1][j-v[i][k]]+w[i][k]);
}
return f[n][V];
}
状态压缩(滚动数组)
物品-体积-决策
int f[110],v[110],w[110];
int s;
int solve(){
for(int i=1;i<=n;i++){
cin>>s;
for(int j=1;j<=s;j++) cin>>v[j]>>w[j];
for(int j=V;j>=1;j--){
for(int k=0;k<=s;k++){
if(j>=v[k])
f[j]=max(f[j],f[j-v[k]]+w[k]);
}
}
}
return f[V];
}
A+B Problem
[problem description]
给定一个正整数n,求将其分解成若干个素数之和的方案总数。
[input]
一行一个正整数 \(n\)。
[output]
一行一个整数表示方案总数。
[sample]
7
3
存在如下三种方案:
- \(7=7\)。
- \(7=2+5\)。
- \(7=2+2+3\)。
[datas]
\(1\le n\le 10^3\)
[solved]
0-1背包
int n;
ll f[1010];
int pr[1010];
int idx;
bool vis[1010];
void solve() {
cin>>n;
for(int i=2;i<=n;i++){
if(!vis[i]){
pr[idx++]=i;
}
for(int j=0;j<idx&&pr[j]*i<=n;j++){
vis[pr[j]*i]=true;
if(i%pr[j]==0) break;
}
}
f[0]=1;
for(int i=0;i<idx;i++){
for(int j=pr[i];j<=n;j++){
f[j]+=f[j-pr[i]];
}
}
cout<<f[n]<<endl;
}
排兵布阵
[problem description]
小 C 正在玩一款排兵布阵的游戏。在游戏中有 n 座城堡,每局对战由两名玩家来争夺这些城堡。每名玩家有m 名士兵,可以向第 i 座城堡派遣 \(a_i\) 名士兵去争夺这个城堡,使得总士兵数不超过 m。
如果一名玩家向第 i 座城堡派遣的士兵数严格大于对手派遣士兵数的两倍,那么这名玩家就占领了这座城堡,获得 i 分。
现在小 C 即将和其他 s 名玩家两两对战,这 s 场对决的派遣士兵方案必须相同。小 C 通过某些途径得知了其他 s 名玩家即将使用的策略,他想知道他应该使用什么策略来最大化自己的总分。
由于答案可能不唯一,你只需要输出小 C 总分的最大值。
[input]
输入第一行包含三个正整数 s,n,m,分别表示除了小 C 以外的玩家人数、城堡数和每名玩家拥有的士兵数。
接下来 s 行,每行 n 个非负整数,表示一名玩家的策略,其中第 i 个数 \(a_i\) 表示这名玩家向第 i 座城堡派遣的士兵数。
[output]
输出一行一个非负整数,表示小 C 获得的最大得分。
[datas]
1≤s≤100
1≤n≤100
1≤m≤20000
对于每名玩家 \(a_i≥0,\)\(∑^{n}_{i=1}a_i≤m\)
[solved]
const int inf = 0x3f3f3f3f;
const ll dinf = 0x7f7f7f7f;
//const int N=100010;
/*
dp[i][j]第i个城堡时,已派出j个士兵
a[i][j]第i个城堡,第j个人出的兵
*/
int s, n, m;
int f[20010], a[110][110], res;
void solve() {
cin >> s >> n >> m;
for (int i = 1; i <= s; i++) {
for (int j = 1; j <= n; j++) {
cin >> a[j][i];
}
}
for (int i = 1; i <= n; i++) {
sort(a[i] + 1, a[i] + 1 + s);
}
for (int i = 1; i <= n; i++)
for (int j = m; j >= 1; j--)
for (int k = 1; k <= s; k++) {
if (j > a[i][k] * 2)
f[j] = max(f[j- a[i][k] * 2 - 1] + k * i, f[j]);
}
cout << f[m] << endl;
}
宝物筛选
[problem description]
终于,破解了千年的难题。小 FF 找到了王室的宝物室,里面堆满了无数价值连城的宝物。
这下小 FF 可发财了,嘎嘎。但是这里的宝物实在是太多了,小 FF 的采集车似乎装不下那么多宝物。看来小 FF 只能含泪舍弃其中的一部分宝物了。
小 FF 对洞穴里的宝物进行了整理,他发现每样宝物都有一件或者多件。他粗略估算了下每样宝物的价值,之后开始了宝物筛选工作:小 FF 有一个最大载重为 \(W\) 的采集车,洞穴里总共有 \(n\) 种宝物,每种宝物的价值为 \(v_i\),重量为 \(w_i\),每种宝物有 \(m_i\) 件。小 FF 希望在采集车不超载的前提下,选择一些宝物装进采集车,使得它们的价值和最大。
[input]
第一行为一个整数 \(n\) 和 \(W\),分别表示宝物种数和采集车的最大载重。
接下来 \(n\) 行每行三个整数 \(v_i,w_i,m_i\)。
[output]
输出仅一个整数,表示在采集车不超载的情况下收集的宝物的最大价值。
[sample]
in
4 20
3 9 3
5 9 1
9 4 2
8 1 3
out
47
[datas]
\(n\leq \sum m_i \leq 10^5\),\(0\le W\leq 4\times 10^4\),\(1\leq n\le 100\)
[solved]
二进制优化多重背包
(1)将一个整数n划分为二进制数,这些二进制数之间可以通过任意求和将[1,n]内的整数表示出来。
const int N=110;
int v,w,s;
int vv[100010],ww[100010],num=0;
int f[40010];
int n,m;
void solve() {
cin>>n>>m;
for(int i=1;i<=n;i++){
cin>>v>>w>>s;
for(int j=1;j<=s;j<<=1){
vv[++num]=j*v;
ww[num]=j*w;
s-=j;
}if(s){
vv[++num]=s*v;
ww[num]=s*w;
}
}
for(int i=1;i<=num;i++){
for(int j=m;j>=ww[i];j--){
f[j]=max(f[j],f[j-ww[i]]+vv[i]);
}
}
cout<<f[m]<<endl;
}