极大团(maximal clique)算法:Bron-Kerbosch算法

不了解极大团(maximal clique)的,请看极大团这篇文章。
该算法是由Coen Bron和Joep Kerbosch在1973提出的,论文原文
参考资料:
Bron-Kerbosch算法视频介绍
极大团算法
当给出一个图之后,我们应该怎么去找出其中的极大团呢?


寻找极大团的简单思想就是:
1、生成原始图的所有子图(可能有2n-1个子图,n表示结点个数);
2、判断这些子图是不是团;
3、将不是极大团的团都删除;
在我们上面给出的图中,会有许多的子图,例如:
子图

这里没有列出所有的子图,只有部分。
在这些子图里面,我们会发现有些并不是团,例如{1、2、3}组成的图,因为2和3之间并不相连,所以这样的图不符合要求,将其去掉。

剩下的所有子图,都是满足了团的要求的图。最后,其中还有许多,不是极大团的,例如{0、1}就不是极大团,因为有{0、1、2}的存在。这些不是极大团的团,我们也要去掉。
极大团

现在剩下的就是我们要找的极大团。

Bron-Kerbosch算法(Version 1)

这个算法主要是构造了三个集合,我们假设:
R集合记录的是当前极大团中已经加入的点。
P集合记录的是可能还能加入的点(也就是与R集合中所有点都有边存在的点,这样加入后,才会构成团)
X集合记录的是已经加入过某个极大团的点(作用是判重,因为会从每个结点开始,枚举所有的团,如果不对已经加入某个极大团的点,进行标记,可能会有重复的极大团出现)
伪代码如下:

Bron-Kerbosch Algorithm(Version 1)
R={}   //已确定的极大团顶点的集合
P={v}  //未处理顶点集,初始状态是所有结点
X={}   //已搜过的并且属于某个极大团的顶点集合

BronKerbosch(R, P, X):
   if P and X are both empty:
       report R as a maximal clique
   for each vertex v in P:
       BronKerbosch1(R ⋃ {v}, P ⋂ N(v), X ⋂ N(v))
       P := P \ {v}
       X := X ⋃ {v}

基础的Born_Kerbosch算法:
1、对于每一个在集合P中的点v,我们把v加入集合R(集合P中的点与集合R中所有的点都是连接的,这样加入v,保证集合R依然是一个团),对在P集合中且与点v相连的这部分集合中寻找下一个可能加入R集合的点(意思就是更新P集合,使P集合中的点依旧可以和R集合中的点相连接,因为这里新R集合新加入了v,所以只要是原P集合中且与v相连的,那这些结点就是与新的R集合中所有结点都相连)。
2、回溯时我们把v从P集合中移出,加入X集合代表当前状态下对包含点v的极大团已经计算完毕了。
3、R集合为极大团的时候,必须要满足P与X都是空的。P存放的是还可能加入R集合的点,P集合为空代表没有点还能再加入到R集合当中,而X集合存放的是已经完成极大团计数的点,而且X集合中的点必然是与所有R集合中的点都有边存在的(因为我们每次向下进行dfs的时候,还对P集合和X集合分别进行取与R集合内都相邻的操作来保证,而且加入X集合中的点,就是从R集合中取出来的,当然会和R集合中的所有结点都有边),也即X集合中点必然可以与R集合构成极大团。如果X集合不是空的的话,可以把X集合中的点加入R集团,此时R集团依然是团,结点数比刚才增加了,说明刚才的R集合就不是极大团,那么说明R集合中的极大团是在之前计算包含X集合中的点的极大团的时候已经计算过了的,故当且仅当P、X都为空集合的时候R才是一个极大团

算法的实例:


Bron-Kerbosch Algorithm(Version 1)

在最开始中,集合P会初始化为所有的结点,其他集合为空集。


Bron-Kerbosch Algorithm(Version 1)

首先,我们将集合P中的第一个结点,1号结点,放入到R中。这个时候,我们的遍历要进入下一层了,所以要更新集合P和集合X,这里集合X没有变化,依然是空集。集合P中的点要是和集合R中的所有结点都是连接的,显然我们只需要找到,原来的P集合中是1号集合的邻居结点的那些点就行了,这里就是{2、3},保证了P集合中的每个结点都可以和R集合中的结点想连接,其实X集合我们也会执行同样的操作,以保证X集合中的结点都是和R集合中的所有结点连接的。
Bron-Kerbosch Algorithm(Version 1)

同样的,我们继续将P集合中的结点放入R集合,这次放入2号结点,R集合变为{1、2},P集合取原P集合中且和2号结点相连接的结点,P集合就变为{3}。


Bron-Kerbosch Algorithm(Version 1)

最后同样的我们将P集合中的结点,放入R集合。此时P集合X集合都为空集,说明这个团已经不能再扩充了,所以{1、2、3}就是一个极大团。
Bron-Kerbosch Algorithm(Version 1)

  我们从1号结点开始,先遍历了{1、2},所以还要从1开始遍历{1、3}。按照DFS的规则,会先退回到上一层,也就是R={1、2},P={3}这一层,我们会将v结点,也就是这一层里操作的结点,在这里是3号结点放入X集合中,表示它已经参与了极大团的构。在这一层里最后就变为了R={1、2},P={},X={3},因为X不为空,且P为空,所以R={1、2}不是一个极大团,然后我们要回溯到上一层了,也就是R={1}、P={2、3}、X={}这一层,再将此时的操作结点2号结点,放入X中,表示它已经属于某个极大子图,这时的三个集合变为了R={1},P={3},X={2}。

  只要P集合中,还有元素,我们就会一直执行将P集合中的元素加入R集合中的操作,所以这里将3号结点加入,并且更新了P集合和X集合(就是保证P,X中的结点和R集合中的所有结点都连接)进入下一层,也就是图中所示的样子。

  在这里X={2},不为空,所以{1、3}不是极大团,因为X集合中的在加入R集合后,都是会让R集合满足极大团的,显然{1、2、3}比{1、3}要大,所以只要X不为空,R就不会是极大团。


Bron-Kerbosch Algorithm(Version 1)

  在完成判断后,退回到上一层,也就是图中的R={1},P={2、3},X={}的这一层,不过刚才有说过,这一层其实在经过将P中的2号结点加入到R后,以及后续的程序,已经变成了R={1},P={3},X={2},此时我们将正在操作的结点3号结点放入到X中,变为R={1},P={},X={2、3},此时P中无结点,X中有两个结点。说明以1开始的遍历操作以及全部完成了,我们需要再去寻找其他结点开始的遍历。
  退回到上一层,也就是最开始的那一层,即R={}、P={1、2、3、4}、X={}的这一层。把操作的结点,也就是1号结点,放入X中,表示已经对其进行过查询。
  再将P中的下一个结点,也就是2号结点,放入R中,同样的我们要更新P和X,保证P和X中的结点,和R中的所有结点,都是连接的,进入下一层,得到的如图所示,R={2}、P={3、4}、X={1},表示我们要由2号结点开始,寻找极大团了。


Bron-Kerbosch Algorithm(Version 1)

  同样的,我们将P中的3号结点放入R中,同时更新P和X,因为P中的4号结点,不与3号结点相连,所以P集合变为空集,X依然为{1}。此时我们发现P为空,但是X={1},所以R={2、3}并不是一个极大团。这说明由2开始,再走3号结点这条路是行不通的,所以需要退回到上一层,也就是R={2}、P={3、4}、X={1}这一层,再将3号结点放入X中,表示已经搜过了,此时变为R={2},P={4},X={1、3}。下面就要走由2号结点开始,在到4号结点的这条路了。
Bron-Kerbosch Algorithm(Version 1)

将P中的4号结点放入R中,同时更新P和X,因为1和3都不与4相连,所以X集合变为空集,此时P集合也为空集,所以R={2、4}是一个极大团。


Bron-Kerbosch Algorithm(Version 1)

我们的遍历当然还要继续,我们现在找了从最初的图中,由1开始,已经由2开始来寻找极大团,当然还要从3开始以及从4开始,寻找极大团,结果如图所示。由3号结点开始,R={3}、P={}、X={1、2};由4号结点开始,R={4}、P={}、X={2}。X集合都不为空,所以R集合不是极大团。
Bron-Kerbosch Algorithm(Version 1)

每一次遍历中,所生成的集合如上图的右下角所示,可以很明显的看出两个极大团。
Bron-Kerbosch Algorithm(Version 1)完整的代码为:
int some[maxn][maxn];  
//some表示P集合,第一个[maxn]表示所在的深度,后一个就是P集合中的某个结点的位置
int none[maxn][maxn]; //表示X集合,其他和some同理
int all[maxn][maxn];    //表示R集合,其他同理
int mp[maxn][manx];    
void dfs(int d, int an, int sn, int nn)
//d为搜索深度,an、sn、nn分别为all(R)、some(P)、none(X)集合中顶点数,
{
    if(!sn && !nn) ++ S;  //sn==0,nn==0时,是一个极大团,S为极大团数量
    for(int i = 0; i < sn; ++i)  //遍历P中的结点,sn==0时,搜索到终点
    {
        int v = some[d][i];  //取出P中的第i个结点
        for(int j = 0; j < an; ++j)  
         {
             all[d+1][j] = all[d][j];  
         }
        all[d+1][an] = v;
        //这里是将取出的v结点,添加到R集合中,当然是添加到下一个深度的R集合。
        int tsn = 0, tnn = 0;
        //用来分别记录下一层中P集合和X集合中结点的个数
        for(int j = 0; j < sn; ++j)  if(mp[v][some[d][j]])  some[d+1][tsn++] = some[d][j];
        //更新P集合(当然是下一层的P集合),保证P集合中的点都能与R集合中所有的点相连接
        for(int j = 0; j < nn; ++j)  if(mp[v][none[d][j]])   none[d+1][tnn++] = none[d][j];
        //更新X集合(当然是下一层的X集合),保证X集合中的点都能与R集合中所有的点相连接
        dfs(d+1, an+1, tsn, tnn);
        //递归进入下一层
        some[d][i] = 0, none[d][nn++] = v;
        //完成后,将操作的结点,放入X中,开始下面的寻找。
    }
}

Bron-Kerbosch 算法(Version 2)

  在上面这个方法中,我们进行了许多不必要的判断,例如在我们找到了极大团{1、2、3}之后,依然去对{1、3},{2、3},{3}这些团进行了判断,然而这些显然不是极大团。所以现在考虑的是对其进行优化,使程序不用进行不必要的递归。

  当我们将一个结点u,放入到R集合后,再取下一个结点,取的必然是u的邻居结点(因为再更新P和X时,会将不是邻居结点的结点都过滤掉)。通俗的讲就是如果取完u之后我们再取与u相邻的点v也能加入到极大团,那么我们只取u就好了,因为我们由u开始递归,已经找到了u及其邻居结点v等等结点构成的极大团了,没有必要再去从v开始寻找极大团,这会增加不必要的计算。至于v可能可以其他结点构成另一个极大团,如果这个极大团包括了u,那么由u开始就已经找到了这个极大团了;如果这个极大团不包括u,那说明这个极大团里面一定存在和u结点不相连的结点k,那没必要从v开始寻找这个极大团了,从u的非邻居结点k开始,一样可以找到这个极大团。这样对u及其邻居结点构成的极大团,只需要从u开始寻找一次就可以了,接下来就直接从u的非邻居结点k开始寻找极大团,这样可以减少许多不必要的计算。

  例如上面的程序中我们从1开始寻找极大团,找到了由1及其邻居结点构成的极大团{1、2、3},接下来我们就直接从1的非邻居结点4号结点开始寻找极大团,可以找到极大团{4、2},最终所有的极大团都被找到了。其中由2和3开始的这些不必要的计算都被省略,当然在递归的内部,我们也依然使用这种思想。而我们要想进一步减少计算,我们就可以取邻居尽可能多的u,这样让我们要遍历的点尽可能减少,但是其实没必要如此,寻找合适的u也会减少一定的效率。
设定关键点 pivot vertex u,只对关键点u自身和u的非邻居结点进行查找。
伪代码如下:

Bron-Kerbosch Algorithm(Version 2)
R={}   //已确定的极大团顶点的集合
P={v}  //未处理顶点集,初始状态是所有结点
X={}   //已搜过的并且属于某个极大团的顶点集合

BronKerbosch(R, P, X):
   if P and X are both empty:
       report R as a maximal clique
   choose a pivot vertex u in P ⋃ X
   for each vertex v in P \ N(u):   
       BronKerbosch1(R ⋃ {v}, P ⋂ N(v), X ⋂ N(v))
       P := P \ {v}
       X := X ⋃ {v}

下图是Bron-Kerbosch Algorithm(Version 1)和Bron-Kerbosch Algorithm(Version 2)的对比


Bron-Kerbosch Algorithm(Version 2)

在右边是没有优化的方法,我们可以看到递归的次数非常的多,有太多不必要的计算。
左边就是优化后的方法,下面讲解其中的具体步骤:
  这里每次会选择邻居结点多的那个结点当作Pivot,所以一开始选择4号结点做Pivot,我们也从4号结点开始进行递归,所以进入下一层,得到R={4},P={1、2、3、5、6},X={},P中的结点经过筛选以后,将7号结点去除。
  我们从P中选2号结点(因为2号结点邻居结点多),同时也将2号结点作为这一层的Pivot 结点,同时更新P集合。这一层变为{4、2},{1、3、5},{}。
  我们从P中选1号结点(因为1号结点邻居结点多,当然这里向图中一样选5号结点也是可以的),同时也将1号结点作为这一层的Pivot 结点,同时更新P集合。这一层变为{4、2、1},{3},{}。
  最后将P集合中的3号结点,放入R集合,此时P集合和X集合都变为空集,所以{4、2、1、3}是极大团。到目前位置,我们的操作步骤和优化之前是一样的,只是这里每次都是选取的邻居最多的结点放入(这样是为了尽可能的减少递归次数,其实按照原来的顺序也是可以的)。和之前一样的原因是,这是第一轮递归,我们在每一层放入R集合的结点,就是该层的Pivot 结点。
  因为P={},所以退回到上一层,也就是{4、2、1},{3},{},然后将P中的结点3放入到X中,表示在这条路上,已经探索过这个结点了。于是变为{4、2、1},{},{3},P集合为空集,所以退回到上一层,也就是{4、2},{1、3、5},{},将1号结点移入X集合中,变为{4、2},{3、5},{1},然后在P中选取下一个结点加入R集合中,在这一层中Pivot结点是1号结点(图中为5号结点,所以多了一步,就是{4、2、3},{},{}这一步),所以凡是在P集合中并且是Pivot结点(也即是这里的1号结点)的邻居结点的,从P集合中移除。比如这里3号结点在P集合中,且3号结点是1号结点的邻居结点,所以将3号结点移除(因为我们已经通过1号结点,找到了同时包含1和3的极大团)。
  最后P集合中只有5号结点了,所以将5号结点放入R集合中,并且更新P和X,变为{4、2、5},{},{},此时P和X都为空集,所以{4、2、5}是一个极大团。由4、2开始的这一层,我们已经探索完了,所以要退回到{4},{1、2、3、5、6},{}这一层。
  在这一层里,Pivot结点是2号结点,所以P集合中和2是邻居结点的那些结点将不会在放入R集合中,所以将P集合中的6放入R集合中,更新P集合与X集合,得到{4、6},{},{},所以{4、6}是一个极大团。
  现在由4开始的这条路径,我们也全部都走完了,所以需要退回到最开始的那一层,也就是{},{1、2、3、4、5、6、7},{},将4号结点放入X集合中,找到P集合中不和4号结点相连的那些结点,这里只有7号结点,所以将7号结点放入到R集合中,同时更新P和X,得到{7},{5},{},这里只有5号结点了,显然选择5号作为Pivot,最后变为{7、5},{},{},所以{7、5}就是一个极大团。
  现在,已经找出了图中所有的极大团,而且从图中可以明显的看出,我们递归的次数是比原来要少很多的。
Bron-Kerbosch Algorithm(Version 2)完整的代码为:


#include <cstdio>
#include <cstring>
using namespace std;
const int maxn = 130;
bool mp[maxn][maxn]; //表示结点之间的连接
int some[maxn][maxn], none[maxn][maxn], all[maxn][maxn];//分别是P集合,X集合,R集合
int n, m, ans; //n表示结点数,m表示边数,ans表示极大团数量
void dfs(int d, int an, int sn, int nn)
{
    if(!sn && !nn) ++ans;
    int u = some[d][0];  //选取Pivot结点
    for(int i = 0; i < sn; ++i)
    {
        int v = some[d][i];
        if(mp[u][v]) continue; 
        //如果是邻居结点,就直接跳过下面的程序,进行下一轮的循环。显然能让程序运行下去的,只有两种,一种是v就是u结点本身,另一种是v不是u的邻居结点。
        for(int j = 0; j < an; ++j)  
         {
             all[d+1][j] = all[d][j];  
         }
        all[d+1][an] = v;
        int tsn = 0, tnn = 0;
        for(int j = 0; j < sn; ++j)  if(mp[v][some[d][j]])  some[d+1][tsn++] = some[d][j];
        for(int j = 0; j < nn; ++j)  if(mp[v][none[d][j]])  none[d+1][tnn++] = none[d][j];
        dfs(d+1, an+1, tsn, tnn);
        some[d][i] = 0, none[d][nn++] = v;
        if(ans > 1000) return;    // 极大团数量超过1000就不再统计
    }
}

int work()
{
    ans = 0;
    for(int i = 0; i < n; ++i) some[1][i] = i+1;
    dfs(1, 0, n, 0);
    return ans;
}

int main()
{
    while(~scanf("%d %d", &n, &m))
    {
        memset(mp, 0, sizeof mp);
        for(int i = 1; i <= m; ++i)
        {
            int u, v;
            scanf("%d %d", &u, &v);
            mp[u][v] = mp[v][u] = 1;
        }
        int tmp = work();
        if(tmp > 1000) puts("Too many maximal sets of friends.");
        else printf("%d\n", tmp);
    }
    return 0;
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,425评论 4 361
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,058评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,186评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,848评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,249评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,554评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,830评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,536评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,239评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,505评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,004评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,346评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,999评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,060评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,821评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,574评论 2 271
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,480评论 2 267

推荐阅读更多精彩内容

  • 1)这本书为什么值得看: Python语言描述,如果学的Python用这本书学数据结构更合适 2016年出版,内容...
    孙怀阔阅读 12,284评论 0 15
  • 一些概念 数据结构就是研究数据的逻辑结构和物理结构以及它们之间相互关系,并对这种结构定义相应的运算,而且确保经过这...
    Winterfell_Z阅读 5,531评论 0 13
  • 目录 0.树0.1 一般树的定义0.2 二叉树的定义 1.查找树ADT 2.查找树的实现2.1 二叉查找树2.2 ...
    王侦阅读 6,988评论 0 3
  • 1.设计步骤 step1.分析需求谁是使用者,将如何使用。5W1H——who what where when wh...
    王侦阅读 1,016评论 0 1
  • 48期88班学员109-西安-流连 毕业感言:爱上理财,爱上未来的你 时光冉冉,转瞬即逝,回想六月,幸运的我在某个...
    自由自在_小牛阅读 222评论 0 3