最大团

1 问题引入

给定一个无向图,定义如下概念:

  • 团:节点之间两两相连的子图。
  • 极大团:不能再继续扩大的团。
  • 最大团:一张无向图中最大的极大团。

现在的问题是怎样求出一个图的最大团。实际上这个问题是 NP-hard 的,也就是没有多项式解法。但是有复杂度较为优秀的指数级算法。

主流算法有两种:状压 dp 和 Bron-Kerbosch 算法。后一种算法复杂度更低,前一种较为简单。

2 状压 dp 解法

先考虑朴素状压怎么办。记录下每一个点向外扩展的点集 Pi,然后 O(2n) 枚举子图,然后检查每一个点 i,看 Pi 是否包含这个子图中的所有点。这样的复杂度是 O(2nn) 的。

接下来我们使用状压 dp + meet in the middle 优化复杂度,可以达到 O(2n/2n)

考虑枚举前一半的点集,对于这些点构成的任意集合 S,求出 f(S) 表示 S 点集中最大团的大小。然后枚举后一半的点集,对于任意一个集合 T,求出 T 中每一个点的 Pi 的交集 R。然后判断 T 是不是团,如果是,则用 cnt(T)+f(R) 来更新答案(其中 cnt(T)T 子集大小)。

那么整体代码如下:

#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 算法其实就是对它的优化。该算法复杂度(据说)是 O(3n/3),证明我也不会,但是它肯定是比上面的状压做法优的。

BK 算法制定了三个集合 R,P,XR 表示当前正在找的极大团中的点,P 表示有可能加入极大团中的点,X 表示已经在极大团中的点。

现在我们进行如下操作:

  • 初始化 R,X 为空集,P 为所有点。
  • P 中顶部元素 u 取出,设 W(u) 表示所有与 u 相邻的点,则递归集合 Ru,PW(u),XW(u)
    • 此时如果 P,X 均为空,则 R 中的所有元素构成一个极大团。
  • uP 中删去,加到 X 中。
  • 重复 2,3 步,知道 P 集合为空。

其实整体思路还算明确,我们分析一下:

  • 当我们取出一个点加入 R 中时,此时有可能加入极大团的点一定需要在 W(u) 中,因此 P 要和它取交集。
  • X 的作用只是为了判重,避免找到重复的极大团。当取出一点后,X 里面剩下的可能会与当前极大团重复的点也一定要在 W(u) 中,因此也要取交集。
  • P,X 均为空时,则这个团没有被找到过,且无法再被扩展,自然就是一个新的极大团。

最后我们还有一个小优化:当我们从 P 中取出一点 u,如果 vu 相连,则它在本层递归中会被加入到极大团中;而在下一层递归我们仍然有可能取出 v 加入极大团,这样会造成重复。所以在这一层,我们只取 u 即可,到下一层再取和 u 相连的点 v。这被称为关键点优化。

最后代码如下:

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
	}
}
posted @   UKE_Automation  阅读(99)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 单元测试从入门到精通
· 上周热点回顾(3.3-3.9)
· winform 绘制太阳,地球,月球 运作规律
点击右上角即可分享
微信分享提示