跳到主要内容

P3067 [USACO12OPEN] Balanced Cow Subsets G

难度算法s日期题目链接
提高+/省选−折半搜索2025-09-24https://luogu.com.cn/problem/P3067

这道题在我的任务列表里面很久了,今天下定决心一定要切掉。

题意简述

我们定义一个奶牛集合 SS 是平衡的,当且仅当满足以下两个条件:

  • SS 非空。
  • SS 可以被划分成两个集合 A,BA,B,满足 AA 里的奶牛产奶量之和等于 BB 里的奶牛产奶量之和。划分的含义是,AB=SA\cup B=SAB=A\cap B=\varnothing

现在给定大小为 nn 的奶牛集合 SS,询问它有多少个子集是平衡的。请注意,奶牛之间是互不相同的,但是它们的产奶量可能出现相同。

范围:1n201\le n\le 201ai1081\le a_i\le 10^8

思路

如果暴力搜索,枚举每种划分方法,每头牛有三种状态:放入集合 AA、放入集合 BB、不放。(这样就涵盖了 SS 的所有子集)

则总时间复杂度为 O(3n)O(3^n)3203×1093^{20}\approx3\times10^9,无法接受。

使用折半搜索,可以把时间复杂度降到 O(3n/2)O(3^{n/2})3106×1043^{10}\approx6\times10^4,温柔得多,可以接受。


本题难点在于如何设计折半的方法。

假设在前 n/2n/2 头奶牛中,选 a1a_1 头放入集合 AAb1b_1 头放入集合 BB;在后 n/2n/2 头奶牛中,选 a2a_2 头放入集合 AAb2b_2 头放入集合 BB

最终集合 AA 和集合 BB 要平衡,就是要满足 a1+a2=b1+b2a_1+a_2=b_1+b_2

可是我们要把前后两个半段分开统计,所以移项一下,变成 a1b1=b2a2a_1-b_1=b_2-a_2

也就是说,我们要在前半段中,分别统计每个 (a1b1)=k(a_1-b_1)=k 的方案数。即统计 ans1[]\text{ans1}[],其中 ans1[k]=true\text{ans1}[k]=\text{true} 代表存在满足 (a1b1)=k(a_1-b_1)=k 的方案。

后半段也类似,统计 ans2[]\text{ans2}[],其中 ans2[k]=true\text{ans2}[k]=\text{true} 代表存在满足 (b2a2)=k(b_2-a_2)=k 的方案。


这对吗?

显然,a1b1=ka_1-b_1=k 一定时,可能有不同的划分方法。所以尽管我们用 a1b1=b2a2a_1-b_1=b_2-a_2 作为判据,但是只记录 ans[k]\text{ans}[k] 是不行的。我们应该记录对于每个 kk划分方式 ss 一共有几种。

这对吗?

再读题目,发现题目要求的是 “子集” 的个数。两个子集不同,当且仅当我们选择放入子集的奶牛不同。至于它是放在集合 AA 还是集合 BB,这里并不关心。所以,我们需要去重,避免那些 ABA\cup B 相同的子集被重复统计。所以,我们应该记录对于每个 kk,所有的划分方式 ss

这里奶牛最多有 2020 头,int 是 32 位的,我们考虑使用状态压缩,用 int 存状态。第一次搜索时,我们记录下所有状态 s1s_1;第二次搜索,设一次搜索结束后状态为 s2s_2,我们令 ans[s1s2]=true\text{ans[}s_1|s_2\text{]}=\text{true}。最后,我们统计 ans[k]=true\text{ans[k]}=\text{true} 的一共有几个,就得到了答案。

大功告成!

编码

#include <bits/stdc++.h>
using namespace std;
const int MAXN = 20;
int cow[MAXN + 1], n;
map<int, vector<unsigned>> states; // state[k] 表示判据为 k 的所有状态
bool ans[(1 << 21) + 1]; // ans[s] 表示这种划分方式是否存在
void dfs1(int dep, int value, unsigned state) {
if (dep > n / 2) {
states[value].push_back(state);
} else {
// 放入集合 A
dfs1(dep + 1, value + cow[dep], state | (1 << dep));
// 不放
dfs1(dep + 1, value, state);
// 放入集合 B
dfs1(dep + 1, value - cow[dep], state | (1 << dep));
}
}
void dfs2(int dep, int value, unsigned state) {
if (dep > n) {
if (!states.count(value)) return;
for (auto s : states[value]) {
ans[s | state] = true;
}
} else {
// 放入集合 A
dfs2(dep + 1, value - cow[dep], state | (1 << dep));
// 不放
dfs2(dep + 1, value, state);
// 放入集合 B
dfs2(dep + 1, value + cow[dep], state | (1 << dep));
}
}
int main() {
cin >> n;
for (int i = 1; i <= n; i++) cin >> cow[i];
dfs1(1, 0, 0);
dfs2(n / 2 + 1, 0, 0);
int total = 0;
for (auto i : ans) total += i;
cout << total - 1 << endl; // 排除空集的情况
return 0;
}