圖論 - 最小生成樹 - Kurskal算法 - 道路升級
問題描述
Z國有 n 個城市和 m 條雙向道路,每條道路連接了兩個不同的城市,保證所有城市之間都可以通過這些道路互達。每條道路都有一個載重量限制,這限制了通過這條道路的貨車最大的載重量。道路的編號從 1 至 m 。巧合的是,所有道路的載重量限制恰好都與其編號相同。
現在,要挑選出若幹條道路,將它們升級成高速公路,並滿足如下要求:
- 所有城市之間都可以通過高速公路互達。
- 對於任意兩個城市 u,v 和足夠聰明的貨車司機:只經過高速公路從 u 到達 v 能夠裝載貨物的最大重量,與經過任意道路從 u 到達 v 能夠裝載貨物的最大重量相等。(足夠聰明的司機只關註載重量,並不在意繞路)
在上面的前提下,要求選出的道路數目盡可能少。
求需要挑選出哪些道路升級成高速公路(如果有多種方案請任意輸出一種)。
輸入
第一行 2 個用空格隔開的整數 n,m ,分別表示城市數目、道路數目。
第 2 行到第 m+1 行,每行 2 個用空格隔開的整數 u,v 描述一條從 u 到 v 的雙向道路,第 i+1 行的道路的編號為 i 。
註意:數據只保證不存在連接的城市相同的道路(自環),並不保證不存在兩條完全相同的邊(重邊)
輸出
第一行一個整數 k ,表示升級成高速公路的道路數。
接下來 k 行每行一個整數,從小到大輸出所有選出的道路的編號。
輸入樣例
3 3
1 2
2 3
1 3
輸出樣例
2
2
3
數據範圍
對於 20% 的數據,保證 n≤5,m≤10。
對於 60% 的數據,保證 n≤1,000,m≤5,000。
對於 100% 的數據,保證 n≤200,000,m≤400,000。
時間限制:10 sec
空間限制:256 MB
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
思路:
此題為最小生成樹算法的推廣習題,其實是求解最大生成樹。
問題抽象:將所有城市看作結點,道路看作邊,載重量看作邊的權值,則所有城市和道路構成一張連通帶權圖。根據題目要求,我們可知,題目要從所有給出的邊中選擇出一些邊,這些邊要滿足以下要求: ① 這些邊使所有結點構成一連通圖。② 該連通圖中任意兩個結點間存在一條通路,且該通路上邊的權值和是所有原圖中這兩個點間所有可能通路中路徑權值最大的。③ 滿足前兩條前提下所選擇的邊盡可能少。
該問題其實是求解原圖的一棵最大生成樹。類似最小生成樹,可用Kurskal算法求解。
C++代碼:
#include <cstdio> #include <cstring> #include <vector> using namespace std; const int MAX_ROAD = 400001; int nl[MAX_ROAD], nr[MAX_ROAD]; // nl[i], nr[i]分別記錄第i條道路的兩個端點 /* 並查集 */ class DisjointSet { private: unsigned int size; // 並查集中所有端點數,集合中元素分別為 1 ~ n。 int * a; // 用於保存並查集,a[i] = j (j > 0) 表示元素i所在集合的父結點是元素j;若a[i] = -k,表示元素i所在的集合其為根,其集合大小為k。 public: DisjointSet() { size = 0; a = NULL; } DisjointSet(unsigned int _size) { size = _size; a = new int[size+1]; memset(a, -1, (size+1)*sizeof(int)); // 初始化各元素各成一個集合,且集合大小均為1。 } ~DisjointSet() { delete [] a; } int find(int x) // 尋找編號為x的元素所在的集合的根,若返回-1表示沒找到。 { if ( x <= 0 || x > size ) // 若x超出合法範圍 return -1; if ( a[x] < 0 ) // x即為根 return x; a[x] = find(a[x]); // 路徑壓縮 return a[x]; } bool unionset(int x, int y) // 將編號為x和y的元素所在的集合合並,成功則返回true,否則返回false。 { int rx = find(x); int ry = find(y); if ( rx < 0 || ry < 0 || rx == ry ) // 有任意元素查找失敗,或者【兩個元素已在同一集合中】則什麽也不做 return false; if ( a[rx] < a[ry] ) // x所在集合規模大 { a[rx] += a[ry]; a[ry] = rx; } else { a[ry] += a[rx]; a[rx] = ry; } return true; } }; int main() { int n_city = 0, n_road = 0; // 城市數,道路數 scanf("%d %d", &n_city, &n_road); // 讀入城市數,道路數 for ( int i = 1; i <= n_road; ++i ) // 讀入道路i連接的城市 scanf("%d %d", nl+i, nr+i); DisjointSet c(n_city); int n_choice = 0; // 選擇的道路數 vector<int> ans; // 存儲選擇的道路編號,編號從大到小。 /* Kruskal算法求最大生成樹,由於此題道路權重即編號,故自然有序,不需要用堆存儲道路 */ for ( int i = n_road; n_choice < n_city && i > 0; --i ) // 若n_choice = n_city - 1,則已得到最大生成樹,不必繼續做下去。 { int sl = c.find(nl[i]), sr = c.find(nr[i]); // 找到道路兩個端點的根 if ( sl != sr ) // 若道路左右兩個城市不在一個集合,則它們選擇該邊,並將兩個集合合並 { ++n_choice; ans.push_back(i); c.unionset(sl, sr); } } printf("%d\n", n_choice); for ( int i = n_choice - 1; i >=0; --i ) printf("%d\n", ans[i]); return 0; }
圖論 - 最小生成樹 - Kurskal算法 - 道路升級