砝码称重问题
砝码问题大体上就是一个问你在背包中选 \(n\) 个物品,体积为 \(j\) 的方案是否存在的问题,求解问题的方式可能稍有不同,但核心都是背包问题求方案是否存在。
\(Easy\)
题意:给定 \(n\) 个砝码,每个砝码的重量为 \(w[i]\),问随意选择 \(k\) 个砝码(\(1\)<=\(k\)<=\(n\)),能得到的不同重量的个数
// 可以转化为背包问题求恰好装满方案数
// 只不过,这个方案数我们只考虑0/1
// f[i]就表示装满体积i的方案数
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int M = 1010;
int w[] = {1, 2, 3, 5, 10, 20};
bool f[M];
int main()
{
f[0] = 1;
for(int i = 0; i < 6; i ++ )
{
int cnt; cin >> cnt;
while(cnt -- )
{
for(int j = M - 1; j >= w[i]; j -- )
f[j] |= f[j - w[i]];
}
}
int res = 0;
for(int i = 0; i < M; i ++ )
res += f[i];
// 不包括重量为0的情况
cout << "Total=" << res - 1 << endl;
return 0;
}
\(Medium\)
给定 \(n\) 个砝码,问将 \(x\) 个放在砝码左侧,\(y\) 个放在砝码右侧,可以称出的重量有多少种(\(1\)<=\(x+y\)<=\(n\))
思路一
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 110, M = 1e5 + 10;
int n, m, w[N];
bool f[2][M]; // 滚动数组优化
int res;
/*
f[i][j]:从前i个物品中选,称出重量为j的方案数(j=0/1)
我们规定:j = abs(left-right)
当前物品不选: f[i][j] |= f[i - 1][j]
当前物品选:
f[i][j] |= f[i - 1][j - w]:放在左侧
f[i][j] |= f[i - 1][j + w]:放在右侧
当放在左侧且j-w<0时,例如j=1,w=2,再不妨设此时左侧为2,右侧为1,j=2-1=1
[2] [1]
-------
|
-------
我们希望此时在左侧放一个重量w=2的砝码之后left-right=j=1,显然不可能啊!
因为显然2+2-1=3,但其实,我们换一种角度想,如果left-right=1
那么我们交换一下左右侧,变为:
[1] [2]
-------
|
-------
此时left-right=-1不久存在了么
因此说,当j-w<0时,由于数组下标不合法,我们可以将j-w改为abs(j-w),它们是等价的
*/
int main()
{
cin >> n;
for(int i = 1; i <= n; i ++ ) cin >> w[i], m += w[i];
f[0 & 1][0] = 1;
for(int i = 1; i <= n; i ++ )
for(int j = 0; j <= m; j ++ )
{
f[i & 1][j] |= f[(i - 1) & 1][j];
f[i & 1][j] |= f[(i - 1) & 1][abs(j - w[i])];
if(j + w[i] <= m) f[i & 1][j] |= f[(i - 1) & 1][j + w[i]];
}
for(int i = 1; i <= m; i ++ ) res += f[n & 1][i];
cout << res << endl;
return 0;
}
思路二
更直观的思路,为了处理负数的问题,添加一个偏移量即可!
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 110, M = 2e5 + 10, F = 1e5;
int n, m, w[N];
bool f[2][M]; // 滚动数组优化
int res;
/*
f[i][j]:从前i个物品中选,称出重量为j的方案数(j=0/1)
j=left-right
但是注意这样设置j的话,有 -m <= j <= m
因为体积为循环的时候是从 -m 到 m 而不是 0 到 m
*/
int main()
{
cin >> n;
for(int i = 1; i <= n; i ++ ) cin >> w[i], m += w[i];
f[0 & 1][0 + F] = 1;
for(int i = 1; i <= n; i ++ )
for(int j = -m; j <= m; j ++ ) // j=-m
{
f[i & 1][j + F] |= f[(i - 1) & 1][j + F];
f[i & 1][j + F] |= f[(i - 1) & 1][j - w[i] + F];
if(j + w[i] <= m) f[i & 1][j + F] |= f[(i - 1) & 1][j + w[i] + F];
}
for(int i = 1; i <= m; i ++ ) res += f[n & 1][i + F];
cout << res << endl;
return 0;
}
\(Hard\)
给你 \(n\) 个砝码,问去掉 \(m\) 个之后,最多能称出多少种不同的重量
/* 思路:先dfs枚举可能的方案再dp求解答案
1. 如何dfs?
(1) 从n个数当中选出n-m个数保留
(2) 从n个数当中删除m个数
通过观察题目范围发现,m<=4,很小
如果我们再对dfs过程进行剪枝
那么方案(2)需要枚举的方案比(1)少得多
因此时间复杂度也就比方案(1)小很多
*/
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 23, M = 2300;
int n, m, w[N];
int sum, res;
bool del[N]; // 某个砝码是否被删除
bool f[M];
void dp()
{
memset(f, 0, sizeof f);
f[0] = true;
for(int i = 0; i < n; i ++ )
{
if(del[i]) continue;
for(int j = sum; j >= w[i]; j -- )
f[j] |= f[j - w[i]];
}
int cur_res = 0;
// 注意不包括0
for(int i = 1; i <= sum; i ++ ) cur_res += f[i];
res = max(res, cur_res);
}
// 由于每次回溯时del[u]都会置为false
// 因此del无需每次都初始化
void dfs(int u, int cnt)
{
// 注意cnt==m的判断应该放在前面
// 否则当u==n时,会直接return漏解
// 因此在dfs中,有解的情况放前面,无解的放后面
if(cnt == m) // 再往下dfs没有意义了,剪枝
{
dp();
return ;
}
if(u == n) return ;
if(n - u + cnt < m) return ; // del的个数不够,可行性剪枝
dfs(u + 1, cnt); // 不删除当前的砝码
del[u] = true; // 删除当前的砝码
dfs(u + 1, cnt + 1);
del[u] = false;
}
int main()
{
cin >> n >> m;
for(int i = 0; i < n; i ++ ) cin >> w[i], sum += w[i];
dfs(0, 0);
cout << res << endl;
return 0;
}