集合划分 题解

题意简述

难得放假回家,可你手头上只有一个集合 \(S = \{i\}_{i=1}^n\) 可以玩,并且可爱的 yzh 竟然也要去你家和你一起玩。你一咬牙,把 \(S\) 分成了 \(2^n - 1\) 个非空子集。你需要把这些集合分成两部分,一部分给 yzh,一部分留下来自己玩。需要满足如下限制:

  1. 如果两个集合同时属于同一个人,那么它们的并集必须属于那一个人;
  2. yzh 特别钟爱其中 \(m\) 个集合,你自然尊重她的想法,这些集合必须分给她;
  3. 最后 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\})\) 转移过来。分为两类讨论:

  1. 分给 yzh。
    正如上文所言,此时需要 \(k\) 在二进制表示中含有 \(n - 1 - \vert T \setminus \{x\}\vert = n - \vert T\vert\) 这一位。把所有 \(M \ni x\) 分给她,同时满足 \(M\) 没有在之前被分出,即 \(M \cap (T \setminus \{x\}) = \varnothing\)
  2. 分给你。
    正如上文所言,此时需要 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;
}
posted @ 2024-11-06 08:45  XuYueming  阅读(10)  评论(0编辑  收藏  举报