POJ 3977 折半枚举
题目链接:http://poj.org/problem?id=3977
前言
如无法区分折半枚举,二分,请点击这里
分析
这题我感觉出了是用枚举,毕竟数据范围很小,但是,集合中每个元素都有可能被选或者不被选,根据计数原理应该会有\(2^{35}-1\)种情况,需要刨除空集,枚举显然是会T掉,那怎么办呢?
考虑选出来的集合有几种情况,有可能都在前一半,有可能都在后一半,还有可能前后都有,前后都有的情况可以拆分成前两种情况, 所以优先考虑前两种,35的一半可以取17,发现\(2^{17}\)是不会T的,我们只要每次枚举一半,然后就能解决这个问题,这就是如题所说的折半枚举。
使用上述方法可以解决前两种情况,那前后都有呢?前后都有的必然是由前一半的一个子集和后一半的一个子集构成,因为要绝对值最小,所以要使这个集合接近于0,因此枚举前一个集合的每个子集,然后找到一个子集的和与这个子集的相反数最接近的集合,把他们当作答案就行。
这道题来说的话,感觉如果不知道折半枚举这种思想,做起来还是挺困难的 ,有的时候暴力并不是没什么用,毕竟人家的另一个名字叫朴素算法,也许有的时候想不出什么高端数据结构,什么转移方程,不如写个暴力看看,也许优化一下就能A,就算A不了也会有很多过程分吧,毕竟OI不像ACM那样只算AC的题目,也许10分就能拉开很大差距。
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<map>
#define ll long long
const int N=40;
using namespace std;
ll a[N],n;
pair<ll,ll> ans;
map<ll,ll> p;
map<ll,ll>::iterator it;
ll abs(ll x){
return x>0?x:-x;
}
void calc(){
for(ll i=1;i<(1<<n/2);i++){
ll t=i,sum=0,len=0;
for(ll j=n/2-1;j>=0;j--)
if(t&(1<<j))sum+=a[j],len++;
ll tep=abs(sum);
if(tep<ans.first || (tep==ans.first&&len<ans.second)) ans=make_pair(tep,len);
if(p[sum]>0)p[sum]=min(p[sum],len);
else p[sum]=len;
}
for(ll i=1;i<(1<<(n-n/2));i++){
ll t=i,sum=0,len=0;
for(ll j=n-1;j>=n/2;j--){
ll v=j-n/2;
if(t&(1<<v))sum+=a[j],len++;
}
ll tep=abs(sum);
if(tep<ans.first||(tep==ans.first&&len<ans.second))
ans=make_pair(tep,len);
it=p.lower_bound(-sum);
if(it!=p.end()){
ll val=abs((*it).first+sum),l=(*it).second+len;
if(ans.first>val||(ans.first==val&&l<ans.second))
ans=make_pair(val,l);
}
if(it!=p.begin()){
it--;
ll val=abs((*it).first+sum),l=(*it).second+len;
if(ans.first>val||(ans.first==val&&l<ans.second)) ans=make_pair(val,l);
}
}
printf("%lld %lld\n",ans.first,ans.second);
}
int main(){
while(~scanf("%lld",&n)){
if(n==0)return 0;
p.clear();
for(int i=0;i<n;i++)
scanf("%lld",&a[i]);
ans=make_pair(abs(a[0]),1);
calc();
}
}
int - > long long
0 - > 100