团(clique): 一个图中的完全子图
极大团(maximal clique): 不被另一个更大的团包含的团
最大团(maximum clique): 含有顶点最多的团
算法思想: 定义两个集合Q和SUBG,Q是一个团包含的的顶点,SUBG是与Q中所有顶点都相邻的顶点集合。将SUBG中任意一个点q加入Q中后,Q是一个更大的团。之后重新计算SUBG(保留SUBG中与q相邻的顶点)。当SUBG为空集时,Q是一个极大团。
剪枝策略:
- 将SUBG划分为两个互不相交的子集FINI和CAND。FINI为已经扩展过的顶点集合,CAND为SUBG中的其余顶点。在CAND中选出要加入Q的顶点q之后,不用将FINI与q的相邻部分作为新的SUBG(重新计算后的SUBG是CAND与q相邻的部分)。这样避免了交换顶点顺序导致的同一个团的重复计算。
- 在从CAND选出一个q加入Q之前,从SUBG中选出任意一个顶点u,对于CAND中u的相邻顶点nbr(u)可以不再扩展。因为包含nbr(u)的极大团中,必定包含u。为了尽可能减小CAND,我们要选出这样的u,令u与CAND中尽可能多的顶点相邻。
算法的证明和时间复杂度的计算在E. Tomita, A. Tanaka, H. Takahashi, The worst-case time complexity for generating all maximal cliques and computational experiments, Theoretical Computer Science, 2006 中给出
代码如下,时间复杂度为O(3n/3)
#include <bits\stdc++.h>
using namespace std;
vector<list<int>> graph; //图的邻接链表
bool nbr(int v, int u); //判断u和v是否相邻
void init();
void expand(int d, int index_subg, int index_cand);
int choose(int d, int index_subg, int index_cand);
int n, m; //顶点数和边数
int *subg[1010], *cand[1010], *result[1010]; //sbug是候选顶点,cand是需要扩展的顶点,subg-cand是已扩展过的顶点
int clique_count = 0; //极大团的个数
int main()
{
init();
for (int i = 0; i < n; ++i) //初始时subg和cand包括所有顶点
{
subg[0][i] = i;
cand[0][i] = i;
}
expand(0, n, n);
printf("共有%d个\n", clique_count);
return 0;
}
bool nbr(int v, int u)
{
for (auto i = graph[v].begin(); i != graph[v].end(); ++i)
{
if (*i == u)
return true;
}
return false;
}
void init()
{
FILE* f = fopen("data.txt", "r");
fscanf(f, "%d %d", &n, &m); //读入顶点数和边数
graph.resize(n);
for (int i = 0; i < 1010; ++i)
{
subg[i] = (int*)malloc(n * sizeof(int));
cand[i] = (int*)malloc(n * sizeof(int));
result[i] = (int*)malloc(sizeof(int)*n);
}
for (int i = 0; i < m; ++i) //创建邻接链表
{
int t1, t2;
fscanf(f, "%d %d", &t1, &t2);
graph[t1].push_back(t2);
}
}
void expand(int d, int index_subg, int index_cand) //d是搜索深度,index_subg是subg集合的元素个数,subg[d][0]~subg[d][index_subg-1]是这一次的subg集合
{
if (index_subg == 0)
{
for (int i = 0; i < d; ++i)
printf("%d ", result[i][0]);
++clique_count;
printf("\n");
}
else
{
int u = choose(d, index_subg, index_cand); //选一个顶点u,让cand中u的相邻顶点尽可能多
int iterate_num = index_cand;
int offset = 0;
for (int i = 0; i < iterate_num; ++i)
{
int now = cand[d][0+offset]; //当前扩展的点(加入团中的点)
if (nbr(now, u)) //要扩展的点不包括u的相邻点
{
++offset;
continue;
}
result[d][0] = now; //加入结果集合
int index_subg_next = 0, index_cand_next = 0;
for (int j = 0; j < index_subg; ++j)
{
if (nbr(subg[d][j], now)) //找出当前subg中的顶点和顶点now相邻的部分
subg[d + 1][index_subg_next++] = subg[d][j];
}
for (int j = 0; j < index_cand; ++j)//找出当前cand中的顶点和顶点now相邻的部分
{
if (nbr(cand[d][j], now))
cand[d + 1][index_cand_next++] = cand[d][j];
}
expand(d + 1, index_subg_next, index_cand_next); //向下一层搜索,subg[d]变成subg[d+1]
for (int j = 0; j < index_cand - 1; ++j) //已经用过的点从cand中移除
cand[d][j] = cand[d][j + 1];
--index_cand;
}
}
}
int choose(int d, int index_subg, int index_cand)
{
int max = 0, res = cand[d][0];
for (int i = 0; i < index_subg; ++i) //在subg中寻找一个点
{
int cnt = 0;
for (int j = 0; j < index_cand; ++j) //计算点i与cand中的多少个点相邻
{
if (nbr(subg[d][i], cand[d][j]))
++cnt;
}
if (cnt > max)
{
max = cnt;
res = cand[d][i];
}
}
return res;
}