CF题解——Love-Hate

D. Love-Hate 解题思路

核心问题分析

题意:nn 个朋友,mm 种货币。给出一个 n×mn \times m 的 01 矩阵,第 ii 行第 jj 列为 11 表示朋友 ii 喜欢货币 jj。保证每个朋友最多只喜欢 pp 种货币。要求找出一个尽可能大的货币子集 SS,使得至少有 n/2\lceil n/2 \rceil 个朋友同时喜欢 SS 里的每一种货币,输出这个 SS

数据范围很有意思:n2×105n \le 2 \times 10^5 很大,但 m60m \le 60p15p \le 15 特别小。m60m \le 60 提示我们每个朋友的喜好可以压成一个 6464 位整数,而 p15p \le 15——那个 215=327682^{15} = 32768 简直是在明示状压。问题在于,直接在 m=60m = 60 个货币上枚举子集是 2602^{60},完全不可能。pp 这个约束到底怎么用上?

1. 关键观察:答案一定是某个朋友喜好集合的子集

先想一件很朴素的事:假设最优解是集合 SS,它被至少 n/2\lceil n/2 \rceil 个朋友"全盘接受"。随便挑出这些朋友中的一个,记他的喜好集合为 TT,那么必然有

STS \subseteq T

因为他喜欢 SS 里的每一种货币嘛。而 Tp15|T| \le p \le 15,所以

答案 SS 一定是某个朋友的喜好集合的子集,而那个集合最多只有 1515 个元素。

这就把搜索空间从 2602^{60} 砍到了「枚举某个朋友 → 在他的 15\le 15 个货币里枚举子集」,即 n2pn \cdot 2^p。但 n=2×105n = 2 \times 10^52×105×327682 \times 10^5 \times 32768 还是太大了,不能对每个朋友都做一遍。

2. 随机化:随便抓一个人,他有一半概率"中奖"

这里就是这道题最漂亮的一步。我们并不需要枚举所有朋友,只需要找到任意一个接受最优解 SS 的朋友就够了。而根据题目条件,这样的朋友至少有 n/2\lceil n/2 \rceil 个——也就是说至少占了一半

于是随机抽一个朋友,他"中奖"(即 STS \subseteq T)的概率 12\ge \frac{1}{2}。独立随机抽 kk 次,一次都不中的概率是 2k\le 2^{-k}。取 k=100k = 100,失败概率是 21002^{-100}——这个数小到什么程度呢,比你的代码在评测机上被宇宙射线打翻还要小得多。

cpp
random_engine get(1, n);
for (int i = 1; i <= 100; i++) {
    string s = a[get()];  // 随机抓一个朋友,赌他包含最优解
    ...
}

这就是经典的 Monte Carlo 随机化思路:与其花 O(n)O(n) 去确定性地找那个"对的人",不如利用「对的人占了一半」这个性质,用几十次随机采样把正确率拉到几乎必然。

3. 对选中的朋友做子集计数:高维前缀和登场

现在固定了一个朋友,他喜欢的货币下标是 v0,v1,,vc1v_0, v_1, \dots, v_{c-1}cp15c \le p \le 15)。我们要对这 cc 个货币的每一个子集 SS,算出「有多少朋友喜欢 SS 里的所有货币」,然后取满足 n/2\ge \lceil n/2 \rceil 的最大子集。

第一步,把每个朋友的喜好投影到这 cc 个位置上,得到一个 cc 位的 mask,然后开桶统计:

cpp
vector<int> dp(1 << c);
for (int j = 1; j <= n; j++) {
    int mask = 0;
    for (int k = 0; k < c; k++) {
        if (a[j][v[k]] == '1') mask |= 1 << k;
    }
    dp[mask]++;
}

此时 dp[mask] 的含义是:恰好喜欢 maskmask 这个集合(在这 cc 个位置上)的朋友数。但我们要的是"至少喜欢 SS 的所有元素"的人数,也就是

f(S)=STdp[T]f(S) = \sum_{S \subseteq T} dp[T]

这是标准的超集和(superset sum),可以用**高维前缀和(SOS DP)**在 O(2cc)O(2^c \cdot c) 内一次性算出所有 SS 的答案——每一维(每一个货币位)独立地做一次"把有这一位的加到没有这一位的上面":

cpp
for (int j = 0; j < c; j++) {
    for (int k = (1 << c) - 1; k > 0; k--) {
        if (!(k & (1 << j))) dp[k] += dp[k | (1 << j)];
    }
}

这一步做完,dp[S] 就直接是「喜欢 SS 中全部货币的朋友数」。相比暴力的 O(3c)O(3^c) 子集枚举,SOS DP 把它压到了 O(2cc)O(2^c \cdot c),这在 c=15c = 15 时是 32768×155×10532768 \times 15 \approx 5 \times 10^5,非常轻。

4. 取答案

最后扫一遍所有子集,凡是 dp[S]n/2dp[S] \ge \lceil n/2 \rceil 的就是合法解,在里面取 popcount\text{popcount} 最大的:

cpp
for (int j = 0; j < (1 << c); j++) {
    if (dp[j] >= target) {
        int now = __builtin_popcount(j);
        if (now > ans) { ans = now; /* 把 j 映射回原来的 m 位下标 */ }
    }
}

注意最后要把 cc 位的 mask 映射回原始的 mm 位下标再输出——jj 的第 kk 位对应的是原始货币 vkv_k

5. 复杂度

一共随机 100100 轮,每轮的代价是:投影统计 O(np)O(n \cdot p),SOS DP O(2pp)O(2^p \cdot p),扫答案 O(2p)O(2^p)。总复杂度

O(100(np+2pp))O\big(100 \cdot (n p + 2^p p)\big)

代进 n=2×105, p=15n = 2\times10^5,\ p = 15,大概是 100×(3×106+5×105)3.5×108100 \times (3 \times 10^6 + 5 \times 10^5) \approx 3.5 \times 10^8——看起来吓人,但这里全是极其简单的位运算和数组访问,缓存友好,33 秒时限绰绰有余。实际上轮数取 305030 \sim 50 就已经足够稳了,100100 只是买个心安。

回头看这道 2400*2400 的题,"p15p \le 15 提示状压"是每个人都能看出来的,真正卡人的是怎么把 nn 这一维消掉。答案是那个漂亮的概率论小转身:既然合法答案的持有者占了全体的一半以上,那我就不必去"找"他,随手一抓再抓一百次,他跑不掉。这种"用随机化把确定性的枚举维度砍掉"的手法,在 CF 的中高难度题里出现频率相当高,值得单独记在脑子里。

CPP 代码实现

cpp
// D. Love-Hate

#include <bits/stdc++.h>
#define lg(x) (63 - __builtin_clzll(x))
#define all(x) (x).begin(), (x).end()
#define low_bit(x) ((x) & (-x))
#define pb push_back
#define db long double
#define int long long
#define sz(x) (int)x.size()
#define endl "\n"

using namespace std;

void solve() {

    int n, m, p;
    cin >> n >> m >> p;
    vector<string> a(n + 1);
    for (int i = 1; i <= n; i++) cin >> a[i];

    int target = (n + 1) / 2, ans = 0;
    string res(m, '0');

    mt19937 rng(chrono::steady_clock::now().time_since_epoch().count());
    uniform_int_distribution<int> uni(1, n);

    for (int i = 1; i <= 100; i++) {
        string s = a[uni(rng)];
        vector<int> v;
        for (int j = 0; j < m; j++) {
            if (s[j] == '1') v.pb(j);
        }

        int c = sz(v);
        vector<int> dp(1 << c, 0);
        for (int j = 1; j <= n; j++) {
            int mask = 0;
            for (int k = 0; k < c; k++) {
                if (a[j][v[k]] == '1') mask |= 1 << k;
            }
            dp[mask]++;
        }

        for (int j = 0; j < c; j++) {
            for (int k = (1 << c) - 1; k > 0; k--) {
                if (!(k & (1 << j))) dp[k] += dp[k | (1 << j)];
            }
        }

        for (int j = 0; j < (1 << c); j++) {
            if (dp[j] >= target) {
                int now = __builtin_popcountll(j);
                if (now > ans) {
                    ans = now;
                    res = string(m, '0');
                    for (int k = 0; k < c; k++) {
                        if (j & (1 << k)) res[v[k]] = '1';
                    }
                }
            }
        }
    }

    cout << res << endl;

}

signed main() {

    ios_base::sync_with_stdio(false);
    cin.tie(nullptr);

    int t = 1;
    // cin >> t;

    while (t--) {
        solve();
    }

}
CF题解——Xor-MST
CF题解——Remove the Grail Tree