第四题:T4平分子集(三)
标签:状态压缩、折半枚举
题意:一个集合被称之为可平分的,如果它可以被分为两部分,且两部分的元素之和相等。空集也算可平分的。给定一个集合
a
1
,
a
2
,
a
3
,
…
,
a
n
a_1,a_2,a_3,…,a_n
a1,a2,a3,…,an,请统计它有多少子集是可平分的。(本题中所指的集合允许元素相等)(
1
<
=
n
<
=
20
,
1
<
=
a
i
<
=
1
0
7
1<=n<=20,1<=a_i<=10^7
1<=n<=20,1<=ai<=107)
题解:看到题目
n
n
n这么小,很容易想到暴力深搜的写法,对于每个数选或不选,然后在选择了的数中再跑一遍深搜看看,能不能分成两部分,然后两部分分别的元素之和相等,试了一下发现这样整体的时间复杂度还是比较大的,并且重复的部分不好处理。
重复的部分,我们会去想 能不能通过状态压缩 把每个数选择的情况根据二进制的情况进行处理一下。针对降低时间复杂度这部分,如果折半枚举题目写的多的同学,应该熟悉这个套路。
每个数有三种情况选择:不选、放在
A
A
A 集合、放在
B
B
B 集合。
我们设前半部分放到
A
A
A 集合内的所有数之和为
a
a
a,放在
B
B
B集合内的所有数之和为
b
b
b。
我们设前半部分放到
A
A
A 集合内的所有数之和为
c
c
c,放在
B
B
B集合内的所有数之和为
d
d
d。
那么,
a
+
c
=
b
+
d
a+c = b+d
a+c=b+d,移项得到
a
−
b
=
d
−
c
a-b=d-c
a−b=d−c。(
a
−
b
a-b
a−b其实就是
A
A
A集合所有数之和减去
B
B
B集合所有数之和,
d
−
c
d-c
d−c是
A
A
A集合所有数之和减去
B
B
B集合所有数之和的相反数)
我们先预处理出前半部分的 a − b a-b a−b情况,去存储一下所有得到当前 a − b a-b a−b的状态情况(即二进制状态压缩的值),然后后半部分的时候 得到 c − d c-d c−d情况的时候,去之前存储的部分 找一下有多少 − ( c − d ) = a − b -(c-d)=a-b −(c−d)=a−b的状态。
为了避免重复,我们把着前半部分的对应值的状态和后半部分对应值的状态进行一下按位或,进行结合,标记一下这
n
n
n个数在选择哪些状态的情况下能够达到题目中的要求。
代码:
#include <bits/stdc++.h>
using namespace std;
map<int, int> m;
int n, id = 0, a[25], ans[2000005];
vector<int> e[2000005];
// p: 选到第p个数
// sum: A集合所有数之和减去B集合所有数之和
// x: 当前选了哪些数, 二进制 状态压缩
void dfs1(int p, int sum, int x) {
if (p > n / 2) {
if (!m[sum]) m[sum] = ++id;
e[m[sum]].push_back(x);
return ;
}
dfs1(p + 1, sum, x);
dfs1(p + 1, sum + a[p], x | (1<<(p-1)));
dfs1(p + 1, sum - a[p], x | (1<<(p-1)));
}
void dfs2(int p, int sum, int x) {
if (p > n) {
// 从前半部分的枚举情况下找-sum的形成状态
int len = e[m[-sum]].size();
for (int i = 0; i < len; i++) {
// 维护实际的值的状态 避免重复
ans[e[m[-sum]][i] | x] = 1;
}
return ;
}
dfs2(p + 1, sum, x);
dfs2(p + 1, sum + a[p], x | (1<<(p-1)));
dfs2(p + 1, sum - a[p], x | (1<<(p-1)));
}
int main() {
cin >> n;
for (int i = 1; i <= n; i++) cin >> a[i];
dfs1(1, 0, 0);
dfs2(n / 2 + 1, 0, 0);
int res = 0;
for (int i = 0; i < (1<<n); i++) {
if (ans[i] == 1) res++;
}
cout << res << endl;
return 0;
}