• [POJ3977] Subet(二分枚举)


    解题报告

    前置知识:折半查找法(二分法)

    顾名思义,折半就是把一组数据(有序)分成两半,判断我们要找的key值在哪一半当中,不断重复该操作直至找到目标key值,这玩意说白了就是二分的另一个名字。

    解决

    一说二分大家都知道,但问题在于我们怎么往二分那里去想。首先打眼一看这道题不像一般的思维题那样有明显的规律,事实上也的确没有,那么我们初步的思路就只能是暴力枚举。

    但单纯的暴力肯定是GG的,N<=35就意味着我们复杂度最多会达到2^35 这个级别,显然不现实。那么怎么减少枚举的次数呢,我们肯定会想到二分?那么为什么二分是可行的呢,我们如果把35个元素看作一个集合,然后把这个集合分成尽量均等的两份(为使复杂度尽量小,下面会说到),所以问题转化为从一个集合中找到若干集合(元素)使几个元素的绝对值最小,转变成求两个集合中的若干集合(元素)所组成的集合的绝对值最小。这里我们设这两个集合为A和B,简单表示一下就是:

    • | 原集合中选若干个元素组成的子集 | = | A中选若干个元素 | + | B中选若干个元素 |

    如果A中有numA个元素,那么A中的预选方案就有2^numA 种,这里可以用状态压缩的思想来解释。对于每一个元素只有选与不选两种状态,我们用一个二进制串来表示,1代表选0代表不选,那么总状态很显然有2^numA 种,B也是是一样的。

    这样我们就可以对区间进行枚举了,使用整数表示集合,将所有可能的和与相对应的元素个数存入map,同时每遍历一个组合,就比较其是否比当前结果更优,如果是则更新结果。

    然后对第二个区间进行枚举,每枚举出一种组合的和sum,比较更新结果。然后使用map内置的lower_bound函数,在第一个区间里找到比−sum大的最小元素,判断该元素与sum相加是否能构成更好的解。此时不能忘了,我们还要看比−sum小的最大元素,同样判断这种情况是否能构成更好的解。

    我们是否需要对前半个区间取空集或者后半个区间取空集进行特判呢?好像没有关注过的样子?答案是不需要管它。因为我们无论是在前后区间进行枚举的时候,一旦找到一个sum值,就会判断它的绝对值是否比当前最优解更小,如果此时更新了结果值,也就是说我们只在单个区间里取了这些个元素,另一个区间根本没有枚举。已经考虑了空的情况。

    实现

    自己的代码有点小问题。。。先放上一个正解的,自己的等考完了再改改。。。(记得每次要清空map)

    #include<iostream>
    #include<iomanip>
    #include<vector>
    #include<algorithm>
    #include<map>
    #include<queue>
    #include<string.h>
    #include<math.h>
    using namespace std;
    
    #define ll long long
    #define inf 1e9
    #define MAX 100000
    #define pair pair<ll,ll>
    #define abs(x) ((x)>=0?(x):-(x))//手写的取绝对值函数,虽然不知道为什么但必须手写
    
    ll n, a[40];
    pair ans;
    map<ll, ll > p;//value->len
    map<ll, ll>::iterator it;//迭代器,都多久没用过了...
    
    void solve() {
    	//折半枚举
    	for (ll i = 1; i < (1 << (n / 2)); i++) {//枚举前(n/2)位
    		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 tmp = abs(sum);
    		if (tmp < ans.first || (tmp == ans.first&&len < ans.second))
    			ans = make_pair(tmp, len);
    		p[sum] > 0 ? p[sum] = min(p[sum], len) : p[sum] = len;
    	}
    	for (ll i = 1; i < (1 << (n - n / 2)); i++) {//枚举后(n/2)位 考虑空集
    		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 tmp = abs(sum);
    		if (tmp < ans.first || (tmp == ans.first&&len < ans.second))
    			ans = make_pair(tmp, len);
    		it = p.lower_bound(-sum);//才知道map还自带lower_bound
    		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.first = val; ans.second = 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.first = val; ans.second = l;
    			}
    		}
    	}
    	cout << ans.first << " " << ans.second << endl;
    	return;
    }
    
    int main(){
        ios::sync_with_stdio(0);
    	while (cin >> n) {
    		if (n == 0) return 0; 
    		p.clear();//每次清map
    		for (ll i = 0; i < n; i++) { cin >> a[i];}
    		ans = make_pair(abs(a[0]), 1);
    		solve();
    	}
    }
    
    
  • 相关阅读:
    2021.07.13 学习总结
    2021.07.12 学习总结
    2021.07.11 学习总结
    form表单提交失败
    url传参和解决中文乱码
    layui日期选择无效的问题
    Eclipse中快速生成Javabean的方法
    一种控制台输出效果的方法
    暑期周总结
    环境配置2
  • 原文地址:https://www.cnblogs.com/Zfio/p/12776656.html
Copyright © 2020-2023  润新知