题解 选数

题目链接

给出 n 个数,要求从中选出任意个数,使之能划分为和相等的两组,求方案数。
\(1\le n\le 23,1\le x\le 10^8\).

一道非常不错的”折半“搜索。

注意:同样一组选数,划分方法不同不会重复计数。

“划分为和相等的两组” 可以进行一定的转化,比如:对于数列:\(1,2,3,4\),我们可以将其划分为 \(\{1,3\},\{2,4\}\),但也可以这么想:将 \(\{1,3\}\) 乘上权值 \(1\),还是 \(\{1,3\}\)\(\{2,4\}\) 乘上权值 \(-1\),变成 \(\{-2,-4\}\),而 \(1+3-2-4=0\)

所以检验一个选数方案是否合法,可以看成能否附上权值 \(\{1,-1\}\) 使得和为 \(0\)

如果直接暴力,那么每个数有 不选(赋\(0\)),选并赋\(1\),选并赋\(-1\),时间复杂度为 \(O(3^n)\),无法通过本题。

由于我们关心到底有哪些数选了,所以可以维护一个有哪些数字没选的状态,二者等价。

考虑折半搜索,假设左边选 \(m\) 个数,可以在 \(O(3^m)\) 内计算出所有和及其对应状态。

对于右边,我们只需处理 \(n-m\) 个数,同样可在 \(O(3^{n-m})\) 内计算出和及对应的状态。

假设现在计算出的和为 \(nsum\), 此时统计答案需要考虑:

左边有多少个状态出现了 \(-nsum\),以及左边的状态和右边状态合起来,会不会在以前出现过,如果出现过,就说明我们之前考虑过这个选数方案,不能重复计数。

所以直接暴力枚举左边的状态,时复 \(O(2^n)\),所以总时复 \(O(3^m+3^{n-m}\times 2^n)\),通过计算,对于极限数据 \(m=16\) 最优。

但是直接暴力枚举常数过大,无法通过本题,由于我们需要快速处理左边出现的状态不会与当前状态并起来出现过的状态的并集的个数,考虑 \(bitset\) 优化。

记:zt_num[i] 表示左边凑出和为 \(i\) 的状态情况,zt_st[i] 表示对于右边的状态 \(i\),有哪些状态可以和他合在一起(即合在一起的状态之前没有出现过)。

那么可以这么快速统计答案:

ans+=(zt_sum[-nsum]&(~zt_st[zt])).count();
zt_st[zt]|=zt_sum[-nsum]&(~zt_st[zt]);

代码解释:

由于 zt_st[zt] 中等于 \(0\) 的位置是之前没有出现过的,即合法的,所以在 \(\&\) 的时候需要进行取反。

zt_sum[-nsum]&(~zt_st[zt]) 即求出上述说到的并集。

zt_st[zt]|=zt_sum[-nsum]&(~zt_st[zt]); 将本次统计的状态进行标记,以后不在统计。

代码:

#include<bits/stdc++.h>
using namespace std;

typedef long long LL;
#define PLI pair<LL,int>
const int N=26;
int n,len1,ans,a[N];
unordered_map<LL,bitset<1<<17>> zt_sum,zt_st;

void dfs(int nd,int ed,LL nsum,int zt) {
    if(nd==ed+1) {
        if(ed!=n) {
            zt_sum[nsum][zt]=1;
        }
        else {
            ans+=(zt_sum[-nsum]&(~zt_st[zt])).count();
            zt_st[zt]|=zt_sum[-nsum]&(~zt_st[zt]);
        }
        return ;
    }
    dfs(nd+1,ed,nsum,zt|(1<<nd-1));
    dfs(nd+1,ed,nsum+a[nd],zt);
    dfs(nd+1,ed,nsum-a[nd],zt);
}

int main(){
    cin>>n;
    for(int i=1;i<=n;i++) cin>>a[i];
    len1=n*16/23;
    dfs(1,len1,0,0);
    dfs(len1+1,n,0,0);
    cout<<ans-1; //代码会统计啥也不选的情况,减掉
    return 0;
}
posted @ 2024-07-12 17:00  2017BeiJiang  阅读(34)  评论(0编辑  收藏  举报