7.21晚上加賽 T2.七負我,做這題找到了性質發現需要求最大團,不會,爆搜,打假了,賽後改,對了,但時間複雜度大爆炸,看下發題解,有這麼一句話:於是學習了一下。
Bron-kerbosch演算法-求圖的最大團,極大團
概念:
- 團:每個頂點都兩兩相連(又叫完全子圖)
- 極大團:沒有被包含在其他團中的團
- 最大團:頂點數最多的極大團
演算法過程:
過程:
我們維護三個集合 \(R、P、X\),\(R\) 表示當前正在找的極大團裡的點,\(P\) 表示有可能加入當前在找的極大團裡的點,\(X\) 表示已經找到的極大團中的點(用來判重),進行以下過程:
-
初始化 \(R、 X\) 為空集,\(P\) 為包含所有點的集合;
-
將 \(P\) 中頂部元素 \(u\) 點取出,(設 \(Q(u)\) 為所有與 \(u\) 相鄰的點)遞迴集合 \(R ∪{u},P ∩ {Q(u)},X ∩ {Q(u)}\);
- 在遞迴的過程中如果集合 \(P 和 X\) 都為空,則集合 \(R\) 中的點構成一個極大團。
- 在遞迴的過程中如果集合 \(P 和 X\) 都為空,則集合 \(R\) 中的點構成一個極大團。
-
將 \(u\) 點從集合 \(P\) 中刪去,新增到集合 \(X\) 中;
-
不斷重複 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\) 為空保證之前沒有找過此團,用來判重。
演算法實現:
帶詳細註釋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;
}
}