啊哈演算法(8)——更多精彩的演算法
1、圖的最小生成樹(Kruskal演算法)
對於一個給定的圖,找出其最小生成樹,用最少的邊讓n個頂點的圖連通,很顯然若要讓n個頂點的圖連通,最少要n-1條邊,最小生成樹還需要滿足這n-1條邊的權重和最小。例如對於如下輸入例項:
6 9
2 4 11
3 5 13
4 6 3
5 6 4
2 3 6
4 5 7
1 2 1
3 4 9
1 3 2
第一行n和m,n表示有n個頂點,m表示有m條路徑,接下來的m行形如a b c表示頂點a到頂點b的權重為c。
Kruskal演算法的核心為:首先對邊按照權重進行排序,每一次從剩餘的邊中選擇權值較小的並且不會產生迴路的邊,加入到生成樹中,直到加入了n-1條邊為止。實現如下:
/*Kruskal演算法,最小生成樹*/
#include<iostream>
using namespace std;
struct edge {
int u;
int v;
int w;
};//為了方便按照邊來排序使用一個結構體來存放邊的資訊
struct edge e[10];// 結構體陣列存放邊的資訊
int f[7]; //最多輸入1000個節點
int n, m;//節點個數和線索數目
//對邊進行排序
void quicksort(int left, int right)
{
//選擇基準
struct edge t = e[left];
if (left > right)
return;
int i = left;
int j = right;
while (i!=j)
{
while (e[j].w >=t.w&&i < j)
j--;
while (e[i].w <=t.w&&i < j)
i++;
//交換
if (i < j)
{
struct edge temp = e[i];
e[i] = e[j];
e[j] = temp;
}
}
//基準歸位
e[left] = e[i];
e[i] = t;
quicksort(left, i - 1); //遞迴處理左邊
quicksort(i+1,right); //遞迴處理右邊
}
//使用並查集來判斷兩個邊是不是在一個集合中 ,比用DFS快
void init() //剛開始每個節點都是孤立的
{
for (int i = 1; i <= n; i++)
{
f[i] = i;
}
}
//尋找祖宗的過程
int getf(int v)
{
if (f[v] == v)
return v;
else
{
//向上繼續尋找其祖宗,並且在遞迴函式返回的時候,把中間的父節點都改為最終找到的祖宗的編號
//這其實就是路徑壓縮,可以提高以後找到最高祖宗的速度
f[v] = getf(f[v]);
return f[v];
}
}
//合併兩個子集合
int merge(int v, int u)
{
int t1 = getf(v);
int t2 = getf(u);
if (t1 != t2)
{
f[t2] = t1;// 遵循靠左合併的原則
return 1;//不在一個集合中 就返回1
}
return 0;
}
int main()
{
int sum = 0;
int count = 0;
cin >> n >> m;
for (int i = 1; i <= m; ++i)
{
cin >> e[i].u >> e[i].v >> e[i].w;
}
quicksort(1, m); //對邊進行排序
//並查集初始化 最開始每個節點都是一個獨立的節點
init();
//Kruskal演算法
for (int i = 1; i <= m; ++i)
{
//判斷兩個頂點是不是在同一個集合中
if (merge(e[i].u, e[i].v))
{
//不在一個樹中就將這條邊加入生成樹中
cout << e[i].u << " " << e[i].v << " " << e[i].w << endl;
count++;
sum += e[i].w;
}
if (count == n - 1) //當選用了n-1條邊之後 就退出
break;
}
cout << sum;
system("pause");
}
Kruskal演算法的時間複雜度為:對邊進行快排是O(MlogM),在m條邊中找出n-1條邊是O(MlogN),所以Kruskal演算法的時間複雜度為O(MlogM+MlogN)。
2、最小生成樹(prim演算法)
prim演算法的核心步驟如下:
- 從任意一個定點開始構造生成樹,假設從一號頂點開始。首先將一號頂點加入生成樹中,用一個數組book來標記哪些頂點已經加入到生成樹中了。
- 用dis陣列記錄生成樹到各個頂點的距離。最初生成樹中只有一號頂點,有直連邊時,陣列dis中儲存的就是1號頂點到該頂點的邊的權值,沒有直連邊的時候就是無窮大,即初始化dis陣列。
- 從陣列中選出離生成樹最近的頂點(假設頂點為j)加入到生成樹中(在dis陣列中找到最小值)。在以j為中間點,更新生成樹到每一個非樹頂點的距離(鬆弛),即如果dis[k]>e[j][k]則更新dis[k]=e[j][k]。
- 重複第三步
例如對於如下示例:
6 9
2 4 11
3 5 13
4 6 3
5 6 4
2 3 6
4 5 7
1 2 1
3 4 9
1 3 2
第一行n和m,n表示有n個頂點,m表示有m條路徑,接下來的m行形如a b c表示頂點a到頂點b的權重為c。實現程式碼如下:
/*prim演算法,使用鄰接矩陣存圖*/
#include<iostream>
using namespace std;
#define inf 99999
int e[7][7], dis[7], book[7];
int n, m; //頂點數和邊數
int main()
{
cin >> n >> m;
//鄰接矩陣初始化
for(int i=1;i<=n;++i)
for (int j = 1; j <= n; ++j)
{
if (i == j)
e[i][j] = 0;
else
e[i][j] = inf;
}
int a, b, c;
for (int i = 1; i <= m; ++i) //讀入邊,由於為無向圖所以兩邊都要存一下
{
cin >> a >> b >> c;
e[a][b] = c;
e[b][a] = c;
}
//初始化dis陣列,假定從1號頂點開始
for (int i = 1; i <= n; ++i)
dis[i] = e[1][i];
int count = 0;
//1號頂點加入到生成樹中
book[1] = 1;
count++;
//prim演算法核心語句
int sum = 0; //最小生成樹路徑
while (count<n) //總共n-1條邊就可以使得圖連通
{
//從非樹節點集合中選擇 離生成樹中最近的點加入到生成樹中
int min = inf;
int pos = 0;
for (int i = 1; i <= n; ++i)
{
if (book[i]==0&&dis[i] < min)
{
min = dis[i];
pos = i;
}
}
book[pos] = 1;
sum += min;
count++;
//以pos點為中心,更新各個非樹節點到生成樹的距離
for (int i = 1; i <= n; ++i)
{
if (book[i] == 0 && dis[i] > e[pos][i])
dis[i] = e[pos][i];
}
}
cout << sum << endl;
system("pause");
}
上面這種演算法的時間複雜度為O(N^2),如果使用堆,每一次選邊的時間複雜度是O(logM),然後使用鄰接表來儲存整個圖時間複雜度會降低到O(MlogN)。實現程式碼如下:
使用三個陣列,dis陣列用來記錄生成樹到各個頂點的距離。陣列h是一個最小堆,堆裡面儲存的是頂點編號。這裡並不是按照頂點的編號來建立最小堆,而是按照頂點在dis陣列中的數值來建立最小堆的,此外還需要一個數組pos來記錄每個頂點在最小堆中的位置。
/*prim演算法實現:使用鄰接表來儲存圖並且使用堆優化*/
#include<iostream>
#define inf 999999;
using namespace std;
int dis[7], book[7];// 記錄各個頂點到生成樹的最小距離,判斷頂點是否在生成樹中
int h[7], pos[7]; //h用來儲存堆,pos用來儲存堆中每一個頂點的位置
int h_size; //size用來表示堆的大小
void swap(int x,int y)
{
int t = h[x];
h[x] = h[y];
h[y] = t;
//同步更新pos
t = pos[h[x]];
pos[h[x]] = pos[h[y]];
pos[h[y]] = t;
}
void siftdown(int i) //對堆中編號為i的節點實時向下調整
{
int t, flag = 0; //flag用來標記是否需要繼續向下調整
while (i*2<= h_size&&flag==0) //左孩子存在
{
if (dis[h[i]] > dis[h[2 * i]])
t = i * 2;
else
t = i;
//如果有右兒子急需判斷
if (i * 2 + 1 <= h_size)
{
if (dis[h[t]] > dis[i * 2 + 1])
t = i * 2 + 1;
}
if (t != i)
{
swap(t, i);
i = t; //便於接下來繼續向下調整
}
else
flag = 1;//不在需要向下調整了
}
}
void siftup(int i) //對編號i進行向上調整
{
int flag = 0;
if (i == 1)
return;//在堆頂直接返回
//不在堆頂並
while (i != 1&&flag==0)
{
//與父節點進行比較
if (dis[h[i / 2]] > dis[h[i]])
swap(i, i / 2);
else
flag = 1;
i = i / 2; //更新節點方便下一次使用
}
}
void create() //建立一個堆
{
//從最後一個非葉節點開始實行向下調整
for (int i = h_size / 2; i >= 1; --i)
siftdown(i);
}
//從堆頂中取出一個元素
int pop()
{
int t = h[1];
pos[t] = 0;
h[1] = h[h_size];
pos[h[1]] = 1; //更新頂點h[1]在堆中的位置
h_size--;
siftdown(1); //向下調整
return t;
}
int main()
{
int n, m;//頂點個數和邊的個數
int u[19], v[19], w[19]; //採用鄰接矩陣來儲存圖 表示頂點u[i]到頂點v[i]的權重為w[i] 由於為無向圖實際的大小為2*m+1
int first[7]; //儲存的是節點i的第一條邊的編號為first[i],大小為n+1;
int next[19]; //儲存的是編號為i的邊的下一條邊的編號next[i]。
cin >> n >> m;
for (int i = 1; i <= m; ++i)
{
cin >> u[i] >> v[i] >> w[i];
}
//由於為無向圖所以還需要儲存一遍
for (int i = m + 1; i <= 2 * m; ++i)
{
u[i] = v[i - m];
v[i] = u[i - m];
w[i] = w[i - m];
}
//採用鄰接表來儲存圖,首先對first陣列初始化,最開始沒有讀入邊 所以記錄為-1;
for (int i = 1; i <= n; ++i)
first[i] = -1;
for (int i = 1; i <= 2 * m; ++i)
{
next[i] = first[u[i]];
first[u[i]] = i;
}
//prim演算法核心
int count = 0;
int sum = 0;
//1號頂點加入到生成樹中
book[1] = 1;
count++;
//初始化dis陣列
dis[1] = 0;
for (int i = 2; i <= n; ++i)
dis[i] = inf;
int k = first[1]; //1號節點的第一條邊的編號
while (k!=-1)
{
dis[v[k]] = w[k];
k = next[k];
}
//初始化堆
h_size = n;
for (int i = 1; i <= h_size; ++i)
{
h[i] = i;
pos[i] = i;
}
create();
pop();//先彈出堆頂元素 此時堆頂元素是一號頂點
while (count<n)
{
//堆頂元素加入到生成樹當中
int j = pop();
book[j] = 1;
count++;
sum += dis[j];
//以j為中心對邊進行鬆弛
int k = first[j];
while (k!=-1)
{
if (book[v[k]] == 0 && dis[v[k]] > w[k])
{
dis[v[k]] = w[k]; //更新距離
siftup(pos[v[k]]); //對該頂點在堆中的位置進行鬆弛,pos[i]中存放的是節點i在堆中的位置
}
k = next[k];
}
}
cout << sum << endl;
system("pause");
}
3、圖的割點
對於一個給定的圖,求出圖中的割點,採用深度優先搜尋時訪問到了k點,此時圖就會被k點分割成兩個部分,一部分是已經被訪問過的點,另一部分是沒有訪問過的點。如果k點是割點,那麼剩下的沒有被訪問過的點中至少會有一個點在不經過k點的情況下,是無論如何再也回不到已訪問過的點。演算法的關鍵在於:當深度優先遍歷訪問到頂點u時,其孩子頂點v還是沒有訪問過的,如何判斷頂點v在不經過其父節點u的情況下可以回到祖先。為此使用兩個陣列:1、num記錄dfs訪問到每個節點時的時間戳,2、low記錄每個頂點在不經過其父節點時,能夠回到的最小時間戳。對於某個頂點u,對於其孩子v,使得low[v]>=num[u],即不能回到祖先,那麼u點就為割點。對於如下輸入:
6 7
1 4
1 3
4 2
3 2
2 5
2 6
5 6
第一行為節點個數和邊的個數,實現程式碼如下:
#include<iostream>
using namespace std;
int n, m, e[7][7], root;
//num記錄dfs訪問到每個節點時的時間戳,low記錄每個頂點在不經過其父節點時,能夠回到的最小時間戳。
//flag標記某個點是否為割點,index為時間戳
int num[7], low[7], flag[7], index;
int min(int a, int b)
{
return a < b ? a : b;
}
//割點演算法核心
void dfs(int cur, int father) //傳入兩個引數,當前頂點的編號和父節點的編號
{
int child = 0, i, j; //child記錄生成樹中當前頂點cur的兒子個數
index++; //時間戳加1
num[cur] =index; //當前頂點的時間戳
low[cur] = index; //當前頂點能夠訪問到的時間戳,最開始就是自己
for (int i = 1; i <= n; ++i) //列舉當前頂點相連的邊
{
if (e[cur][i] == 1)
{
if (num[i] == 0) //如果當前頂點的時間戳為0,說明頂點i還沒有被訪問到
{
child++;
dfs(i, cur); //對此孩子進行升入遍歷
//更新當前頂點能夠訪問到最早頂點的時間戳 不能通過父節點就只能通過孩子節點
low[cur] = min(low[cur], low[i]);
//如果當前頂點不是根節點並且滿足low[i]>=num[cur],則當前頂點為割點
if (cur != root && low[i] >= num[cur])
flag[cur] = 1;
//如果當前頂點是根節點,在生成樹中根節點必須要有兩個兒子,那麼這個根節點才是割點
if (cur == root &&child == 2)
flag[cur] = 1;
}
//否則如果當前頂點被訪問過,並且這個頂點不是當前頂點cur的父親,則要更新當前節點最早可以訪問到的頂點的時間戳
else if (i != father)
low[cur] = min(low[cur], num[i]);
}
}
}
int main()
{
cin >> n >> m;
for (int i = 1; i <= n; ++i)
for (int j = 1; j <= n; ++j)
e[i][j] = 0;
int x, y;
for (int i = 1; i <= m; ++i)
{
cin >> x >> y;
e[x][y] = 1;
e[y][x] = 1;
}
root = 1;
dfs(1, root);
for (int i = 1; i <= n; ++i)
{
if (flag[i] = 1)
cout << i << " ";
}
system("pause");
}
上述採用鄰接矩陣來實現,這樣時間複雜度都會在O(N^2),這與使用蠻力,刪除一個點然後判斷剩餘的點是否連通是一樣的,因此要採用鄰接表來儲存圖,時間複雜度為O(N+M),下面採用鄰接表存圖的割點演算法實現程式碼為:
/*割點演算法,採用鄰接表實現*/
#include<iostream>
using namespace std;
int u[19], v[19], w[19]; //採用鄰接表來儲存圖,由於為無向圖,所以大小為2*m+1
int first[7];
int nex[19];
int n, m;
int num[7], low[7], index;
int child, root; //child記錄一個節點的孩子節點 root根節點
int flag[7]; //用來標記哪些節點為割點
int min(int a, int b)
{
return a < b ? a : b;
}
void dfs(int cur, int father) //要傳入兩個節點,當前節點和當前節點的父節點
{
index++;
num[cur] = index; //訪問到當前節點的時間戳
low[cur] = index; //最開始不經過父節點所能訪問到的節點的時間戳就是本身
int k = first[cur]; //當前節點的第一條邊
while (k!=-1)
{
if (num[v[k]] == 0) //要訪問的節點時間戳為0,則說明還沒有訪問
{
child++;
dfs(v[k], u[k]); // 繼續深入訪問孩子節點,此時u[k]為v[k]的父節點 dfs遍歷就是得到一顆生成樹
//就更新當前節點不經過父節點所能訪問到的節點的時間戳即low
low[u[k]] = min(low[u[k]], low[v[k]]); //由於一個節點要訪問其他節點並且不過父節點只能經過孩子節點,所以與low[v[k]]進行比較
if (u[k] != root && low[v[k]] >=num[u[k]]) //不為根節點並且v[k]不過父節點u[k]最早能訪問到的節點的時間戳小於父節點u[k]的時間戳 那麼u[k]就為割點
flag[u[k]] = 1;
if(u[k]==root && child==2) //為根節點並且有兩個孩子就為割點
flag[u[k]] = 1;
}
//如果當前節點的所有邊都已經訪問,並且它所能到達的頂點不為其父節點,就更新其不經過父節點所能訪問到的節點的時間戳
else if (v[k] != father)
low[u[k]] = min(low[u[k]], num[v[k]]);
k = nex[k]; //編號為k的邊的下一條邊
}
}
int main()
{
cin >> n >> m;
//使用鄰接表儲存影象
for (int i = 1; i <= m; ++i)
{
cin >> u[i] >> v[i];
w[i] = 1;
}
//由於為雙向圖
for (int i = m + 1; i <= 2 * m; ++i)
{
u[i] = v[i - m];
v[i] = u[i - m];
w[i] = 1;
}
//初始化first陣列,由於開始沒有讀入邊的資訊,所以節點第一條邊的編號為-1
for (int i = 1; i <= n; ++i)
first[i] = -1;
//讀入邊
for (int i = 1; i <= 2 * m; ++i)
{
nex[i] = first[u[i]];
first[u[i]] = i;
}
root = 1;
dfs(1, root);
for (int i = 1; i <= n; ++i)
{
if (flag[i] == 1)
cout << i << " ";
}
system("pause");
}
4、圖的割邊
割邊也稱為橋,在一個無向連通圖中,如果刪除某條邊之後圖不載連通,這與求圖的割點類似,在求割點的時候(u為父節點,v為子節點)判斷low[v]>=num[u]即在不經過父節點的情況下子節點最早能到的節點最早為父節點,在求割邊的時候將判斷條件改為low[v]>num[u]說明子節點v連父節點都到達不了,那麼就說明u->v這條邊就為割邊,因為v回不到祖先,並且也沒有另外一條路回到父節點,所以該邊為割邊,實現程式碼為:
對於如下輸入:
6 6
1 4
1 3
4 2
3 2
2 5
5 6
使用鄰接表實現,複雜度為O(M+N),實現的程式碼為:
/*割點演算法,採用鄰接表實現*/
#include<iostream>
using namespace std;
int u[19], v[19], w[19]; //採用鄰接表來儲存圖,由於為無向圖,所以大小為2*m+1
int first[7];
int nex[19];
int n, m;
int num[7], low[7], index;
int child, root; //child記錄一個節點的孩子節點 root根節點
int flag[7]; //用來標記哪些節點為割點
int min(int a, int b)
{
return a < b ? a : b;
}
void dfs(int cur, int father) //要傳入兩個節點,當前節點和當前節點的父節點
{
index++;
num[cur] = index; //訪問到當前節點的時間戳
low[cur] = index; //最開始不經過父節點所能訪問到的節點的時間戳就是本身
int k = first[cur]; //當前節點的第一條邊
while (k!=-1)
{
if (num[v[k]] == 0) //要訪問的節點時間戳為0,則說明還沒有訪問
{
child++;
dfs(v[k], u[k]); // 繼續深入訪問孩子節點,此時u[k]為v[k]的父節點 dfs遍歷就是得到一顆生成樹
//就更新當前節點不經過父節點所能訪問到的節點的時間戳即low
low[u[k]] = min(low[u[k]], low[v[k]]); //由於一個節點要訪問其他節點並且不過父節點只能經過孩子節點,所以與low[v[k]]進行比較
if (u[k] != root && low[v[k]] > num[u[k]]) //不為根節點並且v[k]不過父節點u[k]最早能訪問到的節點的時間戳小於父節點u[k]的時間戳 那麼u[k]-v[k]就為割邊
cout << u[k] << "-" << v[k] << endl;
}
//如果當前節點的所有邊都已經訪問,並且它所能到達的頂點不為其父節點,就更新其不經過父節點所能訪問到的節點的時間戳
else if (v[k] != father)
low[u[k]] = min(low[u[k]], num[v[k]]);
k = nex[k]; //編號為k的邊的下一條邊
}
}
int main()
{
cin >> n >> m;
//使用鄰接表儲存影象
for (int i = 1; i <= m; ++i)
{
cin >> u[i] >> v[i];
w[i] = 1;
}
//由於為雙向圖
for (int i = m + 1; i <= 2 * m; ++i)
{
u[i] = v[i - m];
v[i] = u[i - m];
w[i] = 1;
}
//初始化first陣列,由於開始沒有讀入邊的資訊,所以節點第一條邊的編號為-1
for (int i = 1; i <= n; ++i)
first[i] = -1;
//讀入邊
for (int i = 1; i <= 2 * m; ++i)
{
nex[i] = first[u[i]];
first[u[i]] = i;
}
root = 1;
dfs(1, root);
system("pause");
}
當然也可以使用鄰接矩陣來存圖,但是時間複雜度至少為O(N^2),這樣完全沒有意義,因為完全可以依次刪除一條邊來進行DFS或者BFS來判斷整個圖是不是連通的,這裡還是給出實現程式碼:
#include<iostream>
using namespace std;
int n, m, e[7][7], root;
//num記錄dfs訪問到每個節點時的時間戳,low記錄每個頂點在不經過其父節點時,能夠回到的最小時間戳。
//flag標記某個點是否為割點,index為時間戳
int num[7], low[7], flag[7], index;
int min(int a, int b)
{
return a < b ? a : b;
}
//割點演算法核心
void dfs(int cur, int father) //傳入兩個引數,當前頂點的編號和父節點的編號
{
int child = 0, i, j; //child記錄生成樹中當前頂點cur的兒子個數
index++; //時間戳加1
num[cur] = index; //當前頂點的時間戳
low[cur] = index; //當前頂點能夠訪問到的時間戳,最開始就是自己
for (int i = 1; i <= n; ++i) //列舉當前頂點相連的邊
{
if (e[cur][i] == 1)
{
if (num[i] == 0) //如果當前頂點的時間戳為0,說明頂點i還沒有被訪問到
{
child++;
dfs(i, cur); //對此孩子進行升入遍歷
//更新當前頂點能夠訪問到最早頂點的時間戳 不能通過父節點就只能通過孩子節點
low[cur] = min(low[cur], low[i]);
//如果當前頂點不是根節點並且滿足low[i]>num[cur],則cur-i為割邊
if (cur != root && low[i] > num[cur])
cout << cur << "-" << i << endl;
}
//否則如果當前頂點被訪問過,並且這個頂點不是當前頂點cur的父親,則要更新當前節點最早可以訪問到的頂點的時間戳
else if (i != father)
low[cur] = min(low[cur], num[i]);
}
}
}
int main()
{
cin >> n >> m;
for (int i = 1; i <= n; ++i)
for (int j = 1; j <= n; ++j)
e[i][j] = 0;
int x, y;
for (int i = 1; i <= m; ++i)
{
cin >> x >> y;
e[x][y] = 1;
e[y][x] = 1;
}
root = 1;
dfs(1, root);
system("pause");
}
5、二分圖最大匹配
二分圖:如果一個圖的所有頂點可以被分為X和Y兩個集合,並且所有邊的兩個頂點恰好一個屬於集合X,另一個屬於集合Y,即每個集合內的頂點每有邊相連,那麼此圖就是二分圖。
二分圖的最大匹配就是,兩兩通過邊匹配(點不可以重複使用),求出最大的匹配樹,最直觀的解法:找出全部的匹配方案輸出配對數最多的一種方案。但是時間複雜度很高。
使用匈牙利演算法解決這個問題,匈牙利演算法過程如下:
- 首先從任意一個未配對的點u開始,從點u的邊中任意選擇一條邊(假設這條邊是u->v)開始配對。如果此時點v還沒有匹配,則配對成功,此時就找到了一條增廣路。如果此時點v已經配對了,就要嘗試進行連鎖反應(即看看v先在配對的點還有沒有其他的點可以配對),如果嘗試成功就找到了一條增廣路,此時需要更新原來的配對關係。這裡使用一個match陣列來記錄配對關係,比如v與u匹配成功就記作match[v]=u和match[u]=v。配對成功後將配對數加1。配對過程使用DFS來實現
- 如果剛才嘗試失敗,就從u的邊中重新選擇一條邊進行嘗試,直到點u匹配成功。
- 接下來對剩下沒有配對的點一一進行配對,直到所有的點嘗試完畢
對於如下輸入:
6 5
1 4
1 5
2 5
2 6
3 4
其中1、2、3代表集合x中的元素,4、5、6代表集合Y中的元素,求出最大匹配對數,實現程式碼為:
/*匈牙利演算法實現*/
#include<iostream>
using namespace std;
int e[101][101];//使用鄰接矩陣儲存整個地圖
int match[101]; //用來記錄配對的關係 比如v與u匹配成功就記作match[v]=u和match[u]=v。
int book[101]; //用來標記某個頂點是否已經被嘗試了
int n, m;//頂點個數和邊的數目
int dfs(int u)
{
for (int i = 1; i <= n; ++i) //嘗試每一個頂點
{
if (book[i] == 0 && e[u][i] == 1) //還沒有嘗試並且有邊相連
{
book[i] = 1; //標記已經嘗試
//match[i]==0說明當前節點還沒有匹配
/*dfs(match[i]) 說明當前節點i已經匹配了節點假設為j,則讓節點j與其他節點進行重新匹配看看是不是可以成功
如果成功則當前節點就可以與節點j匹配*/
if (match[i] == 0 || dfs(match[i]))
{
//更新匹配關係
match[u] = i;
match[i] = u;
return 1;
}
}
}
return 0;
}
int main()
{
int sum = 0;
cin >> n >> m;
int x, y;
for (int i = 1; i <= m; ++i)
{
cin >> x >> y;
e[x][y] = 1;
e[y][x] = 1; //為無向圖
}
//最開始沒有匹配關係 初始化match陣列
for (int i = 1; i <= n; ++i)
match[i] = 0;
for (int i = 1; i <= n; ++i)
{
for (int j = 1; j <= n; ++j)
book[j] = 0; //清空上次搜尋的記錄
if (dfs(i))
sum++; //尋找到一條
}
cout << sum << endl;
system("pause");
}
如果二分圖有n個點,那麼最多找到n/2條增廣路。如果圖中有m條邊,那麼沒找一條增廣路就要把所有邊遍歷一遍,所花時間時m。所以總的時間複雜度是O(NM)。對於判斷一個圖是否是二分圖,可以首先將任意一個頂點進行著紅色,然後將相鄰的點著藍色,如果按照這樣的著色可以將全部的頂點著色,並且相鄰的頂點著色不同,那麼該圖就是二分圖。