文章首先於微信公衆號:小K算法,關注第一時間獲取更新信息算法
大清都亡了,咱們村尚未通網。爲了響應國家的新農村建設的號召,村裏也開始了網絡工程的建設。
窮鄉僻壤,人煙稀少,如何佈局網線,成了當下村委會首個急需攻克的難題。
以下圖,農戶之間的距離隨機,建設網線的成本與距離成正比,怎樣才能用最少的成本將整個村的農戶網絡連通呢?微信
若是農戶A到農戶B,農戶B到農戶C的網線已經建好了,那農戶A和農戶C也間接的連通了,不用再建設。網絡
每一根線均可以連通2個農戶,因此有N個農戶,就只須要N-1條網線就能夠了。框架
將上述問題轉化爲無向圖來表示。佈局
用鄰接矩陣存儲農戶之間的距離。3d
這樣問題就轉化成:找N-1條邊將上述圖組成一個連通圖,要求N-1條邊的權值和最小。code
這就是經典的最小生成樹問題。有兩種算法專門解決這類問題,Prim和Kruskal。blog
對於一個N個點,N-1條邊的連通圖:
若是剪掉1條線,整個圖會變成2個連通子圖;若是剪掉2條線,就會變成3個連通子圖。ci
若是剪掉了B到D之間的網線,這時變成2個連通子圖。get
爲了將整個圖連通,就須要找出兩個子圖之間的最小距離邊,連通這條邊就好了。
其實就是找出子圖1中的全部點到子圖2中的全部點的最小邊。
這裏有3條邊,A-C,B-D,B-E,其中A-C距離最小,連通這條邊就是最好的方案。
推論:
如上圖就不是最優方案,由於兩個子圖之間還有更小的邊
對於加權連通圖G=(V,E),V爲頂點集,E爲邊集。
算法解釋:把S和非S想象成兩個子圖,每一步其實就是在找出這兩個子圖之間的最小邊。
過程模擬以下圖:
繼續重複以上過程直到S=V,T即爲所求邊集。
變量定義
const int MAXN = 100; int n, m, temp, ans = 0, map[MAXN][MAXN], length[MAXN]; char s, t; bool flag[MAXN];
初始化
void init() { cin >> n >> m; memset(map, -1, MAXN * MAXN); for (int i = 0; i < n; ++i) { map[i][i] = 0; flag[i] = false; length[i] = 0x7fffffff; } for (int i = 0; i < m; ++i) { cin >> s >> t >> temp; map[s - 'A'][t - 'A'] = temp; map[t - 'A'][s - 'A'] = temp; } }
核心算法
int main() { init(); // 將0做爲起點加入集合S flag[0] = true; for (int i = 0; i < n; ++i) { if (map[0][i] >= 0) { length[i] = map[0][i]; } } // 選擇N-1條邊 for (int i = 0; i < n - 1; ++i) { int min = 0x7fffffff; int k = 0; // 枚舉非S全部點,選擇最小的邊 for (int j = 1; j < n; ++j) { if (!flag[j] && length[j] < min) { min = length[j]; k = j; } } ans += min; cout << "k=" << k << " ,min=" << min << endl; // 將新的點k加入集合S,並經過k更新非S中點的距離 flag[k] = true; for (int j = 1; j < n; ++j) { if (!flag[j] && map[k][j] >= 0 && map[k][j] < length[j]) { length[j] = map[k][j]; } } } cout << "ans=" << ans; return 0; }
最優解是要選取N-1條邊,邊的數量是固定的,但邊的權值不同,因此可讓這N-1條邊儘量的小。那就能夠用貪心的思想,從最小的邊開始選擇。
如上圖,從最小的邊開始選擇,第1條是A-B,第2條是B-D,第3條是A-D。
但這裏就出現了衝突,由於A與D已經連通,再多一條邊會造成環,沒有意義。
因此再多加一個判斷,若是一條邊所關聯的兩個點已經連通就不能選擇,不然能夠選擇。
當選擇第4條邊D-E時,判斷D和E沒有連通,將這兩個子圖連通。把兩個子圖當作不一樣的集合,這一步就是合併成同一個集合。
若是初始每一個點都屬於一個獨立的集合,每選擇一條邊,就將所在的集合合併成同一個,在下一次選擇邊的時候,就只需判斷關聯的兩個點是否爲同一集合。這就能夠用並查集快速處理。
詳細可查看並查集專題。
對於加權連通圖G=(V,E),V爲頂點集,E爲邊集。
過程模擬以下圖:
繼續重複以上過程直到選出N-1條邊。
變量定義
struct Edge { int start; int end; int value; }; const int MAXN = 100, MAXM = 100; int n, m, answer = 0, edgeNum = 0, father[MAXN]; Edge edge[MAXM];
初始化
void init() { char s, e; int temp; // 並查集根結點,初始爲-1,合併以後爲-num,num表示集合中的個數 memset(father, -1, MAXN); cin >> n >> m; for (int i = 0; i < m; i++) { cin >> s >> e >> temp; edge[i].start = s - 'A'; edge[i].end = e - 'A'; edge[i].value = temp; } } bool compare(const Edge &a, const Edge &b) { return a.value < b.value; }
查找根
int findFather(int s) { int root = s, temp; // 查找s的最頂層根 while (father[root] >= 0) { root = father[root]; } // 路徑壓縮,提升後續查找效率 while (s != root) { temp = father[s]; father[s] = root; s = temp; } return root; }
合併集合
void unionSet(int s, int e) { int rootS = findFather(s); int rootE = findFather(e); int weight = father[rootS] + father[rootE]; // 將結點數少的集合做爲結點數多的集合的兒子節點 if (father[rootS] > father[rootE]) { father[rootS] = rootE; father[rootE] = weight; } else { father[rootE] = rootS; father[rootS] = weight; } }
核心算法
int main() { init(); sort(edge, edge + m, compare); for (int i = 0; i < m; i++) { if (findFather(edge[i].start) != findFather(edge[i].end)) { unionSet(edge[i].start, edge[i].end); answer += edge[i].value; edgeNum++; if (edgeNum == n - 1) { break; } } } cout << answer << endl; return 0; }
prim基於頂點操做,適用於點少邊多的場景,多用鄰接矩陣存儲。
kruskal基於邊操做,適用於邊少點多的場景,多用鄰接表存儲。
掃描下方二維碼關注公衆號,第一時間獲取更新信息!