折半枚举--poj3977

Language:
Subset
Time Limit: 30000MS Memory Limit: 65536K
Total Submissions: 1136 Accepted: 190

Description

Given a list of N integers with absolute values no larger than 10 15, find a non empty subset of these numbers which minimizes the absolute value of the sum of its elements. In case there are multiple subsets, choose the one with fewer elements.

Input

The input contains multiple data sets, the first line of each data set contains N <= 35, the number of elements, the next line contains N numbers no larger than 10 15  in absolute value and separated by a single space. The input is terminated with N = 0

Output

For each data set in the input print two integers, the minimum absolute sum and the number of elements in the optimal subset.

Sample Input

1
10
3
20 100 -100
0

Sample Output

10 1
0 2


     
     
解题思路:
这是一道明显的NP问题,方法:枚举,但是问题是怎么枚举,N <= 35 ,如果把所有的情况枚举完那么
复杂度就是 2 ^ 35 这个可以自己证明 : (1+x) ^ n = C(n,0) * x ^ 0  + C(n,1) * x ^ 1 + C(n,2) * x ^ 2 
+ ... + C(n,n) * x ^ n , 令x = 1,显然这个复杂度太大了不可取,那么又怎么枚举呢,其实这里可以采用二分
的思想,想象一下我们把这35个元素当成一个集合,然后将这个集合尽量分成均等的两份(这里之所以尽量分成相等
的两份是因为只有分成尽量相等的两份这样复杂度才是最小这个接下来就会讲到),然后问题变成由原来的求一个
集合中选若干个集合使几何元素绝对值之和最小,转变成求两个集合中的若干个元素组成的集合绝对值最小,这里
不妨设这两个集合分别为 A 和 B 那么我们将会得到这样的一个式子:
                                  ——   A 中选若干个元素
                                 |
原集合中选若干个元素构成的子集 = |            +           
                                 |
                                  ——   B 中选若干个元素
假设A中有numA 个元素,那么A的预选方案就有 2 ^ numA , 解释:对于A中的每个元素无外乎两种情况,选或者不选,
如果选的话我们就用1来表示反之用0来表示,我们就得到了若干个0,1组城的一串数字,如:
假设A中有5个元素,那么我们有这几种表示方法:
00000 (全不选)   11111 (全选)   10010 (部分选,部分不选) ...
如果,选了某一个元素就加上相应的元素的数值,然后 另设一个变量num记录一共选了多少个元素
那么如果A集合一共有numA个元素,那么他的所有可能的情况就是:
for(i = 0;i < ( 1 << numA );i++){
  ...
   for(j = 0;j < numA;j++)
     if( i & ( 1 << j )){
   dr[i].sum += ar[j];
dr[i].num++;
}
  ...
}
这样就用 2 ^ (n / 2) 的复杂度完成了集合A的枚举
但是dr中的有些元素的sum是一样的,这样我们只需要选num较小的即可,但是当两个元素的sum = 0 且 较小的
一个元素的num = 0时,这时候就要选上num 较大的元素,但是并不存在两个sum 和 num同时等于0 的情况,原因:
sum 和 num 同时为0 表示所有元素都不选,这样的情况就只有一种。
#include<iostream>
#include<cstdio>
#include<climits>
#include<cmath>
#include<algorithm>
using namespace std;
typedef long long LL;
#define INF 1000000000000000LL
struct node
{
    LL sum;
    LL num;
} set[2050000],s[2050000];
int N;
LL k;
long long a[50];
long long aabs(LL sum)
{
    return sum>0?sum:(-sum);
}
bool cmp(node d,node b)
{
    if(d.sum==b.sum)
        return d.num<b.num;
    return  d.sum<b.sum;
}
int search(LL sum)
{
    int l=0,r=k-1,mid;
    while(r>=l)
    {
        mid=(r+l)/2;
        if(s[mid].sum==sum)
            return mid;
        else if(s[mid].sum>sum)
            r=mid-1;
        else
            l=mid+1;
    }
    return r+1;
}
void solve()
{
    int len=N/2;
    for(int i=0; i<(1<<len); i++)
    {
        set[i].num=set[i].sum=0;
        for(int j=0; j<len; j++)
        {
            if((i&(1<<j))>0)
            {
                set[i].sum+=a[j];
                set[i].num++;
            }
        }
    }
    sort(set,set+(1<<len),cmp);
    k=0;
    s[k++]=set[0];
    for(int i=1; i<(1<<len); i++)
        if(set[i].sum>s[k-1].sum)
            s[k++]=set[i];
        else if(set[i].sum==0&&s[k-1].sum==0&&s[k-1].num==0)
            s[k++]=set[i];
    int len2=N-len;
    LL maxn=INF,maxi=INF;
    for(int i=0; i<(1<<len2); i++)
    {
        LL sum=0,cnt=0;
        for(int j=0; j<len2; j++)
        {
            if((i&(1<<j))>0)
            {
                sum+=a[len+j];
                cnt++;
            }
        }
        LL nn=search(-sum);
        for(LL j=(nn-3>=0?nn-3:0); j<(nn+3<k?nn+2:k); j++)
        {
            if((cnt+s[j].num)!=0)
                if(aabs(sum+s[j].sum)<maxn)
                {
                    maxn=aabs(sum+s[j].sum);
                    maxi=cnt+s[j].num;
                }
                else if(aabs(sum+s[j].sum)==maxn&&cnt+s[j].num<maxi)
                    maxi=cnt+s[j].num;
        }
    }
    cout<<maxn<<' '<<maxi<<endl;
}
int main()
{
    while(scanf("%d",&N)!=EOF)
    {
        if(!N)
        break;
        for(int i=0; i<N; i++)
            scanf("%lld",&a[i]);
        solve();
    }
    return 0;
}


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值