Subset POJ - 3977 (折半枚举+lower_bound)

Description

Given a list of N integers with absolute values no larger than 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

大致题意:给你n个数,让你找出一个子集,使得子集中的所有数的和的绝对值最小,如果有多种情况,选择子集中数量最少的情况输出。注意子集不能为空。

思路:对于n个数如果直接去暴力枚举的话时间复杂度为2^n,(n<=35)这样的时间复杂度是无法接受的,所以我们考虑折半枚举,将这n个数分成两半部分,分别去暴力枚举求出所有可能的和,然后通过二分查找(或者方便点写用lower_bound)枚举左边部分的值快速找到对应的右边部分的最优值。那么时间复杂度差不多就降到了(2^(n/2)+2^(n/2)+n/2*log(n/2))

注意:
1.对于longlong型的整数不能直接用math中的abs(),需要自己再手写一个,也就几行的代码。
2.左半部分可能一个都不取,或者右半部分一个都不取,此时另一半必须取。
3.注意lower_bound的用法,考虑寻找的最优解val的值大于所有map中的值。

开始我想的是用两个map来分别存两部分暴力枚举出来后的值,然后再选择其中一个进行枚举,利用lower_bound求出另一个map中的最优解。虽然a掉了但是时间和空间复杂度都令人发指
这里写图片描述
后来又想了下,其实用一个map来存其左半部分暴力枚举出来的值就可以了,然后右半部分在暴力枚举的时候就可以直接去找对应的左半部分的最优解。
这里写图片描述
第一个代码写的太搓了就不贴出来了。。。

优化后的代码如下

#include <iostream>  
#include <cstdio>  
#include <cstring>  
#include <cmath>  
#include <algorithm>
#include <vector>
#include <map>
using namespace std;  
#define ll long long int 
map<ll,int> mp1;//mp1[i]=j表示和为i的子集中元素最少为j
ll a[40];
map<ll,int>::iterator it1;
ll ans=1e18+5;
int shu=40;
ll ABS(ll x)
{
    if(x<0)
    return -x;
    return x;
}
int main()  
{  
    int n;
    while(scanf("%d",&n)&&n)
    {
        ans=1e18+5;
        shu=40;
        for(int i=0;i<n;i++)
        scanf("%lld",&a[i]);

        if(n==1)//如果只有一个的话就直接选了
        {
            printf("%lld 1\n",ABS(a[0]));
            continue;
        }

        mp1.clear();
        for(int i=1;i<(1<<(n/2));i++)//用二进制进行状压,第j个位上0表示不选,1表示选。状态数从1开始表示至少选一个数
        {
            ll sum=0;
            int num=0;
            for(int j=0;j<n/2;j++)//左半部分0到n/2-1
            {
                if((i>>j)&1)//如果第j位是1,表示选择
                {
                    sum+=a[j];
                    num++;
                }
            }

            if(mp1[sum]==0)
            mp1[sum]=num;
            else 
            {
                if(mp1[sum]>num)
                mp1[sum]=num;
            }

            if(ans>ABS(sum))//假如右半部分一个都不选,此时的结果即为sum
            {
                ans=ABS(sum);
                shu=num;
            }
            else if(ans==ABS(sum)&&shu>num)
            shu=num;
         }
         mp1[0]=0;//如果左半部一个都不选

         for(int i=1;i<(1<<(n-n/2));i++)//右半部分的状态,原理同上
         {
            ll sum=0;
            int num=0;
            for(int j=0;j<n-n/2;j++)
            {
                if((i>>j)&1)
                {
                    sum+=a[j+n/2];
                    num++;
                 }
             }

             if(sum==0)//如果此时和为0,那么最优解即左半部分一个都不选
             {
                if(ans>0)
                {
                    ans=0;
                    shu=num;
                 }
                 else if(ans==0&&shu>num)
                 shu=num;
                 continue;
             }

            it1=mp1.lower_bound(-sum);//寻找左半部分的最优解
            if(it1==mp1.end())//如果最优解大于mp1中的所有值,此时最优选mp1中的最后一个
            {
                it1--;
                if(ans>ABS(sum+it1->first))
                {
                    ans=ABS(sum+it1->first);
                    shu=num+it1->second;
                }
                else if(ans==ABS(sum+it1->first)&&shu>(num+it1->second))
                shu=num+it1->second;

                continue;
            }
            if(it1!=mp1.begin())//否则最优可能是当前位置,或者是前面一个位置,判断前面一个位置是否存在
            {
                it1--;
                if(ans>ABS(sum+it1->first))
                {
                    ans=ABS(sum+it1->first);
                    shu=num+it1->second;
                }
                else if(ans==ABS(sum+it1->first)&&shu>(num+it1->second))
                shu=num+it1->second;
                it1++;
            }
            if(ans>ABS(sum+it1->first))
            {
                ans=ABS(sum+it1->first);
                shu=num+it1->second;
            }
            else if(ans==ABS(sum+it1->first)&&shu>(num+it1->second))
            shu=num+it1->second;
         }       
        printf("%lld %d\n",ans,shu);
    }
    return 0;  
}  
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值