「圖論」Bron-kerbosch演算法

_yuen發表於2024-07-23

7.21晚上加賽 T2.七負我,做這題找到了性質發現需要求最大團,不會,爆搜,打假了,賽後改,對了,但時間複雜度大爆炸,看下發題解,有這麼一句話:於是學習了一下。


Bron-kerbosch演算法-求圖的最大團,極大團

概念:

  • :每個頂點都兩兩相連(又叫完全子圖)
  • 極大團:沒有被包含在其他團中的團
  • 最大團:頂點數最多的極大團



演算法過程:

過程:

我們維護三個集合 \(R、P、X\)\(R\) 表示當前正在找的極大團裡的點,\(P\) 表示有可能加入當前在找的極大團裡的點,\(X\) 表示已經找到的極大團中的點(用來判重),進行以下過程:

  1. 初始化 \(R、 X\) 為空集,\(P\) 為包含所有點的集合;

  2. \(P\) 中頂部元素 \(u\) 點取出,(設 \(Q(u)\) 為所有與 \(u\) 相鄰的點)遞迴集合 \(R ∪{u},P ∩ {Q(u)},X ∩ {Q(u)}\)

    • 在遞迴的過程中如果集合 \(P 和 X\) 都為空,則集合 \(R\) 中的點構成一個極大團。

  3. \(u\) 點從集合 \(P\) 中刪去,新增到集合 \(X\) 中;

  4. 不斷重複 2~3 操作,直至 \(P\) 為空。

只看演算法過程可能不好理解,那麼下面是虛擬碼及分析。


虛擬碼(虛擬碼出處CSDN已改進):

void dfs(R, P, X){
	if(P 和 X 均為空) 輸出 R 集合為一個極大團 
	for 從 P 中選取一個點 a,與 a 相連的點集為 Q(a) {
		dfs(R 並上 a,P 和 Q(a) 的交集,X 和 Q(a) 的交集)
		從 P 中移除 a 點
		把 a 點加入 X 集合
	}
}

分析:

  • 演算法主要思路:很簡單,我們每次列舉合法的點加入極大團中,合法即為保證該點加入團中,該團仍然是團,接著更新合法點集合(即可能屬於在找的團的點集 \(P\) ),不斷遞迴直到該團極大即可。

  • 我們用 \(P\) 集合維護可能包含於目前所在找的極大團的點集,分析 \(P\) 集合是如何更進的:
    \(R\) 是當前在找的極大團,由於 \(R\) 集合是每次任意從 \(P\) 中取一個點,我們知道團的定義為任意兩個點都有邊相連,所以若我把當前新選擇的點 \(a\) 加入團中,那麼 \(R\) 加入 \(a\) 之後,要想保證新 \(P\) 集合中的點可能包含於新 \(R\) 中團,那麼需要滿足 \(P\) 中的點都與 \(R\) 中任意一點相連。我們已經可以保證原 \(R\) (加入 \(a\) 之前)集合裡所有點都與原 \(P\) 中的點相連,所以現在只需新增條件使得新 \(P\) 中的點與 \(a\) 點相連,於是 \(P∩{Q(a)}\) 是新 \(P\) 集合。

  • 找到一個極大團時需要滿足 \(P,X\) 集合都為空:
    \(P\) 為空即再沒有點可以加到 \(R\) 集合中,保證在找的團極大;\(X\) 為空保證之前沒有找過此團,用來判重。


圖片演示:

如此圖,藍點為 \(P\) 集合中的點(可能屬於當前在找的極大團中的點),橙點為 \(R\) 集合中的點(已經加入極大團中的點),灰色為啥也不是點。

  1. 先取 \(1\) 號點作為 \(u\) 點加入 \(R\) (在找的團)中,此時我們遞迴集合 \((R\) ∪{1}, \(P ∩ Q(1)\), \(X ∩Q(u)\) ) ,\(Q(1)\)即與 \(1\) 相連的點——只有 \(2\) 號點,那麼現在有 \(R=\){1}, \(P=\){2}, \(X=\) ∅,\(3,4\) 號點變為啥也不是點;

  1. 再從當前的 \(P\) 中取 2 號點加入 \(R\) 中,再次遞迴新集合(\(R\) ∪ {2}, \(P ∩ Q(2)\), \(X ∩ Q(2)\)

  1. 在新的遞迴中我們發現 \(P, X\) 都為空,於是找到了一個極大團{1,2},回溯到第 1 步位置又開始了以 \(2\) 號點為 \(u\) 點的新遞迴;

  1. 不在演示新遞迴,過程與之前一次一樣,可以自己手模一下。



演算法實現:

帶詳細註釋code:

注:建議先看本篇部落格的演算法過程部分以方便看懂程式碼的註釋
int to[N][N], mnt; //to[i][j]用來判斷 i 到 j 之間是否連邊,mnt為最大團中點的個數
int had[N][N], may[N][N], vis[N][N]; //had,may,vis分別表示 當前在找的團中已有的點、可能加入當前在找的團中的點、已經搜過的點(分別對應演算法過程的集合 R,P,X)
//had,may,vis的第一維i都表示處於搜尋的第i層,第二維j表示相應的點的個數

//d表示當前搜尋處於第幾層,R、P、X分別表示had,may,vis在該層搜尋中點的個數
void Bron_Kerbosch(int d, int R, int P, int X){
	if(!P and !X){ mnt = max(mnt, R); return;} //找到一個極大團
	for(int i=1; i<=P; i++){
		int u = may[d][i]; //從 P 中取點

		for(int j=1; j<=R; j++){
			had[d+1][j] = had[d][j];
		} had[d+1][R+1] = u; //即 R' = R + {u} 的操作

		int newP = 0, newX = 0; 
		for(int j=1; j<=P; j++) // P' = P ∩ Q(u)
			if(to[u][may[d][j]]) may[d+1][++newP] = may[d][j];

		for(int j=1; j<=X; j++) // X' = X ∩ Q(u)
			if(to[u][vis[d][j]]) vis[d+1][++newX] = vis[d][j];

		Bron_Kerbosch(d+1, R+1, newP, newX); //遞迴搜尋

		may[d][i] = 0, vis[d][++X] = u; //將 u 點從 P 中刪去,加入 X 中
	}
}

到這裡,就已經可以 A 掉那晚加賽的 T2.七負我 了。

AC 程式碼
#include<bits/stdc++.h>
#define mp make_pair
#define ll long long
using namespace std;

const int N = 50;

int n, m, x, hnt;
int to[N][N];
int had[N][N], may[N][N], vis[N][N];

void Bron_Kerbosch(int d, int R, int P, int X){
	if(!P and !X){ hnt = max(hnt, R); return; }
	for(int i=1; i<=P; i++){
		int u = may[d][i];

		for(int j=1; j<=R; j++){
			had[d+1][j] = had[d][j];
		} had[d+1][R+1] = u;

		int newP = 0, newX = 0;
		for(int j=1; j<=P; j++)
			if(to[u][may[d][j]]) may[d+1][++newP] = may[d][j];

		for(int j=1; j<=X; j++)
			if(to[u][vis[d][j]]) vis[d+1][++newX] = vis[d][j];

		Bron_Kerbosch(d+1, R+1, newP, newX);

		may[d][i] = 0, vis[d][++X] = u;
	}
}

signed main(){
	// freopen("in.in", "r", stdin); freopen("out.out", "w", stdout);

	scanf("%d%d%d", &n, &m, &x);
	for(int i=1; i<=m; i++){
		int a, b; scanf("%d%d", &a, &b);
		to[a][b] = to[b][a] = 1;
	}

	int num = 0;
	for(int i=1; i<=n; i++)
		may[1][++num] = i;

	Bron_Kerbosch(1, 0, num, 0);

	double ans = x * 1.0 / hnt;
	ans *= ans;
	ans *= ((hnt - 1) * hnt / 2);
	printf("%.6lf", ans);

	return 0;
}

但是,這個演算法還可以透過設定關鍵點(pivot vertex)\(v\) 進行最佳化。主要最佳化原理見 oi-wiki


最佳化程式碼(純享版):

int to[N][N], hnt;
int had[N][N], may[N][N], vis[N][N];

void Bron_kerbosch(int d, int R, int P, int X){
    if(!P and !X) { hnt = max(hnt, R); return;}
    int u = may[d][1];

    for(int i=1; i<=P; i++){
        int v = may[d][i];
        if(to[u][v]) continue;

        for(int j=1; j<=R; j++){
            had[d+1][j] = had[d][j];
        } had[d+1][R+1] = v;

        int newP = 0, newX = 0;
        for(int j=1; j<=P; j++)
            if(to[v][may[d][j]]) may[d+1][++newP] = may[d][j];
        for(int j=1; j<=X; j++)
            if(to[v][vis[d][j]]) vis[d+1][++newX] = vis[d][j];
        
        Bron_kerbosch(d+1, R+1, newP, newX);

        may[d][i] = 0, vis[d][++X] = v;

    }
}

相關文章