问题来源
杭电OJ-Problem - 5616 (hdu.edu.cn)
题面
Jam 有一个天平和N块砝码。(1≤N≤20);使用天平只能得知两端质量是否相等。砝码可以放置在左右任意一端。判断天平能否测量质量为M的物体。
输入
第一行有一个整数T(1≤T≤5),表示测试用例数。
对于每组测试用例:
第一行为N,表示砝码数。
第二行有N个数,第i个数wi(1≤wi≤100)表示
第三行有一个整数M。M为待测物体质量。
输出
输出"YES"or"NO"。
前提知识
对于本题许多人是作为二进制枚举法的入门学习题(包括本人)。那么还是优先介绍一下二进制枚举法。
二进制枚举法是一种枚举方法(废话),它的枚举方式是通过利用二进制各个位置上的0和1分别表示每个事物的枚举状态,一般用0表示未使用,1表示使用。结合本题作为例子来理解,我们可以发现,可以利用二进制中的0和1分别表示砝码有无使用,0表示未使用,1表示已使用。在有n个砝码的前提下,我们可以有2^n个状态(000..00表示全部未使用 到 111..11表示全部使用),为了方便理解,我们此处采用n=4个砝码,质量分别为1,2,3,4作为具体例子。我们可以用0110表示质量为2和质量为3的砝码被使用,并以此类推。
代码实现
for(int i=0;i<(1<<n);i++)//枚举每种情况
for(int j=0;j<n;j++)//判断i的二进制上每一位上的数字
if(i&(1<<j)){
<代码段>
...
<代码段>
}
/*为了防止有人不理解位运算这里进行解释
1<<n 表示1向左移动n位,右边补0,得到的就是二进制数字
例如 1<<2 = (100)2 = (4)10
而 i & x 就是 i和x进行位的与运算
例如 100 & 110 (此时都是二进制)
1 0 0
& 1 1 0
----------
1 0 0
所以100 & 110 = 100 */
优点:①全面性,二进制枚举法可以生成集合的所有可能情况,确保不会漏掉任何情况。这对于涉及组合、排列或子集的问题非常有用,特别是在需要枚举所有情况以找到最佳解决方案的情况下。②简单实现,二进制枚举法的实现通常比其他复杂的枚举方法更加简单。它只涉及位运算,通常使用循环结构,因此易于理解和编写。③效率, 对于某些问题,二进制枚举法可以在较短的时间内找到解决方案,因为它不需要递归调用或其他复杂的控制结构。
缺点:①时间复杂度高,对于大规模的问题,二进制枚举法的时间复杂度会非常高。当要枚举的元素数量增加时,生成所有可能情况的时间会指数增长,因此在这些情况下,算法可能不够高效。②内存消耗,生成所有可能情况的数组或数据结构可能会占用大量内存,尤其是当元素数量很大时。这可能导致内存消耗过高,甚至导致程序崩溃。③不适用于所有情况: 二进制枚举法适用于特定类型的问题,如集合的组合、排列和子集。对于其他类型的问题,它可能不是最佳选择,因为其效率可能不如其他算法。
二进制枚举法的错误
对于此题其实是不适用二进制枚举法的,其他博主的解析大多都是使用二进制枚举砝码的使用状态来进行解题,初步的思路其实也没错,但问题出在后续的处理之中,其他博主的解题思路总结如下:
思路:二进制枚举出n个秤砣所有能表示出的质量,用vis标记,即可做到 O(1) 的访问!举例:当我们有质量分别为w[3]= {1、5、7}的 n= 3个秤砣时, 砝码可以放置在左右任意一端。 问:能称出哪些 质量 的物体?①可称量出的质量有:1、5、7、6、 8、12、13(即1+5、1+7、5+7、1+5+7)②列出所有情况了吗???③还可称量:4、2、3、11(即5-1、7-5、7+1-5、7+5-1)4种质量!利用砝码之间的差值!④所以枚举完后还需要一个循环减去w[i]。共计:11种。
所以大部分博主全部采用了 枚举完后使用一个循环减去w[i],参考代码如下:
参考来源:【二进制枚举】Jam's balance HDU - 5616-优快云博客
#include<bits/stdc++.h>
using namespace std;
const int N = 1e7;
bool vis[N];
int w[N];
int main()
{
int t;
cin >>t;
while (t--) {
memset(vis,0,sizeof(vis));
int n,m,k;
cin >> n;
for (int i = 0; i < n; ++i)
cin >> w[i];
for (int i = 0; i < (1 << n); ++i) {//二进制枚举
int sum = 0;
for (int j = 0; j < n; ++j) {
if(i & (1 << j))
sum += w[j];
}
vis[sum] = 1;
for (int j = 0; j < n; ++j) {//5-1、7-1、7-5的情况
if(sum - w[j] > 0)
vis[sum-w[j]] = 1;
}
}
cin >> m;
for(int i = 0; i < m; ++i){
cin >> k;
if(vis[k]) puts("YES");
else puts("NO");
}
}
return 0;
}
很显然,此处是使用循环让vis[sum-w[j]] 赋值为1,表明可以称量sum-w[i],但仔细考虑可以发现,这个处理方式只能处理另一个盘中只有一个砝码或没有砝码的情况,解释如下:
如果我们有{1,2,3,4}4个砝码,如果当前sum=2+3+4=9,那么通过这次循环我们可以得到左盘{2,3,4}右盘{1}、左盘{2,3,4}右盘{2}、左盘{2,3,4}右盘{3}、左盘{2,3,4}右盘{4}这几种情况,其中第3、4、5种情况左盘和右盘抵消相同数,也就是不用对应的砝码,第1种就是另一个盘有一个砝码的情况。但对于左盘{1,3} 右盘{2,4}等两个盘都有两个及两个以上的情况都没有做到处理。例如我们的四个砝码的质量分别为{2,3,11,17}得出的结果如下
可以明显发现当左盘为{2,3}右盘为{11,17}时,我们按理来说是可以称量23的,但上面并没有出现23,所以vis[23]仍然是为0的,因此可以证明这个方法是错误的。但代码可以通过杭电OJ,应该是数据锅了不是。要分析为什么可以AC过题,其实也可以理解,大数据量产生的必然运气,我们可以发现题目n极限为20,那么2^20=1,048,576,说明我们最多有1e6个状态,但我们的w[i]都是小于100的,那么也就导致了sum最大也就20*100=2000,但我们的状态是足够多的,完全有机会把0到2000的所有值全部给计算到,所以真的就是运气。
但我们仍然需要明确这个方法还是错误的!
尝试改进
其实应该也没改进的方法了,毕竟代码的时间复杂度也到了O(2^n*n)了,极限情况有10^7了,时间复杂度已经高了,除非采用非常低的时间复杂度的方法来处理砝码中的减的问题才可以解决,但笔者目前还未想到。
所以这到题正确的解法应该只有动态规划了,具体的就不讲解了,可以参考下面这一篇博客hdu 5616 Jam's balance ( 动态规划 )-优快云博客
最后的感慨
本来对这种题目没什么兴趣的,但有幸在班上讲解了这道题目,感觉讲的还行,但在结束的时候有同学提问如果两个盘中都有两个及两个以上的情况该怎么处理,说实话,一下子就问住我了,确实没有考虑到这种情况。课后有一位同学认为二进制枚举的方法是对的,私底下和我交流了一下,认为{1,2,3,4}这种情况下,左{1}右{3,4}就等价于左{1,2}右{2,3,4}的情况,对于两边有相同的砝码时确实可以抵消,所以这样理解是可以的,但对于二者没有重复的呢,例如{1,3}和{2,4}这种,则是无法处理的。
对于自己学了那么久算法,面对这些细节仍然会粗心大意也是挺烦躁的,面对人云亦云的学习环境也是令人讨厌的,但自己也是那种它代码都AC了,思路肯定没错的认同者吧,所以还是要一直保留着初学者的心态去面对后面的学习啊。
就写到这里,与君共勉!