N的範圍很小,可以聯想到枚舉子集和狀壓.但是如果直接枚舉兩個子集,顯然是不夠的.那麼我們可以聯想到折半枚舉!Meet inthe Middle!
把n個數分成兩部分A,B集合,答案子集的來源有以下幾種:
1. A集合的子集.
2. B集合的子集.
3. 一部分是A的子集,一部分是B的子集.
對於1,2兩種情況都可以直接預處理出:
枚舉A的子集x,再枚舉x的子集y,把x分爲y,和x^y兩個集合,判斷它們的總和是否相等.
對於第3種情況:
假設a,b在A集合裏,c,d在B集合裏,而有a+c=b+d.我們可以把式子移項=>a-b=d-c,也就是說在A集合(集合元素爲n1)中的每個元素有三種可能:不取,給A集合,給B集合.那麼對於A集合一共有3n1種方案.同理B集合就有3n2種方案,把各集合產生的方案的值,與選取的元素記錄下來,按照值排序.對於兩個有序數組進行歸併:
對於值相等的兩個集合,它們的交集一定可以滿足條件.
優化:
在歸併的過程中,會發現有很多重複的情況.
在A集合裏既有a-b的狀態,也有b-a的狀態.它們分別於d-c,c-d對應,但是它們產生的交集是相同的,都是{a,b,c,d}.所以b-a和c-d狀態是無效的,我們在處理狀態時可以過濾掉 值爲負數的狀態.
#include<cstdio>
#include<iostream>
#include<algorithm>
#include<vector>
#include<map>
#include<cstring>
using namespace std;
const int M=20;
int A[M],mark[1<<M],n,f[1<<M],totc=0,totb=0;
struct node{
int val,p;
}B[60005],C[60005];
bool cmp(node a,node b){
return a.val<b.val;
}
int solve(int st,int t){//判斷一個集合內部滿足要求的子集個數
int i,j,k,ans=0;
for(i=1;i<(1<<t);i++){
f[i]=0;
for(j=0;j<t;j++)
if(i&(1<<j))f[i]+=A[st+j];
for(j=i;j;j=(j-1)&i){
int a=j,b=i^j;//兩個子集
if(a==0||b==0)continue;
if(f[a]==f[b]){
ans++;break;//
}
}
}
return ans;
}
void get(int all,int l,int r,node t[],int &num){
int i,j,k;
for(i=all;i;i=(i-1)&all){
for(j=i;j;j=(j-1)&i){
int a=j,b=i^j,now=0;//b集合要給對方
for(k=l;k<r;k++){
if(a&(1<<k))now+=A[k];
else if(b&(1<<k))now-=A[k];
}
if(now>=0)t[++num]=(node){now,i};
}
}
sort(t+1,t+1+num,cmp);
}
int main(){
int i,j,k,ans=0,stb=1,stc=1;
scanf("%d",&n);
for(i=0;i<n;i++)scanf("%d",&A[i]);
int s1=n/2,s2=n-n/2;
ans+=solve(0,s1)+solve(s1,s2);
get((1<<s1)-1,0,s1,B,totb);
get(((1<<n)-1)^((1<<s1)-1),s1,n,C,totc);
while(stb<=totb&&stc<=totc){//歸併兩個序列
while(stc<=totc&&stb<=totb&&C[stc].val!=B[stb].val){
while(C[stc].val<B[stb].val)stc++;
while(B[stb].val<C[stc].val)stb++;
}
if(stc>totc||stb>totb)break;
int c1=stc,b1=stb,x=C[stc].val;
while(C[stc].val==x)stc++;
while(B[stb].val==x)stb++;
for(i=c1;i<stc;i++)
for(j=b1;j<stb;j++)
mark[C[i].p|B[j].p]=1;
}
for(i=0;i<(1<<n);i++)ans+=mark[i];
printf("%d\n",ans);
return 0;
}