最大獨立集和最大完全子圖

定義

最大獨立集:當且僅當對於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算法)

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章