題意簡述
難得放假回家,可你手頭上只有一個集合 \(S = \{i\}_{i=1}^n\) 可以玩,並且可愛的 yzh 竟然也要去你家和你一起玩。你一咬牙,把 \(S\) 分成了 \(2^n - 1\) 個非空子集。你需要把這些集合分成兩部分,一部分給 yzh,一部分留下來自己玩。需要滿足如下限制:
- 如果兩個集合同時屬於同一個人,那麼它們的並集必須屬於那一個人;
- yzh 特別鍾愛其中 \(m\) 個集合,你自然尊重她的想法,這些集合必須分給她;
- 最後 yzh 分到了恰好 \(k\) 個集合。
為了在假期讓你們兩個都玩得盡興,你能找到一個合法方案嗎?或者報告無解。
\(1 \leq n \leq 18\),\(m, k \leq 2^n - 1\)。
題目分析
需要從第一個限制入手。並集越並越大,不妨從極端來思考。併到最後肯定會有人分到了全集,為方便敘述,欽定 yzh 幸運地拿到了。那麼此時你拿到的最大的集合不能是全集,記其為 \(S_b\),補集為 \(\overline{S_b}\),那麼對於一個 \(x \in \overline{S_b}\),不能存在你拿了 \(T \ni x\),否則 \(|S_b \cup T| > |S_b|\) 與假設矛盾。也就是說,你拿到的所有集合中,至少存在一個元素,一次都沒有出現過。
以上可以歸結為一個引理,存在一個元素,包含其的所有集合都被分給了同一個人。
考慮一次決策選擇一個元素,將包含其的所有集合分給 yzh 或你,去掉這 \(2^{n - 1}\) 個集合後,又是一個子問題。也就是說,已經決策了的元素記為 \(S\),那麼這次第 \(\vert S\vert + 1\) 次決策會將 \(2^{n - 1 - \vert S\vert}\) 個集合分給某一個人。\(2\) 的冪次之間互不干擾!這是一個值得深挖的性質。我們考慮,如果 \(k\) 在二進位制表示中,不含有 \(n - 1 - \vert S\vert\) 這一位,那麼這 \(2^{n - 1 - \vert S\vert}\) 個集合不能分給 yzh。
那麼就可以用 DP 構造方案了。記 \(f(T)\) 表示能否分配所有 \(x \in T\)。邊界 \(f(\varnothing) = \text{true}\),如果 \(f(S) = \text{false}\) 說明無解,否則可以在轉移的時候記錄方案。
轉移考慮列舉一個 \(x \in T\),從 \(f(T \setminus \{x\})\) 轉移過來。分為兩類討論:
- 分給 yzh。
正如上文所言,此時需要 \(k\) 在二進位制表示中含有 \(n - 1 - \vert T \setminus \{x\}\vert = n - \vert T\vert\) 這一位。把所有 \(M \ni x\) 分給她,同時滿足 \(M\) 沒有在之前被分出,即 \(M \cap (T \setminus \{x\}) = \varnothing\)。 - 分給你。
正如上文所言,此時需要 yzh 擁有的 \(m\) 個集合中不能包含 \(x\)。但是這是在已經分配了 \(T\) 的前提之下的,也就是說,我們需要考慮 \(S \setminus T\) 的所有非空子集中,是否存在 yzh 必須選擇的集合,並且該集合包含 \(x\)。等價於檢查是否存在 yzh 必選的集合 \(P\) 滿足 \(S \setminus T \supseteq P \supset S \setminus T \setminus \{x\}\)。這一步可以採用高維字首和最佳化判斷。我們可以預處理出 \(h(S)\) 表示 \(S\) 的所有非空子集中,有多少必須被 yzh 選擇。
這樣,我們可以 \(\mathcal{O}(n 2^n)\) 預處理高維字首和、轉移。
構造方案我們從 \(S \rightarrow \varnothing\),每次分出包含 \(x\) 的所有集合,這裡 \(x\) 是我們轉移的時候記錄的轉移點,參考上文,如果 \(x\) 要分給 yzh,需要標記一下。如果你搞懂了上面的過程,相信這裡難不倒你。這裡時間複雜度也是 \(\mathcal{O}(n2^n)\)。
總時間複雜度 \(\mathcal{O}(n2^n)\)。
程式碼
#include <cstdio>
const int N = 1 << 18 | 10;
int n, m, k;
int a[N], g[N];
bool f[N], ans[N];
signed main() {
#ifndef XuYueming
freopen("set.in", "r", stdin);
freopen("set.out", "w", stdout);
#endif
scanf("%d%d%d", &n, &m, &k);
for (int i = 1, x; i <= m; ++i) scanf("%d", &x), a[x] = 1;
for (int i = 0; i < n; ++i)
for (int j = 0; j < 1 << n; ++j)
if (j & 1 << i)
a[j] += a[j ^ 1 << i];
// a[S] 表示 S 子集中有多少個必須被 yzh 選擇
f[0] = true;
int S = (1 << n) - 1;
for (int i = 1; i < 1 << n; ++i) {
int x = __builtin_popcount(i); // 現在是第 x 次分
for (int j = 0; j < n; ++j) // 把第 j 個元素分給 yzh 或 xym
if ((i & 1 << j) && f[i ^ 1 << j] && (
(k & 1 << (n - x)) // 分給 yzh
|| a[S ^ i] == a[S ^ i ^ 1 << j] // 分給 xym
)) {
f[i] = true;
g[i] = j;
break;
}
}
if (!f[S]) return puts("-1"), 0;
for (int x = S, i = n; i; --i) {
if (k & 1 << (n - i)) { // 需要分給 yzh
for (int j = 1; j < 1 << n; ++j)
if (!(j & (x ^ 1 << g[x])) && (j & 1 << g[x])) // 沒在之前分走,並且包含 g[x]
ans[j] = true;
}
x ^= 1 << g[x];
}
for (int i = 1; i < 1 << n; ++i)
putchar('0' | ans[i]);
return 0;
}