最大团
1.随机化算法2.组合数学3.概率和期望4.树链剖分5.矩阵乘法与矩阵快速幂6.最短路7.卡特兰数、Prüfer 序列、BSGS8.分块和莫队9.AC 自动机10.平衡树11.基础字符串算法12.基础数论13.Miller-Rabin 与 Pollard-Rho14.广义后缀自动机15.后缀自动机 SAM16.回文自动机 PAM17.Manacher18.后缀数组19.01 分数规划20.网络流21.整体二分22.cdq 分治23.点分治24.虚树25.扫描线26.笛卡尔树27.基环树28.树哈希29.莫比乌斯反演30.二分图31.朱刘算法
32.最大团
33.杂项34.杜教筛35.拉格朗日插值36.线段树综合37.可持久化数据结构38.K-D Tree39.Burnside 引理与 Polya 定理40.线性基41.替罪羊树42.LCT43.插头 dp44.原根45.多项式乘法46.斯特林数47.二项式反演与斯特林反演48.Min-Max 容斥49.辛普森积分法50.Min_25 筛51.凸包52.2-SAT1 问题引入
给定一个无向图,定义如下概念:
- 团:节点之间两两相连的子图。
- 极大团:不能再继续扩大的团。
- 最大团:一张无向图中最大的极大团。
现在的问题是怎样求出一个图的最大团。实际上这个问题是 NP-hard 的,也就是没有多项式解法。但是有复杂度较为优秀的指数级算法。
主流算法有两种:状压 dp 和 Bron-Kerbosch 算法。后一种算法复杂度更低,前一种较为简单。
2 状压 dp 解法
先考虑朴素状压怎么办。记录下每一个点向外扩展的点集
接下来我们使用状压 dp + meet in the middle 优化复杂度,可以达到
考虑枚举前一半的点集,对于这些点构成的任意集合
那么整体代码如下:
#include <bits/stdc++.h>
#define int long long
using namespace std;
const int Maxn = 2e5 + 5;
const int Inf = 2e9;
int n, m;
int out[45];
int f[1 << 20];
int lowbit(int x) {
return x & (-x);
}
signed main() {
ios::sync_with_stdio(0);
cin.tie(0), cout.tie(0);
cin >> n >> m;
for(int i = 1; i <= m; i++) {
int u, v;
cin >> u >> v;
out[u] |= (1ll << (v - 1));//记录每一个点的出点
out[v] |= (1ll << (u - 1));
}
int ans = 0;
int x = (n >> 1), y = n - x;
for(int i = 1; i < (1 << x); i++) {//枚举前一半的点集
bool flg = 1;
for(int j = 1; j <= x; j++) {
if(!((i >> (j - 1)) & 1)) continue;
if((out[j] & i) != (i ^ (1 << (j - 1)))) {//判断是不是完全图
flg = 0;
}
}
if(flg) {
f[i] = __builtin_popcount(i);//直接记录下每一个团的 f 值
ans = max(ans, f[i]);
}
}
for(int i = 1; i < (1 << x); i++) {
for(int j = 1; j <= x; j++) {//用每一个团去更新剩下的子图
if(!((i >> (j - 1)) & 1)) continue;
f[i] = max(f[i], f[i ^ (1 << (j - 1))]);
}
}
for(int i = 1; i < (1 << y); i++) {//枚举后一半点集
bool flg = 1;
for(int j = 1; j <= y; j++) {
if(!((i >> (j - 1)) & 1)) continue;
if(((out[j + x] >> x) & i) != (i ^ (1 << (j - 1)))) {//判断是不是完全图
flg = 0;
}
}
if(!flg) continue;
int S = (1 << x) - 1;
for(int j = 1; j <= y; j++) {
if(!((i >> (j - 1)) & 1)) continue;
S &= out[j + x];//求出点交集
}
ans = max(ans, f[S] + __builtin_popcount(i));//更新答案
}
return 0;
}
3 Bron-Kerbosch 算法
BK 算法的核心其实是搜索,我们每次加入一个点,看当前点集能否构成最大团。如果能继续递归,否则就进行回溯。显然这样做是非常不优的,而 BK 算法其实就是对它的优化。该算法复杂度(据说)是
BK 算法制定了三个集合
现在我们进行如下操作:
- 初始化
为空集, 为所有点。 - 将
中顶部元素 取出,设 表示所有与 相邻的点,则递归集合 。- 此时如果
均为空,则 中的所有元素构成一个极大团。
- 此时如果
- 将
从 中删去,加到 中。 - 重复
步,知道 集合为空。
其实整体思路还算明确,我们分析一下:
- 当我们取出一个点加入
中时,此时有可能加入极大团的点一定需要在 中,因此 要和它取交集。 的作用只是为了判重,避免找到重复的极大团。当取出一点后, 里面剩下的可能会与当前极大团重复的点也一定要在 中,因此也要取交集。- 当
均为空时,则这个团没有被找到过,且无法再被扩展,自然就是一个新的极大团。
最后我们还有一个小优化:当我们从
最后代码如下:
int ans;
int g[45][45];
int r[45][45], p[45][45], x[45][45];
void dfs(int d, int R, int P, int X) {
if(!P && !X) {//P,X 集合都不为空
ans = max(ans, R);//则 R 中元素构成极大团
return ;
}
int u = p[d][1];//关键点优化
for(int i = 1; i <= P; i++) {
int v = p[d][i];
if(g[u][v]) continue;//相邻不必在此递归
for(int j = 1; j <= R; j++) {
r[d + 1][j] = r[d][j];
}
r[d + 1][R + 1] = v;//取并集
int nP = 0, nX = 0;
for(int j = 1; j <= P; j++) {//取交集
if(g[v][p[d][j]]) p[d + 1][++nP] = p[d][j];
}
for(int j = 1; j <= X; j++) {//取交集
if(g[v][x[d][j]]) x[d + 1][++nX] = x[d][j];
}
dfs(d + 1, R + 1, nP, nX);
p[d][i] = 0, x[d][++X] = v;//加入 v
}
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 单元测试从入门到精通
· 上周热点回顾(3.3-3.9)
· winform 绘制太阳,地球,月球 运作规律