101 北一女中 資訊選手培訓營

Preview:

DESCRIPTION

101 北一女中 資訊選手培訓營. 最小花費擴張樹 Minimum (Cost) Spanning Tree, MST. 2012.08. 06 Nan. 一張圖的 MST 就是 …. M inimum Cost. 花費最小. S panning. 且包含所有點. T ree. 的樹. Tree 的性質. 每棵樹一定都有一個 root 一棵樹如果有 N 個點 ,那麼樹上一定會有 N-1 條邊 樹上不會存在 cycle 樹上任兩個節點之間的路是唯一的 樹 上的邊可以有權重 (weight ). MST- 問題描述. - PowerPoint PPT Presentation

Citation preview

101 北一女中資訊選手培訓營最小花費擴張樹

Minimum (Cost) Spanning Tree, MST

2012.08. 06 Nan

Minimum Cost花費最小

且包含所有點

的樹

一張圖的 MST 就是…

Spanning

Tree

Tree 的性質• 每棵樹一定都有一個 root• 一棵樹如果有 N 個點,那麼樹上一定會有 N-1 條邊• 樹上不會存在 cycle• 樹上任兩個節點之間的路是唯一的• 樹上的邊可以有權重 (weight)

MST- 問題描述給妳一張圖,圖上有 N 個節點和 M 條邊,每條邊上有權重 (Weight) ,代表建這條邊需要的花費 (Cost) ,問你如何在這張圖上選出一些邊,使得這張圖兩兩之間都有路徑可以到達,並且建邊的總花費最少。

10

20 40

70

100105

60 80

問題始源在二次世界大戰過後的波希米亞要重新鋪設電力線,但是因為他們沒有什麼錢了,所以他們希望所有的區域都可以配到電,且鋪設電力線的成本要最低。

Minimum Cost 成本要最低Spanning Tree 原圖所有點的集合 + 原圖所有邊的子集合,邊的子集合要使得整張圖是連通的

= 所有區域都可以配到電

問題轉換

10

20 40

70

100105

60 80

區域 節點兩區域之間可建電力線邊 建電力線的花費權重

特性觀察可能不是唯一解 10

10 10

10

101010

10 10

10

10 10

1010

10 10

10

10 10

特性觀察如果原圖有 cycle ,則拿掉的邊一定會是比較重的邊

10

20 40

70

100105

60 80

特性觀察比較小的邊一定會先被挑到

10

20 40

70

100105

60 80

10

20 40

105

把邊的 Weight由小到大排序5

10102040607080

100

簡單的方向: Greedy

Algorithm I : Prim’s Algorithm

Prim’s Algorithm• 使用鄰接矩陣 ( 二維陣列 ) 的時間複雜度是

O(n2)• 概念是 Greedy :盡量選小的,如果會造成

Cycle 就不選 ( 為何會對? )

• 可以從任意一個點開始做 (Why?)

Prim’s Algorithm• 使用鄰接矩陣 ( 二維陣列 ) 的時間複雜度是

O(n2)• 概念是 Greedy :盡量選小的,如果會造成

Cycle 就不選 ( 為何會對? )如果只能選 N-1 條邊,那麼選盡量小的,能組成樹( 不會產生 Cycle) 的邊,花費一定最少

• 可以從任意一個點開始做 (Why?)一棵樹可以以任何一個節點作為 root( 每個點一定會至少和一條邊相連接 )

範例

挑選第一個點加入正確聯盟,也就是當成這棵樹的root

1

2

64 5

310

20 40

70

100105

60 80

*只要一個節點被上了橘色,就代表他加入了「正確聯 盟」,也就是 root 到它的路徑已經確定了

1 2 3 4 5 6

0 inf inf inf inf inf

範例

看看所有和正確聯盟中的點相連的邊,挑出最小的連過去

1

2

64 5

310

20 4070

100105

60 80

*紅色代表最小的邊,咖啡色代表已經確定存在的邊

1 2 3 4 5 6

0 20 40 inf inf inf

1 2 3 4 5 6

0 20 40 10 5 10更新後:

範例

看看所有和正確聯盟中的點相連的邊,挑出最小的連過去

1

2

64 5

310

20 40

100105

60 80

如果有兩條邊可以讓正確聯盟連到該點則保留較小權重的邊即可

1 2 3 4 5 6

0 20 40 10 5 10

範例

看看所有和正確聯盟中的點相連的邊,挑出最小的連過去

1

2

64 5

310

20 40

100105

*有相等的就隨意挑

1 2 3 4 5 6

0 20 40 10 5 10

範例

看看所有和正確聯盟中的點相連的邊,挑出最小的連過去

1

2

64 5

310

20 40

100105

1 2 3 4 5 6

0 20 40 10 5 10

範例

看看所有和正確聯盟中的點相連的邊,挑出最小的連過去

1

2

64 5

310

20 40

105

1 2 3 4 5 6

0 20 40 10 5 10

範例

所有的點都加入正確聯盟之後,就做完了!

1

2

64 5

310

20 40

105

1 2 3 4 5 6

0 20 40 10 5 10

演算法描述1. 初始化,開一個陣列存放正確聯盟到各個點的最近距離2. 將第一個點無條件加入正確聯盟3. 每次都找目前距離正確聯盟最近的點,將之加入正確聯盟4. 更新因該點而使得正確聯盟到其距離更近的點

( 前提是此點還沒有被走過且新加進去的點有路可到此點 )5. 重複 3 & 4 直到所有的點都加入正確聯盟

其他一些處理• 要知道總花費?開一個變數,邊做的時候就邊計算• 要知道選了那些邊?開一個陣列,紀錄每個點是透過誰加入正確聯盟• 要知道是哪些路有通,並依序印出改用一個鄰接矩陣紀錄,邊做的時候邊把有通的路標起來,最後用

DFS跑一次即可• 在邊很少時,更快的方法找最小值?鄰接串列 +Minimum Heap

另外要注意的• 有可能存在不連通的點 ( 要注意是否有說為連通圖 )• 有可能會有多重邊 ( 要看題目敘述,是否有說是簡單圖或兩個點之間只有一條路 ) ,如果有多重邊只要留下 cost 最小的邊即可• 整個複雜度是 O(V2) ,用鄰接串列 &Heap 可加速到 O((V+E)lgV)

#include <string.h> // 為了用 memset 這個 function 所以要 include 這個#define INF 2147483647 // 用 int 的最大值做為無限大int graph[N][N]; // 假設我們有 N 個點。這裡存的是邊 (i,j) 的花費 ( 無向邊 ) // 沒有邊時的花費就是 INFint cost[N]; // 記錄目前要把第 i 個點加入正確聯盟所需要的花費int last[N]; // 記錄第 i 個點是透過誰加入了正確聯盟 ( 等於是存在 edge(last[i], i))int choosed[N]; // 記錄是否已經加入了正確聯盟int fin_cnt; // 記錄已經加入正確聯盟的點的個數int total_cost; // 記錄總花費void init(){ // 初始化 // memset 會把整塊記憶體空間都填上零,有歸零作用 ( 但不能用來歸成除了 0 和 -1 之外的其他值 ) 。 memset(choosed, 0, sizeof(choosed)); // last = -1 代表自己就是 root ,一開始所有點都是自己的 root memset(last, -1, sizeof(last)); // 以 idx=0 的點作為 root 開始看花費 cost[0] = 0; choosed[0] = 1; int i; for ( i = 1 ; i < N ; i++ ){ cost[i] = graph[0][i]; // 如果有邊 cost 就會是該條邊,反之則會是 INF if ( cost[i] != INF ) last[i] = 0; } fin_cnt = 1; // 一開始只有一個點在正確聯盟裡}

void prim(){ int min; // 用來存這一輪找到的花費最小值 int min_idx; // 用來存這一輪找到花費最小的是哪個點 int i; while ( fin_cnt < N ) { // 如果小於 N 代表還沒找完 min = INF; // 初始化成 INF ,用來找最小值 min_idx = -1; // 初始化成 -1 ,之後用來判別有沒有找到新的可用的點 for ( i = 1 ; i < N ; i++ ){ // 跑過所有點,找最小值 if ( choosed[i] == 1 ) // 已經在正確聯盟裡就不考慮 continue; if ( cost[i] < min ){ min_idx = i; min = cost[i]; } } if ( min_idx == -1 ) break; // 如果沒找到代表此圖找不到 spanning tree choosed[min_idx] = 1; // 標記 min_idx 這個點進入了正確聯盟 total_cost += cost[min_idx]; // 加上加入這個點的 cost fin_cnt++; // fin_cnt增加一,代表多了一個點已經確定 // 看看還沒有被選的點,有沒有點能夠透過 min_idx 這個點而更近的 for ( i = 1 ; i < N ; i++ ){ if ( choosed[min_idx] == 1 ) continue; // 被選過的就跳過 if ( graph[min_idx][i] < cost[i] ){ // 有更近就更新 last[i] = min_idx; cost[i] = graph[min_idx][i]; } } }}

Algorithm II: Kruskal’s Algorithm

Kruskal’s Algorithm• 一樣是 Greedy• 用到剛剛從小到大排序抓較小的方法• 以邊為主體:對於每條邊,紀錄起點、終點、花費• 先將邊依照 cost 排序,由小到大一條一條做,在選時判斷:如果會造成 Cycle 就不選該條邊,並記錄每條邊有沒有被選到• 可以想成許多的 MST 在做 Union

用 Disjoint Set 來做 Union-Find輔助之!• 整體時間複雜度是 O(ElogE)

範例1

2

64 5

310

20 40

70

100105

5 80

5

5

10

10

20

40

70

80

100

先將所有邊從小到大排序好

1 2 3 4 5 6

-1 -1 -1 -1 -1 -1

範例1

2

64 5

310

20 40

70

100105

5 80

5

5

10

10

20

40

70

80

100

由小到大開始做,不會造成 Cycle 就選起來

1 2 3 4 5 6

-1 -1 -1 -1 -1 -1

Find()操作在節點 parent 為 -1 時會傳回節點本身編號

2 != 5 Union

2

範例1

2

64 5

310

20 40

70

100105

5 80

5

5

10

10

20

40

70

80

100

由小到大開始做,不會造成 Cycle 就選起來

1 2 3 4 5 6

-1 -1 -1 -1 2 -1

2 != 4 Union

2

範例1

2

64 5

310

20 40

70

100105

5 80

5

5

10

10

20

40

70

80

100

由小到大開始做,不會造成 Cycle 就選起來

1 2 3 4 5 6

-1 -1 -1 2 2 -1

2

2 != 6 Union

範例1

2

64 5

310

20 40

70

100105

5 80

5

5

10

10

20

40

70

80

100

出現 Cycle 代表左右兩點本來就是在同一個集合裡了,要判斷可以用 Disjoint Sets 的操作。

出現 Cycle 了!這條邊不選!

1 2 3 4 5 6

-1 -1 -1 2 2 2

2 == 2 Don’t Union

範例1

2

64 5

310

20 40

70

100105

5 80

5

5

10

10

20

40

70

80

100

1 2 3 4 5 6

-1 -1 -1 2 2 2

1 != 2 Union

2

範例1

2

64 5

310

20 40

70

100105

5 80

5

5

10

10

20

40

70

80

100

1 2 3 4 5 6

2 -1 -1 2 2 2

2 != 3 Union

2

範例1

2

64 5

310

20 40

70

100105

5 80

5

5

10

10

20

40

70

80

100出現 Cycle 了!這條邊不選!

1 2 3 4 5 6

2 -1 2 2 2 2

2 == 2 Don’t Union

範例1

2

64 5

310

20 40

70

100105

5 80

5

5

10

10

20

40

70

80

100出現 Cycle 了!這條邊不選!

1 2 3 4 5 6

2 -1 2 2 2 2

2 == 2 Don’t Union

範例1

2

64 5

310

20 40

70

100105

5 80

5

5

10

10

20

40

70

80

100出現 Cycle 了!這條邊不選!

1 2 3 4 5 6

2 -1 2 2 2 2

2 == 2 Don’t Union

範例1

2

64 5

310

20 40

70

100105

5 80

5

5

10

10

20

40

70

80

100

做完了!

1 2 3 4 5 6

2 -1 2 2 2 2

演算法描述1. 將所有的邊按照 cost 排序2. 對所有的點作 Disjoint set 的初始化3. 從 cost 小的到 cost 大的,枚舉每條邊4. 對於每條邊,看看該條邊所連接的兩點是否屬於同的 Set( 代表有 Cycle) ,如果不同則將該條邊標上使用,並 Union 他們5. 重複 4 直到所有的邊都枚舉完

其他一些處理• 要知道總花費?

開一個變數,邊做的時候就邊計算• 要知道是哪些路有通,並依序印出

把所有有使用的邊印出來即可• 要注意是否所有的點都有連通

(才算整張圖存在一個 MST)

#include <stdlib.h> // 為了用 qsort 所以要 include 這個#include <string.h> // 為了用 memset 這個 function 所以要 include 這個int st[M], ed[M], cost[M];// 假設我們有 M 條邊。 edge(st[i], ed[i]) 的 cost 為 cost[i]int ind[M]; // 用來索引排序的陣列int choosed[M]; // 用來記該邊是否有被選到int set_r[N]; // 用來記 disjoint set 中每個點的 parent 的陣列int total_cost;

void init(){ // 初始化 memset(choosed, 0, sizeof(choosed)); qsort(ind, M, sizeof(int), comp); // comp裡請以 cost 由小到大排 int i; for ( i = 0 ; i < N ; i++ ){ set_r[i] = -1; // disjoint set 初始化 } total_cost = 0;}void kruskal(){ int i, vRt1, vRt2; for ( i = 0 ; i < M ; i++ ){ vRt1 = findRoot(st[ind[i]]); // disjoint set 的操作 : 找 root vRt2 = findRoot(ed[ind[i]]); if ( vRt1 != vRt2 ){ // 如果 root 不同 => 代表在不同 set=> 做 union set_r[vRt2] = vRt1; choosed[ind[i]] = 1; // 標上選了這條邊 total_cost += cost[ind[i]]; // 加上這條邊的 cost } }}

int findRoot(int idx){ if ( set_r[idx] == -1 ) return idx; return (set_r[idx] = findRoot(set_r[idx])); }

一些參考資料• http://en.wikipedia.org/wiki/Prim%27s_algorithm• http://en.wikipedia.org/wiki/Kruskal%27s_algorithm• http://www.csie.ntnu.edu.tw/~u91029/

SpanningTree.html

• http://en.wikipedia.org/wiki/Minimum_spanning_tree• http://www.cyut.edu.tw/~ckhung/b/al/graph.php

看完影片你應該要知道• 什麼是 Spanning Tree• MST 的問題定義是什麼• Prim 演算法的操作過程• 「正確聯盟」的概念• Prim 演算法和 Heap 的搭配• Kruskal 演算法的操作過程• Kruskal 演算法儲存圖的方式• Kruskal 演算法和 Disjoint Sets 的搭配• 如何印出兩種演算法的結果 ( 總值跟選那些邊 )