Subset POJ - 3977(折半枚举+二分+二进制枚举)

题意:

给你一个集合N(N<=35),问集合的子集,除了空集,使得自己中所有元素和的绝对值最小,若存在多个值,那么选择子集中元素最少的那个。

题目:

Given a list of N integers with absolute values no larger than 1 0 15 10^{15} 1015, 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 1015 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

分析:

1.n 最大到 35,每个数有选、不选两种可能,最多有 2 35 2^{35} 235 个子集,因此暴力枚举的话,一定会Time Limit Exceed,采用折半枚举的思想,分成两个集合,这样每边最多 18 个元素,分别进行枚举,复杂度降到 2 18 2^{18} 218
2.利用二进制将和以及元素个数存在两个数组中,先预判是否满足题意,再将其中一个元素和取相反数后排序,因为总元素和越接近零越好,再二分查找即可,用lower_bound时考虑查找到的下标和他前一个下标,比较元素和以及元素个数,不断更新即可。
3.由于是求大数的绝对值,此时需要开函数,(不知道为什么,我调用labs函数,结果是错误的,所以自己写了一个)
4.然后枚举其中一个子集,排序后暂存后,再枚举另一个子集,通过二分查找第一个集合中与该值的相反数最接近的元素,要注意的是如果有多个元素与相反值最接近,取数的个数最小的那一个。
5.与寻找合适的子集并与第一个集合的子集相加,从而找到绝对值最小的子集.
下面附上AC代码,里面注解有部分解答。

AC代码:

#include<stdio.h>
#include<map>
#include<string.h>
#include<math.h>
#include<algorithm>
#include<iostream>
#define inf 0x3f3f3f3f3f3f3f3f
using namespace std;
typedef long long ll;
const int M=50;
int n,cnt=0;
map<ll,ll> vis;
ll Labs(ll x){
    if(x>0) return x;
    return -x;
}
ll a[M],dp[1<<20],ans;//vis当前所选的最少个数,dp数组存储前一半的值,ans是最终结果
ll solve(ll num)
{
    if(num<=dp[1])
        return dp[1];
    else if(num>=dp[cnt])
        return dp[cnt];
    int mid=upper_bound(dp+1,dp+cnt+1,num)-dp;
    if(Labs(dp[mid]-num)==Labs(dp[mid-1]-num)){
        if(vis[dp[mid-1]]<vis[dp[mid]])
            return dp[mid-1];
        return dp[mid];
    }
    else if(Labs(dp[mid]-num)>Labs(dp[mid-1]-num))
        return dp[mid-1];
    return dp[mid];
}//查找与x最接近的元素,要注意的是如果有多个元素与x最接近,取数的个数最小的那一个
int main()
{
    while(~scanf("%d",&n)&&n)
    {
        cnt=0,ans=inf;
        vis.clear();
        memset(a,0,sizeof(a));
        memset(dp,0,sizeof(dp));
        for(int i=1; i<=n; i++)
            scanf("%lld",&a[i]);
        if(n==1){
            printf("%lld 1\n",Labs(a[1]));
            continue;
        }
        int x=n/2,y=n-x;
        int mi=(1<<x)-1;/*mi为所选元素的所有可能性,除去空集*/
        ll id=inf;
        for(int i=1; i<=mi; i++){
            ll now=0,tot=0;
            for(int j=1; j<=x; j++){
                if(i&(1<<(j-1)))/**枚举i为多少种方式,j为多少个数,用j来控制位数,i控制方式*/
                    now+=a[j],tot++;
            }
            if(vis[now])
                vis[now]=min(vis[now],tot);//如果当前值已经出现过,元素个数取较小的
            else
                vis[now]=tot,dp[++cnt]=now;//没有出现过,建立映射关系
            if(Labs(now)<ans){
                ans=Labs(now);
                id=tot;
            }//如果答案更优,更新答案
            else if(Labs(now)==ans)
                id=min(id,tot);//如果答案相同,元素个数取较小的
        }
        sort(dp+1,dp+cnt+1);
        for(int i=1; i<=(1<<y)-1; i++){
            ll now=0,tot=0;
            for(int j=1; j<=y; j++)
                if(i&(1<<(j-1)))
                    now+=a[j+n/2],tot++;
            if(Labs(now)<ans){
                ans=Labs(now);
                id=tot;
            }
            else if(Labs(now)==ans)
                id=min(id,tot);
            ll num=solve(-now);//二分找到与相反数最接近的数
            if(ans>Labs(num+now)){
                ans=Labs(num+now);
                id=tot+vis[num];
            }
            else if(ans==Labs(num+now))
                id=min(id,tot+vis[num]);
        }
        printf("%lld %d\n",ans,id);
    }
    return 0;
}

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值