定義
最大獨立集:當且僅當對於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算法):
在該算法中有四個集合: 。其中:
:目前已經在團中的結點的集合(temporary result)
:可能在團中的結點的集合(possible candidates)
:不被考慮的結點的集合(excluded set,在樸素的Bron Kerbosch算法表現爲:包含該結點的最大團已經被搜索)
:結點 的所有直接鄰居(有邊直接相連)結點的集合。其中, 。
該算法文字描述爲:從 中選出一個結點 找包含 的最大團。將 放入集合 中,並將不在 的結點從 和 中移出。從剩下的 中再選出一個結點,重複上述操作。直到 成爲空集。此時,若 也爲空,則 是新的最大團(如果 不爲空,則說明 是已經找到的最大團的一個子集)。然後,回溯到上一個選擇的結點,並將集合 也恢復到原來的狀態,同時,將本次選擇的結點從 中移出,加入 ,從 中選出下一個結點重複上述操作。如果 爲空集,則返回到上一級。對於下圖所示的圖,使用該算法求解極大完全圖的步驟如下:
Pivot 優化的 Bron-Kerbosch算法
樸素的Bron Kerbosch算法在有很多非最大團的情況下,效率不是很好。因爲,該算法會遍歷所有的團。該算法的其中一個變種是加入軸(pivot),基本思想是選擇一個結點 軸,最大團要麼包含 ,要麼包含 的非直接鄰居。
僞代碼:
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(生成子圖)。
生成子圖:對於圖 , 若 滿足 ,則 爲 的生成子圖。簡單來說,生成子圖的頂點與原圖的頂點一樣,但邊是原圖邊的子集。
誘導子圖:對於圖 , 若 滿足 ,並且 當且僅當 ,則 爲 的誘導子圖。也就是說,頂點可以少,一旦選定了頂點,則頂點對應的所有的邊都要選進來。
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 ] )。
使用退化序優化的思路
如果算法在從 集合中選結點時,按照退化序(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;
}
對於最大完全子圖(最大獨立集)問題(僅僅求最大),還可以進一步剪枝優化(如,預估後續搜索能否超過當前最大值),本文暫不討論,可參考相關資料(如:最大獨立集求解、最大團等)
對於二分圖的最大獨立集問題,有更好的解法,感興趣的可搜索相關資料。
參考資料: