D. Love-Hate 解题思路
核心问题分析
题意: 个朋友, 种货币。给出一个 的 01 矩阵,第 行第 列为 表示朋友 喜欢货币 。保证每个朋友最多只喜欢 种货币。要求找出一个尽可能大的货币子集 ,使得至少有 个朋友同时喜欢 里的每一种货币,输出这个 。
数据范围很有意思: 很大,但 、 特别小。 提示我们每个朋友的喜好可以压成一个 位整数,而 ——那个 简直是在明示状压。问题在于,直接在 个货币上枚举子集是 ,完全不可能。 这个约束到底怎么用上?
1. 关键观察:答案一定是某个朋友喜好集合的子集
先想一件很朴素的事:假设最优解是集合 ,它被至少 个朋友"全盘接受"。随便挑出这些朋友中的一个,记他的喜好集合为 ,那么必然有
因为他喜欢 里的每一种货币嘛。而 ,所以
答案 一定是某个朋友的喜好集合的子集,而那个集合最多只有 个元素。
这就把搜索空间从 砍到了「枚举某个朋友 → 在他的 个货币里枚举子集」,即 。但 , 还是太大了,不能对每个朋友都做一遍。
2. 随机化:随便抓一个人,他有一半概率"中奖"
这里就是这道题最漂亮的一步。我们并不需要枚举所有朋友,只需要找到任意一个接受最优解 的朋友就够了。而根据题目条件,这样的朋友至少有 个——也就是说至少占了一半。
于是随机抽一个朋友,他"中奖"(即 )的概率 。独立随机抽 次,一次都不中的概率是 。取 ,失败概率是 ——这个数小到什么程度呢,比你的代码在评测机上被宇宙射线打翻还要小得多。
random_engine get(1, n);
for (int i = 1; i <= 100; i++) {
string s = a[get()]; // 随机抓一个朋友,赌他包含最优解
...
}这就是经典的 Monte Carlo 随机化思路:与其花 去确定性地找那个"对的人",不如利用「对的人占了一半」这个性质,用几十次随机采样把正确率拉到几乎必然。
3. 对选中的朋友做子集计数:高维前缀和登场
现在固定了一个朋友,他喜欢的货币下标是 ()。我们要对这 个货币的每一个子集 ,算出「有多少朋友喜欢 里的所有货币」,然后取满足 的最大子集。
第一步,把每个朋友的喜好投影到这 个位置上,得到一个 位的 mask,然后开桶统计:
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] 的含义是:恰好喜欢 这个集合(在这 个位置上)的朋友数。但我们要的是"至少喜欢 的所有元素"的人数,也就是
这是标准的超集和(superset sum),可以用**高维前缀和(SOS DP)**在 内一次性算出所有 的答案——每一维(每一个货币位)独立地做一次"把有这一位的加到没有这一位的上面":
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] 就直接是「喜欢 中全部货币的朋友数」。相比暴力的 子集枚举,SOS DP 把它压到了 ,这在 时是 ,非常轻。
4. 取答案
最后扫一遍所有子集,凡是 的就是合法解,在里面取 最大的:
for (int j = 0; j < (1 << c); j++) {
if (dp[j] >= target) {
int now = __builtin_popcount(j);
if (now > ans) { ans = now; /* 把 j 映射回原来的 m 位下标 */ }
}
}注意最后要把 位的 mask 映射回原始的 位下标再输出—— 的第 位对应的是原始货币 。
5. 复杂度
一共随机 轮,每轮的代价是:投影统计 ,SOS DP ,扫答案 。总复杂度
代进 ,大概是 ——看起来吓人,但这里全是极其简单的位运算和数组访问,缓存友好, 秒时限绰绰有余。实际上轮数取 就已经足够稳了, 只是买个心安。
回头看这道 的题," 提示状压"是每个人都能看出来的,真正卡人的是怎么把 这一维消掉。答案是那个漂亮的概率论小转身:既然合法答案的持有者占了全体的一半以上,那我就不必去"找"他,随手一抓再抓一百次,他跑不掉。这种"用随机化把确定性的枚举维度砍掉"的手法,在 CF 的中高难度题里出现频率相当高,值得单独记在脑子里。
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();
}
}