2025.06.19 CW 模拟赛 C. 挑战 NPC
C. 挑战 NPC
题目描述
众所周知,一般图最大团问题是 NPC 问题。现在自命不凡的你要挑战它。
我们定义团是每两个点都有边相连的点集,特别地,单独一个点也算作团。
思路
原题数据较水, 搜索配合剪枝即可通过.
关于「一般图最大团」问题可以使用 \(\text{Bron–Kerbosch}\) 算法, 但是对于这道题, 我们有更简洁的做法.
观察到 \(n \le 45\), 复杂度 \(\mathcal{O}(n 2^{\frac n 2})\) 的算法是可以通过的, 不难想到使用折半搜索.
具体而言, 折半搜索的一般流程为
- 将原序列分为两半, 分别进行搜索并记录状态.
- 合并两侧状态.
在这道题中, 第一步即先搜索出前一半所有能够成为团的点的集合, 再搜索出后一半能够成为团的点集.
考虑合并.
在第一步中, 我们将点集状压为一个二进制数 \(S\), 定义 \(f_S\) 表示 \(S\) 及它的子集中能够成为团的最大点数, 使用高维前缀和可以做到 \(\mathcal{O}(\frac n 2 \times 2^{\frac n 2})\).
合并时, 我们枚举后一半所有合法点集, 并记录其中所有点到前一半的边集与起来的值, 最后答案即为 \(\max\{\text{后一半点集大小} + f_{\text{边集与起来的值}}\}\).
复杂度 \(\mathcal{O}(\frac n 2 \times 2^{\frac n 2})\).
#include <iostream>
using namespace std;
typedef long long ll;
int n, m, mid, f[4194304];
ll pow2[45], e[45];
void pre() {
pow2[0] = 1;
for (int i = 1; i < 45; ++i) {
pow2[i] = pow2[i - 1] + pow2[i - 1];
}
}
void init() {
cin >> n >> m;
for (int i = 0; i < n; ++i) {
e[i] = pow2[i];
}
for (int i = 1, u, v; i <= m; ++i) {
cin >> u >> v, --u, --v;
e[u] |= pow2[v], e[v] |= pow2[u];
}
mid = n / 2;
for (int S = 0; S < pow2[mid]; ++S) {
int T = pow2[mid] - 1;
for (int i = 0; i < mid; ++i) {
if (S >> i & 1) {
T &= e[i];
}
}
if ((S & T) == S) {
f[S] = __builtin_popcount(S);
}
else {
f[S] = 0;
}
}
for (int S = 0; S < pow2[mid]; ++S) {
for (int i = 0; i < mid; ++i) {
if (S >> i & 1) {
f[S] = max(f[S], f[S ^ pow2[i]]);
}
}
}
}
void calculate() {
int ans = 0;
for (int S = 0; S < pow2[n - mid]; ++S) {
ll Tbig = (pow2[n] - 1) ^ (pow2[mid] - 1);
int Tsmall = pow2[mid] - 1;
for (int i = 0; i < n - mid; ++i) {
if (S >> i & 1) {
Tbig &= e[mid + i], Tsmall &= e[mid + i];
}
}
if ((S & (Tbig >> mid)) == S) {
ans = max(ans, __builtin_popcount(S) + f[Tsmall]);
}
}
cout << ans << '\n';
}
void solve() {
cin.tie(nullptr)->sync_with_stdio(false);
pre();
int t;
cin >> t;
while (t--) {
init(), calculate();
}
}
int main() {
solve();
return 0;
}

浙公网安备 33010602011771号