- 背包问题
- 1. 算法分析
- 1.1 基础模型
- 1.2 时间复杂度
- 1.3 处理技巧
- 1.3.1 求max/min
- 1.3.2 求count
- 2. 板子
- 2.1 01背包问题
- 2.2 完全背包问题
- 2.3 多重背包
- 2.3.1 朴素版本
- 2.3.2 二进制优化
- 2.4 混合背包
- 2.5 二维费用的背包问题
- 2.6 分组背包问题
- 2.7 有依赖的背包问题
- 2.8 背包问题求方案数
- 2.9 背包问题求具体方案
- 3. 典型例题
- 3.1 01背包
- 3.2 完全背包
- 3.3 分组背包
- 1. 算法分析
背包问题
1. 算法分析
1.1 基础模型
01背包特性的问题是把体积从大到小枚举,完全背包特性的问题是把体积从小到大枚举
1.2 时间复杂度
- 01背包: O(nm)
- 完全背包: O(nm)
- 多重背包:
朴素做法: O(nms)
二制优化: O(nmlogs) - 单调队列优化:
- 混合背包: O(nmlogs)
- 二维费用的背包问题: O(vnm)
- 分组背包: O(nms)
- 有依赖的背包问题: O(nm^2)
- 背包问题求方案数: O(nm)
- 背包问题求具体方案: O(nm)
1.3 处理技巧
背包问题的两种类型
1.3.1 求max/min
- 体积最多是j: 初始化时f全部置为0,且满足for(int j = m; j >= v; --j),即j 必须要 大于等于v才能跑
for(int j = m; j >= v; --j) f[j] = max(f[j], f[j - v] + w);
- 体积恰好是j: 初始化时f[0]=0, f[i]=正(负)无穷,且满足for(int j = m; j >= v; --j),即j 必须要 大于等于v才能跑
for(int j = m; j >= v; --j) f[j] = max(f[j], f[j - v] + w);
- 体积至少是j: 初始化时f[0]=0, f[i]=正(负)无穷,且满足for(int j = m; j >= 0; --j),即j 不需要 大于等于v才能跑
for(int j = m; j >= 0; --j)f[j] = max(f[j], f[max(0, j - v)] + w);
1.3.2 求count
状态转移方程会对应地变成累加,初始化:f[0]=1,f[i]=0
2. 板子
2.1 01背包问题
有 N 件物品和一个容量是 V 的背包。每件物品只能使用一次。第 i 件物品的体积是 vi,价值是 wi。求解将哪些物品装入背包,可使这些物品的总体积不超过背包容量,且总价值最大。输出最大价值。
/* 思考:
1.为什么第二维循环是从大到小遍历?
解答:计算是否放入第i个物品后的状态是要基于是否放入第i-1个物品后的状态来更新,而如果你从小到大遍历,那么是否放入第i个物品后的状态变成基于已经放入或者没放入第i个物品的状态来更新,即一个物品可能被重复放入,从大到小是为了保证这个物品只被放入一次。
而完全背包从小到大遍历,是因为每个物品的个数无限,你放入这个物品后可以继续放入物品,所以放入第i个物品后的状态可以基于已经放入第i个物品的状态来更新。反之,如果你从大到小遍历,那么每个物品只能放入一次,不符合题意。
*/
#include
using namespace std;
int const N = 1e3 + 10;
int n, m;
int f[N]; // f[i]为当体积为i的情况下的最大的价值
int main()
{
cin >> n >> m; // 输入物品的个数和背包的体积
for (int i = 0; i < n; ++i) // 从小到大循环个数
{
int vi, wi; // 物品体积和价值
cin >> vi >> wi; // 读入物品体积和价值
for (int j = m; j >= vi; --j) // 从大到小循环体积
f[j] = max(f[j], f[j - vi] + wi); // 在选和不选之中取最大
}
cout << f[m] << endl;
return 0;
}
2.2 完全背包问题
有 N 种物品和一个容量是 V 的背包,每种物品都有无限件可用。第 i 种物品的体积是 vi,价值是 wi。求解将哪些物品装入背包,可使这些物品的总体积不超过背包容量,且总价值最大。输出最大价值。
#include
using namespace std;
int n, m;
int const N = 1010;
int f[N]; // f[i]表示体积为i情况下的最大价值
int main()
{
cin >> n >> m; // 输入物品的个数和背包的体积
for (int i = 0; i < n; ++i) // 从小到大循环个数
{
int vi, wi; // 物品体积和价值
cin >> vi >> wi; // 读入物品体积和价值
for (int j = vi; j <= m; ++j) // 从小到大循环体积
f[j] = max(f[j], f[j - vi] + wi); // 在选和不选之中取最大
}
cout << f[m] << endl;
return 0;
}
2.3 多重背包
有 N 种物品和一个容量是 V 的背包。第 i 种物品最多有 si 件,每件体积是 vi,价值是 wi。求解将哪些物品装入背包,可使物品体积总和不超过背包容量,且价值总和最大。输出最大价值。
2.3.1 朴素版本
#include
using namespace std;
int const N = 1010;
int n, m;
int f[N]; // f[i]为体积为i的情况下的最大价值
int main()
{
cin >> n >> m; // 物品个数和背包的体积
for (int i = 0; i < n; ++i) // 从小到大枚举物品
{
int v, w, s; // 物品的体积、价值、个数
cin >> v >> w >> s;
for (int j = m; j >= 0; j--) // 从大到小枚举体积,因为物品个数有限,所有这里和01背包类似
for (int k = 1; k <= s && k * v <= j; ++k) // 从1开始枚举每个物品的个数
f[j] = max(f[j], f[j - k * v] + k * w); // 在取k个之间选择最大价值的
}
cout << f[m] << endl; // 输出体积为m情况下的最大价值
return 0;
}
2.3.2 二进制优化
#include
#include
using namespace std;
int n, m;
int const N = 2010;
int f[N]; // f[i]表示体积为i时背包的最大价值
struct Good // 存储新的生成的物品
{
int v, w;
};
int main()
{
// 二进制划分
vector goods;
cin >> n >> m; // 输入物品种类和背包体积
for (int i = 0; i < n; ++i)
{
int v, w, s; // 输入每种物品的体积、价值和个数
cin >> v >> w >> s;
for (int k = 1; k <= s; k *= 2) // 按照2进制的方式把这种物品划分成新的物品
{ // 即1份为一个新的物品,2份为一个新的物品,4份为一个新的物品,8份为一个新的物品
s -= k;
goods.push_back({v * k, w * k});
}
if (s > 0) goods.push_back({v * s, w * s}); // 剩下的一份为一个新的物品
}
// 01背包
for (auto good: goods) // 从小到大枚举物品
for (int j = m; j >= good.v; --j) // 从大到小枚举体积
f[j] = max(f[j], f[j - good.v] + good.w); // 状态转移
cout << f[m] << endl; // 输出体积为m时的最大价值
return 0;
}
2.4 混合背包
有 N 种物品和一个容量是 V 的背包。
物品一共有三类:
- 第一类物品只能用1次(01背包);
- 第二类物品可以用无限次(完全背包);
- 第三类物品最多只能用 si 次(多重背包);
每种体积是 vi,价值是 wi。求解将哪些物品装入背包,可使物品体积总和不超过背包容量,且价值总和最大。输出最大价值。
#include
#include
using namespace std;
int n, m ;
int const maxn = 1010;
int f[maxn];
struct Things
{
int kind;
int v, w;
};
int main()
{
vector things;
cin >> n >> m;
for (int i = 1; i <= n; ++i)
{
int v, w, s;
cin >> v >> w >> s;
if (s < 0) things.push_back({-1, v, w}); // 小于0为01背包
else if (s == 0) things.push_back({0, v, w}); // 等于0为完全背包
else // 大于0,那么该物品就有对应的si个
{
for (int j = 1; j <= s; j *= 2) // 使用二进制优化
{
s -= j;
things.push_back({-1, j * v, j * w});
}
if (s > 0) things.push_back({-1, s * v, s * w});
}
}
for (auto thing: things)
{
if (thing.kind < 0) // 01背包的话,从大到小
{
for (int j = m; j >= thing.v; --j) f[j] = max(f[j], f[j -thing.v] + thing.w);
}
else // 完全背包的话,从小到大
{
for (int j = thing.v; j <= m; ++j) f[j] = max(f[j], f[j - thing.v] + thing.w);
}
}
cout << f[m] << endl;
return 0;
}
2.5 二维费用的背包问题
有 N 件物品和一个容量是 V 的背包,背包能承受的最大重量是 M。每件物品只能用一次。体积是 vi,重量是 mi,价值是 wi。求解将哪些物品装入背包,可使物品总体积不超过背包容量,总重量不超过背包可承受的最大重量,且价值总和最大。输出最大价值。
#include
using namespace std;
int n, v, m;
int const N = 1010;
int f[N][N];
int main()
{
cin >> n >> v >> m; // 输入物品个数和背包的承载体积和承载重量
for (int i = 0; i < n; ++i) // 从小到大枚举物品
{
int vi, mi, wi; // 每个物品的体积、质量、价值
cin >> vi >> mi >> wi;
for (int j = v; j >= vi; --j) // 体积做01背包
for (int k = m; k >= mi; --k) // 重量做01背包
f[j][k] = max(f[j][k], f[j - vi][k - mi] + wi); // 状态转移
}
cout << f[v][m] << endl; // 输出体积最多为v,称重量最大为m时的背包的最大价值
return 0;
}
2.6 分组背包问题
有 N 组物品和一个容量是 V 的背包。每组物品有若干个,同一组内的物品最多只能选一个。每件物品的体积是 vij,价值是 wij,其中 i 是组号,j 是组内编号。求解将哪些物品装入背包,可使物品总体积不超过背包容量,且总价值最大。输出最大价值。
#include
using namespace std;
int const N = 110;
int f[N], v[N], w[N];// f[i]表示体积为i时的最大价值,v存储体积,w存储价值
int n, m;
int main()
{
cin >> n >> m; // 输入分组个数和背包的体积
for (int i = 0; i < n; ++i) // 从小到大枚举分组
{
int s;
cin >> s; // 读入分组内的物品个数
for (int j = 0; j < s; ++j) cin >> v[j] >> w[j]; // 读入分组内的每个物品的体积和价值
for (int j = m; j >= 0; --j) // 从大到小枚举体积(01背包)
for (int k = 0; k < s; ++k) // 枚举每种分组内物品的情况
if (j >= v[k]) // 如果能够放入这个物品
f[j] = max(f[j], f[j - v[k]] + w[k]); // 状态转移
}
cout << f[m] << endl; // 输出体积为m的情况下的最大价值
return 0;
}
2.7 有依赖的背包问题
有 N 个物品和一个容量是 V 的背包。
物品之间具有依赖关系,且依赖关系组成一棵树的形状。如果选择一个物品,则必须选择它的父节点。
如下图所示:
如果选择物品5,则必须选择物品1和2。这是因为2是5的父节点,1是2的父节点。
每件物品的编号是 i,体积是 vi,价值是 wi,依赖的父节点编号是 pi。物品的下标范围是 1…N。求解将哪些物品装入背包,可使物品总体积不超过背包容量,且总价值最大。输出最大价值。
#include
using namespace std;
int const N = 110;
int f[N][N], v[N], w[N]; // f[u][j]表示以u为根节点,体积最大为j的子树的最大价值
int idx, e[N], ne[N], h[N]; // 建立邻接表需要的数据结构
int n, m; // 点数和体积
// 建立邻接表
void add(int a, int b)
{
e[idx] = b, ne[idx] = h[a], h[a] = idx++;
}
// dfs函数进行一次得到以u结点为根的树的各种体积的最大价值
// 即能够得到任何的f[u][j]
void dfs(int u)
{
for (int i = h[u]; i != -1; i = ne[i]) // 遍历与u相连的所有点
{
int son = e[i]; // 得到与u相连的点为son
dfs(son); // 对son进行dfs,得到任意j的f[son][j]
for (int j = m - v[u]; j >= 0; j--) // 根结点u必须选上,所以最大能使用的体积为m-v[u],枚举体积
for (int k = 0; k <= j; ++k) // 枚举以son结点为根,任意体积k的子树,选择其中最大价值的一种
f[u][j] = max(f[u][j], f[u][j - k] + f[son][k]); // 状态转移
}
for (int i = m; i >= v[u]; --i) f[u][i] = f[u][i - v[u]] + w[u]; // 如果体积大于等于v[u]表明能放入根节点,那么给所有这种的点补上之前没有放入的根节点u
for (int i = 0; i < v[u]; ++i) f[u][i] = 0; // 如果体积小于v[u],那么无法放入根节点,那么不可能出现这种情况,则全为0
}
int main()
{
memset(h, -1, sizeof h); // 邻接表初始化
int root; // 标记根
cin >> n >> m; // 读入点数和体积
for (int i = 1; i <= n; ++i)
{
int pre; // 父节点
cin >> v[i] >> w[i] >> pre; // 读入每个物品的体积、价值和父节点
if (pre == -1) root = i; // 如果父节点为-1,那么为根节点
add(pre, i); // 表示和父节点pre相连的点为i
}
dfs(root); // 从根开始dfs,树一般都是从根开始dfs
cout << f[root][m]; // 输出以root为根节点,体积最大为m的最大价值
return 0;
}
2.8 背包问题求方案数
有 N 件物品和一个容量是 V 的背包。每件物品只能使用一次。第 i 件物品的体积是 vi,价值是 wi。求解将哪些物品装入背包,可使这些物品的总体积不超过背包容量,且总价值最大。输出 最优选法的方案数。注意答案可能很大,请输出答案模 109+7 的结果。
/*注意:
要保证背包的体积全部用完,那么f[i]初始化为-INF,这样才能保证每个f[i]都是从f[0]转移而来,保证背包的体积才能全部用完;如果初始化为f[i]全为0,那么背包的体积可能不会全部用完*/
#include
using namespace std;
int const N = 1e3 + 10, mod = 1e9 + 7, INF = -2e9;
int f[N], g[N]; // f[i]表示当体积刚好为i时的最大价值,g[i]记录当体积刚好为j时的方案数
int n, m;
int main()
{
cin >> n >> m; // 输入物品数目和背包体积
g[0] = 1; // 体积为0的方案数为1
for (int i = 1; i <= m; ++i) f[i] = INF; // 初始化时除了f[0]=0,其他都为负无穷,这样保证所有的f[i]表示的为当体积为i时的最大价值,而不是体积最大为i时的最大价值
for (int i = 0; i < n; ++i) // 读入n个物品
{
int vi, wi;
cin >> vi >> wi; // 读入体积和价值
for (int j = m; j >= vi; --j) // 01背包方式从大到小枚举体积
{
int s = 0; // 方案数
int t = max (f[j], f[j - vi] + wi); // 得到较大的那个
if (t == f[j]) s += g[j]; // 如果相等,那么就要加上对应的方案数
if (t == f[j - vi] + wi) s += g[j - vi];
if (s > mod) s = s % mod; // 超出数据要求范围,取模
g[j] = s; // 记录方案数
f[j] = t; // 记录当价值为j时的最大价值
}
}
int maxi = INF; // 记录所有情况的最大价值
for (int i = 0; i <= m; ++i) maxi = max (maxi, f[i]); // 得到最大价值
int res = 0; // 记录方案数
for (int i = 0; i <= m; ++i) // 循环每一种体积下的情况
{
if (maxi == f[i]) // 如果当前体积对应的价值对应最大价值
{
res += g[i]; // 方案数就要加上这种情况
if (res > mod ) res %= mod;
}
}
cout << res << endl;
return 0;
}
2.9 背包问题求具体方案
有 N 件物品和一个容量是 V 的背包。每件物品只能使用一次。第 i 件物品的体积是 vi,价值是 wi。求解将哪些物品装入背包,可使这些物品的总体积不超过背包容量,且总价值最大。输出 字典序最小的方案。这里的字典序是指:所选物品的编号所构成的序列。物品的编号范围是1…N。
// 如果f[i][vol] <- f[i-1][vol-vi]+wi,即f[i][vol]是从f[i-1][vol-vi]+wi转移过来的,那么说明第i个物品选择了
// 如果f[i][vol] <- f[i-1][vol],即f[i][vol]是从f[i-1][vol]转移过来的,那么说明第i个物品没选择
// 如果是从f[i]转移到f[i+1], 那么打印路径的时候要逆着打印,即从f[i+1]往f[i]打印,因为如果还是正着打印,可能会使用到不是最优子结构内的数据
#include
using namespace std;
int const N = 1e3 + 10;
int f[N][N], v[N], w[N]; // f[i][j]表示选择i个物品体积为j的最大价值
int n, m; // 物品数目和体积
int main()
{
cin >> n >> m; // 读入物品数目和背包体积
for (int i = 1; i <= n; ++i) cin >> v[i] >> w[i]; // 读入每个物品的体积和价值
for (int i = n; i >= 1; --i) // 要求字典序最小,所以从大到小枚举,这样打印路径才能从小到大
for (int j = m; j >= 0; --j) // 二维的从大到小和从小到大都可以
{
f[i][j] = f[i + 1][j]; // f[i][j]必须有值且不能为0,所以默认为不选的情况
if (j >= v[i]) f[i][j] = max (f[i][j], f[i + 1][j - v[i]] + w[i]); // 如果可以选,那么比较两个情况,取最大的价值情况
}
int vol = m; // 初始体积为m
for (int i = 1; i <= n; ++i) // 从小到大枚举,打印路径
{
if (vol >= v[i] && f[i][vol] == f[i + 1][vol - v[i]] + w[i]) // 如果当前的剩余体积vol大于当前物品v[i],且f[i][vol]这个状态是从f[i+1][vol-v[i]+w[i]这个状态转移过来
{ // 那么说明第i个物品选择了
cout << i << " ";
vol -= v[i];
}
}
return 0;
}
3. 典型例题
3.1 01背包
acwing1020 潜水员
题意: 潜水员为了潜水要使用特殊的装备。他有一个带2种气体的气缸:一个为氧气,一个为氮气。让潜水员下潜的深度需要各种数量的氧和氮。潜水员有一定数量的气缸。每个气缸都有重量和气体容量。潜水员为了完成他的工作需要特定数量的氧和氮。他完成工作所需气缸的总重的最低限度的是多少?例如:潜水员有5个气缸。每行三个数字为:氧,氮的(升)量和气缸的重量:
3 36 120
10 25 129
5 50 250
1 45 130
4 20 119
如果潜水员需要5升的氧和60升的氮则总重最小为249(1,2或者4,5号气缸)。你的任务就是计算潜水员为了完成他的工作需要的气缸的重量的最低值。
题解: 二维的01背包,然后是最低值,初始化时需要f[0]=0,其他为0x3f,转移时也需要注意。
代码:
#include
using namespace std;
const int N = 22, M = 80;
int n, m, K;
int f[N][M];
int main()
{
cin >> n >> m >> K;
memset(f, 0x3f, sizeof f);
f[0][0] = 0;
while (K -- )
{
int v1, v2, w;
cin >> v1 >> v2 >> w;
for (int i = n; i >= 0; i -- )
for (int j = m; j >= 0; j -- )
f[i][j] = min(f[i][j], f[max(0, i - v1)][max(0, j - v2)] + w);
}
cout << f[n][m] << endl;
return 0;
}
acwing278 数字组合
题意: 给定N个正整数A1,A2,…,AN,从中选出若干个数,使它们的和为M,求有多少种选择方案
题解:
f[0][0] = 1, f[0][i] = 0
f[i][j] = f[i - 1][j] + f[i - 1][j - vi]
=>
f[0] = 1;
f[i] = f[i] + f[i - vi]
代码:
#include
using namespace std;
int const N = 1e4 + 10;
int f[N];
int n, m;
int main()
{
cin >> n >> m;
f[0] = 1;
for (int i = 0; i < n; ++i)
{
int v;
cin >> v;
for (int j = m; j >= v; --j)
f[j] += f[j - v];
}
cout << f[m] << endl;
return 0;
}
acwing734能量石
题意: 岩石怪物杜达生活在魔法森林中,他在午餐时收集了N块能量石准备开吃。
由于他的嘴很小,所以一次只能吃一块能量石。能量石很硬,吃完需要花不少时间。
吃完第 i 块能量石需要花费的时间为Si秒。
杜达靠吃能量石来获取能量。不同的能量石包含的能量可能不同。此外,能量石会随着时间流逝逐渐失去能量。第 i 块能量石最初包含Ei单位的能量,并且每秒将失去Li单位的能量。当杜达开始吃一块能量石时,他就会立即获得该能量石所含的全部能量(无论实际吃完该石头需要多少时间)。能量石中包含的能量最多降低至0。请问杜达吃完所有石头可以获得的最大能量是多少?
题解:
先i再i+1:Ans1 = Ei + Ei+1 - Si * Li+1
先i+1再i: Ans2 = Ei + Ei+1 - Si+1 * Li
因此,需要Ans1 > Ans2,则:Si * Li+1 < Si+1 * Li
即:Li/Si > Li+1/Si+1
那么需要按照Li/Si从大到小排序才能保证能量最大(为了防止精度损失,可以使用乘法代替除法)
然后01背包即可(本题的f[j]表示为恰好的情况,因为如果表示为最多的情况的话,每个时刻能量石都在损失能量)
代码:
#include
using namespace std;
int const N = 1e6 + 10, M = 110;;
int f[N];
int n, m, t;
struct Stone
{
int e, s, l;
}stone[M];
int kase;
bool cmp(struct Stone x, struct Stone y)
{
return x.l * y.s > y.l * x.s;
}
int main()
{
cin >> t;
while (t--)
{
scanf("%d", &n);
// 初始化
m = 0;
memset(f, 0xcf, sizeof f);
f[0] = 0;
memset(f, 0, sizeof f);
memset(stone, 0, sizeof stone);
// 读入排序
for (int i = 1; i <= n; ++i)
{
scanf("%d %d %d", &stone[i].s, &stone[i].e, &stone[i].l);
m += stone[i].s;
}
sort(stone + 1, stone + 1 + n, cmp);
// 01背包
for (int i = 1; i <= n; ++i)
{
for (int j = m; j >= stone[i].s; --j)
{
f[j] = max(f[j], f[j - stone[i].s] + max(0, stone[i].e - stone[i].l * (j - stone[i].s)));
}
}
int res = 0;
for (int i = 0; i <= m; ++i) res = max(res, f[i]);
printf("Case #%d: %d\n", ++kase, res);
}
return 0;
}
3.2 完全背包
acwing1023买书
题意: 小明手里有n元钱全部用来买书,书的价格为10元,20元,50元,100元。问小明有多少种买书方案?(每种书可购买多本)
题解:
f[i][j] = f[i - 1][j] + f[i - 1][j - v] + f[i - 1][j - 2v] + ... + f[i - 1][tv]
而f[i][j - v] = f[i -1][j - v] + f[i - 1][j - 2v] + ... + f[i - 1][j - tv]
所以
=>
f[i][j] = f[i - 1][j] + f[i][j - v] (这样子就把三维优化为二维了)
=>
f[j] = f[j - v] (这样子就把二维优化为一维了)
求方案,只有f[0]是有效的,其他的都是无效的,无效的如果是加的类型,那就初始化为0
代码:
#include
using namespace std;
int v[] = {0, 10, 20, 50, 100};
int n, m;
int const N = 1e3 + 10;
int f[N];
int main()
{
cin >> m;
memset(f, 0, sizeof f);
f[0] = 1;
for (int i = 1; i <= 4; ++i)
{
for (int j = 0; j <= m; ++j)
if (j >= v[i]) f[j] += f[j - v[i]];
}
cout << f[m] << endl;
return 0;
}
acwing532 货币系统
题意: 在网友的国度中共有 n 种不同面额的货币,第 i 种货币的面额为 a[i],你可以假设每一种货币都有无穷多张。为了方便,我们把货币种数为 n、面额数组为 a[1..n] 的货币系统记作 (n,a)。 在一个完善的货币系统中,每一个非负整数的金额 x 都应该可以被表示出,即对每一个非负整数 x,都存在 n 个非负整数 t[i] 满足 a[i]× t[i] 的和为 x。然而,在网友的国度中,货币系统可能是不完善的,即可能存在金额 x 不能被该货币系统表示出。
例如在货币系统 n=3, a=[2,5,9] 中,金额 1,3 就无法被表示出来。
两个货币系统 (n,a) 和 (m,b) 是等价的,当且仅当对于任意非负整数 x,它要么均可以被两个货币系统表出,要么不能被其中任何一个表出。
现在网友们打算简化一下货币系统。
他们希望找到一个货币系统 (m,b),满足 (m,b) 与原来的货币系统 (n,a) 等价,且 m 尽可能的小。
他们希望你来协助完成这个艰巨的任务:找到最小的 m。
1≤n≤100,
1≤a[i]≤25000,
1≤T≤20
题解:
本题就是要求任何一个数字是否能够被其他数字表示,如果能够被其他数字表示,那么这个数字就不选。因此可以先把所有数字按照从大到小排序,然后利用完全背包判断每个数字是否可以被其他数字表示,即判断通过前1~i-1个数字是否能够表示第i个数字,如果到达第i个数字的f[i]==0,那么说明第i个数字不能被其他数字表示。求所有a[i]对应的f[a[i]]时,可以整体求一遍到f[a[n]],这样在求出最大f[t]的同时,所有的f[i]都能求出来
代码:
#include
using namespace std;
typedef long long LL;
int t, n, m;
int const N = 25010;
LL f[N];
int a[N];
int main()
{
cin >> t;
while (t--)
{
// 输入、排序
scanf("%d", &n);
for (int i = 1; i <= n; ++i) scanf("%d", &a[i]);
sort(a + 1, a + n + 1);
// 求f[i]
int ans = 0;
int m = a[n];
memset(f, 0, sizeof f);
f[0] = 1;
for (int i = 1; i <= n; ++i)
{
if (!f[a[i]]) ans++;
for (int j = a[i]; j <= m; ++j)
f[j] += f[j - a[i]];
}
printf("%d\n", ans);
}
return 0;
}
3.3 分组背包
acwing487金明的预算方案
题意: 金明今天很开心,家里购置的新房就要领钥匙了,新房里有一间金明自己专用的很宽敞的房间。更让他高兴的是,妈妈昨天对他说:“你的房间需要购买哪些物品,怎么布置,你说了算,只要不超过N元钱就行”。今天一早,金明就开始做预算了,他把想买的物品分为两类:主件与附件,附件是从属于某个主件的,下表就是一些主件与附件的例子:
如果要买归类为附件的物品,必须先买该附件所属的主件。每个主件可以有0个、1个或2个附件。附件不再有从属于自己的附件。
金明想买的东西很多,肯定会超过妈妈限定的N元。于是,他把每件物品规定了一个重要度,分为5等:用整数1~5表示,第5等最重要。他还从因特网上查到了每件物品的价格(都是10元的整数倍)。他希望在不超过N元(可以等于N元)的前提下,使每件物品的价格与重要度的乘积的总和最大。设第j件物品的价格为v[j],重要度为w[j],共选中了k件物品,编号依次为j1,j2,…,jk,则所求的总和为:v[j1]∗w[j1]+v[j2]∗w[j2]+…+v[jk]∗w[jk](其中*为乘号)
请你帮助金明设计一个满足要求的购物单。
题解:
本题属于分组背包模型
每个物品如果有主件,那么和在主键算。对于每个分组,一共有2^i种,i为附件的个数
在枚举每种附件选配时可以使用二进制来做分组背包
代码:
#include
using namespace std;
typedef pair PII;
int const N = 3e4 + 50;
int n, m;
PII master[N];
vector ser[N];
int f[N];
int main()
{
cin >> m >> n;
for (int i = 1; i <= n; ++i)
{
int v, priority, idx;
cin >> v >> priority >> idx;
if (!idx) master[i] = {v, priority * v};
else ser[idx].push_back({v, v * priority});
}
for (int i = 1; i <= n; ++i)
if (master[i].first)
{
for (int j = m; j >= 0; --j)
{
// 二进制选择配件使用
for (int k = 0; k < 1 << ser[i].size(); ++k)
{
int v = master[i].first, w = master[i].second;
for (int p = 0; p < ser[i].size(); ++p)
{
if (k >> p & 1)
{
v += ser[i][p].first;
w += ser[i][p].second;
}
}
if (j >= v) f[j] = max(f[j], f[j - v] + w);
}
}
}
cout << f[m] << endl;
return 0;
}
acwing1013机器分配
题意: 总公司拥有M台相同的高效设备,准备分给下属的N个分公司。各分公司若获得这些设备,可以为国家提供一定的盈利。盈利与分配的设备数量有关。问:如何分配这M台设备才能使国家得到的盈利最大?求出最大盈利值。分配原则:每个公司有权获得任意数目的设备,但总台数不超过设备数M。
1≤N≤10,1≤M≤15
题解: 分组背包变形题,分组为每个公司获得多少台设备。由于M非常小,因此可以分组背包枚举。
代码:
#include
using namespace std;
const int N = 11, M = 16;
int n, m;
int w[N][M];
int f[N][M];
int way[N];
int main()
{
cin >> n >> m;
for (int i = 1; i <= n; i ++ )
for (int j = 1; j <= m; j ++ )
cin >> w[i][j];
for (int i = 1; i <= n; i ++ )
for (int j = 0; j <= m; j ++ )
for (int k = 0; k <= j; k ++ )
f[i][j] = max(f[i][j], f[i - 1][j - k] + w[i][k]);
cout << f[n][m] << endl;
int j = m;
for (int i = n; i; i -- )
for (int k = 0; k <= j; k ++ )
if (f[i][j] == f[i - 1][j - k] + w[i][k])
{
way[i] = k;
j -= k;
break;
}
for (int i = 1; i <= n; i ++ ) cout << i << ' ' << way[i] << endl;
return 0;
}