最大独立集和最大完全子图

定义

最大独立集:当且仅当对于U 中任意点u 和v所构成的边(u , v) 不是G 的一条边时,U 定义了一个空子图。当且仅当一个子集不被包含在一个更大的点集中时,该点集是图G 的一个独立集(independent set ),同时它也定义了图G 的空子图。最大独立集是具有最大尺寸的独立集(摘自:百度百科:最大独立集)。

最大完全子图:图中任意两顶点都直接相连的图,称为完全图,也称全连接图。图G的子图若为完全图,则称为图G的完全子图。最大的完全子图为最大完全子图。最大完全子图又称最大团,也称最大完备子图。

容易知道(画个图想想便知),一个图的最大独立集,等价于其补图的最大完全子图。

对应的,也有极大独立集、极大完全子图的概念。求解最大完全子图和极大完全子图,是经典的NP完全问题,目前只能使用回溯策略求解。但单纯的回溯效率很低,Bron-Kerbosch算法于1973年被提出,大大加快了搜索效率,之后又有各种优化。本文将对此进行简单介绍。

朴素的 Bron-Kerbosch算法

伪代码:

BronKerbosch1(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}

解释(摘自:百度文库:Bron_Kerbosch算法):

在该算法中有四个集合:R,P,X,N(v) 。其中:

        R :目前已经在团中的结点的集合(temporary result)

        P :可能在团中的结点的集合(possible candidates)

        X :不被考虑的结点的集合(excluded set,在朴素的Bron Kerbosch算法表现为:包含该结点的最大团已经被搜索)

        N(v) :结点 的所有直接邻居(有边直接相连)结点的集合。其中,P\cup X=N(v)

该算法文字描述为:从 P 中选出一个结点 v 找包含 v 的最大团。将 v 放入集合 R 中,并将不在 N(v) 的结点从P 和 X 中移出。从剩下的 P 中再选出一个结点,重复上述操作。直到 P 成为空集。此时,若 X 也为空,则 R 是新的最大团(如果 不为空,则说明 是已经找到的最大团的一个子集)。然后,回溯到上一个选择的结点,并将集合 R,P,X也恢复到原来的状态,同时,将本次选择的结点从 P 中移出,加入 X ,从 P 中选出下一个结点重复上述操作。如果 P 为空集,则返回到上一级。对于下图所示的图,使用该算法求解极大完全图的步骤如下:

      

      

 

Pivot 优化的 Bron-Kerbosch算法

朴素的Bron Kerbosch算法在有很多非最大团的情况下,效率不是很好。因为,该算法会遍历所有的团。该算法的其中一个变种是加入轴(pivot),基本思想是选择一个结点 u 轴,最大团要么包含 u ,要么包含 u 的非直接邻居。

伪代码:

BronKerbosch2(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):
           BronKerbosch2(R ⋃ {v}, P ⋂ N(v), X ⋂ N(v))
           P := P \ {v}
           X := X ⋃ {v}

前述示例的执行步骤:

      

 

退化序优化的 Bron-Kerbosch算法

预备知识

Induced subgraph

在介绍 induced subgraph(诱导子图)前,先回顾下 spanning subgraph(生成子图)。

生成子图:对于图 G(V,E), 若 G^{'}(V^{'},E^{'}) 满足 V^{'}=V, E^{'}\subseteq E,则G^{'} 为 G 的生成子图。简单来说,生成子图的顶点与原图的顶点一样,但边是原图边的子集。

诱导子图:对于图 G(V,E), 若 G^{'}(V^{'},E^{'}) 满足 V^{'}\subseteq V,并且 e \in E^{'} 当且仅当 u,v \in V^{'}, e\in E,则G^{'} 为 G 的诱导子图。也就是说,顶点可以少,一旦选定了顶点,则顶点对应的所有的边都要选进来。

Degeneracy

本部分翻译自 Wiki: Degeneracy

Degeneracy(退化):如果图的结点存在一种序列,使得每个结点和它所有前驱形成的诱导子图中,该结点度不超过K,则称该图为 k-degenerate graph(K-退化图;可以找到最小的K值,使得原图满足K-退化图,此时对应的序列即为退化序(degeneracy ordering)。K-退化图又称 K-诱导图。容易知道,树结构为 1-诱导图。

      

K-退化图的K值,与 K-core number(K核值)是相等的,也与 coloring number(染色数)可能一致。这里大致说一说 K-core。K-core是原图的子图,该子图满足条件:任意一个顶点的度数都不小于 K。上述图中,展示的便是一个 2-degenerate graph,以及对应的 2-core(黄色部分)。怎么理解呢?反复删除度数小于2的点,最终保留下来的便是 2-core。其实这一反复删除的操作过程便是求解 K-degenerate graph和 K-core 的逆向过程(下文将详细介绍)。K-core在很多领域(如:社交网络、生物信息学等)中都有用到(可参考相关资料详细了解其应用价值)。

求解算法:

初始化结果序列 L。

定义d(v)为:顶点v所有不在 L中的邻接顶点个数。初始化的 d(v)即使v的度数。

定义D(i)为:所有度数为 i 的顶点集合。初始化D(i)。

初始化k=0。

重复下列计算:

    依次扫描D(0), D(1), ...直至D(i)不为空。

    令k=max(k,i)。

    从D(i)中拿出一个顶点v,将v加入到 L的最前面,并将D(i)中删除v。

    更新d数组和D数组,即:对于v的所有邻居u,对应的d(u)值都减1;D数组根据d值更新。

最终得到的K即是最小的K值,L序列即是可行的退化序列,K-core 就是第一次对D(K)取顶点前尚未进入L的顶点构成的子图(其实求解过程可以得出各个 i-core [ i=2,3,...K ] )。

使用退化序优化的思路

如果算法在从 P 集合中选结点时,按照退化序(degeneracy ordering)选择, 能够减少算法调用的次数,从而提高效率。其中,退化序能在线性复杂度内计算完成。但,该变种(严格来说,这种变化并没有改变算法,只是在算法执行的时候选择能加快速度的序列)会有退化的时候。

伪代码:

BronKerbosch3(G):
       P = V(G)
       R = X = empty
       for each vertex v in a degeneracy ordering of G:
           BronKerbosch2(R ⋃ {v}, P ⋂ N(v), X ⋂ N(v))
           P := P \ {v}
           X := X ⋃ {v}

前述示例的执行步骤:

      

在Pivot 优化和退化序优化后,Bron-Kerbosch算法的求解效率得到极大的提升。

C++代码实现(代码借鉴自:无向图的极大团、最大团(Bron-Kerbosch算法),笔者加入了degeneracy ordering优化代码):

#include <iostream>
#include <fstream>
#include <cstring>
#include <string>
#include <sstream>
#define MAX_N 1500
 
using namespace std;

bool mp[MAX_N][MAX_N];
int some[MAX_N][MAX_N], none[MAX_N][MAX_N], all[MAX_N][MAX_N];
int n, m, ans;
int L[MAX_N], degeneracyOrdering[MAX_N];
int degree[MAX_N], DegreeVertex[MAX_N][MAX_N], numberVertex[MAX_N];
int k, L_number;
ifstream input;
ofstream output;
int kk;
stringstream ss;

/* d: the depth;
   an: the number of all_set;
   sn: the number of some_set;
   nn: the number of none_set.
*/
void dfs(int d, int an, int sn, int nn) {
	/* 
	cout<<"DFS: { ";
	for (int i = 0; i < an; i++) cout<<all[d][i]<<" ";
	cout<<"} { ";
	for (int i = 0; i < sn; i++) cout<<some[d][i]<<" ";
	cout<<"} { ";
	for (int i = 0; i < nn; i++) cout<<none[d][i]<<" ";
	cout<<"}\n";
	*/
 
	if(!sn && !nn && an>=kk) {
		ans++;
		for (int i = 0; i < an-1; i++) output<<all[d][i]<<", ";
		output<<all[d][an-1]<<"\n";
		
	}
	int u = some[d][0];
 
	for(int i = 0; i < sn; i++) {
		int v = some[d][i];
		if(mp[u][v]) continue;
		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;
	}
}
 
int getDegeneracy() {
	memset(L, 0, sizeof L);
	memset(numberVertex, 0, sizeof numberVertex);
	memset(DegreeVertex, 0, sizeof DegreeVertex);
	for (int v = 1; v <= n; v++)
		DegreeVertex[degree[v]][numberVertex[degree[v]]++] = v;
	k = 0;
	L_number = 0;
	while (L_number < n) {
		int i = 0;
		while (numberVertex[i]==0) i++;
 
		if (i>k) k = i;
 
		int v = DegreeVertex[i][0];
		L[L_number++] = v;
		degree[v] = -1;
		for (int u = 1; u <= n; u++)
			if (mp[u][v]==1) degree[u]--;
 
		memset(numberVertex, 0, sizeof numberVertex);
		memset(DegreeVertex, 0, sizeof DegreeVertex);
		for (int v = 1; v <= n; v++)
			if (degree[v]>=0)
				DegreeVertex[degree[v]][numberVertex[degree[v]]++] = v;
	}
 
	memset(degeneracyOrdering, 0, sizeof degeneracyOrdering);
	for (int i = 0; i < n; i++)
		degeneracyOrdering[i] = L[n-i-1];
 
	cout<<"The degeneracy ordering is: ";
	for (int i = 0; i < n; i++)
		cout<<degeneracyOrdering[i]<<" ";
	cout<<"\n";
	
	return 0;
}
 
int work() {
	getDegeneracy();
	ans = 0;
	for(int i = 0; i < n; i++) some[1][i] = degeneracyOrdering[i];
	dfs(1, 0, n, 0);
	return ans;
}

int main(int argc, char* argv[]) {
//	input.open("adjacency_2.txt",ios::in);
//	output.open("Independency_2_.txt",ios::app);
//	ss<<"2";
	input.open(argv[1],ios::in);
	output.open(argv[2],ios::app);
	ss<<argv[3];
	ss>>kk;
	if (!input) {
		cout<<"open error!"<<endl;
		exit(-1);
	}
	input>>n>>m;
	memset(mp, 0, sizeof mp);
	memset(degree, 0, sizeof degree);
	for(int i = 0; i < m; i++) {
		int u, v;
		input>>u>>v;
		mp[u][v] = mp[v][u] = 1;
		degree[v]++;
		degree[u]++;
	}
 
	int tmp = work();
	cout<<"The number of set is: "<<tmp<<"\n";
	return 0;
}

 

对于最大完全子图(最大独立集)问题(仅仅求最大),还可以进一步剪枝优化(如,预估后续搜索能否超过当前最大值),本文暂不讨论,可参考相关资料(如:最大独立集求解最大团等)

对于二分图的最大独立集问题,有更好的解法,感兴趣的可搜索相关资料。

 

参考资料:

百度文库:Bron_Kerbosch算法

Bron–Kerbosch算法-最大独立集与最大团

无向图的极大团、最大团(Bron-Kerbosch算法)

 

  • 3
    点赞
  • 27
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

FarmerJohn

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值