Q:
A:
典型最小生成樹問題。
圖的生成樹是一棵含有其所有的頂點的無環聯通子圖,一幅加權圖的最小生成樹( MST ) 是它的一顆權值(樹中所有邊的權值之和)最小的生成樹。
根據題意,我們可以把 N 座城市看成 N 個頂點,連接兩個城市的成本 cost 就是對應的權重,需要返回連接所有城市的最小成本。很顯然,這是一個標准的最小生成樹
注意,圖中邊的頂點是從1開始的,但我們一般從0開始,所以點在存儲時常常要減一
kruskal 算法
既然我們需要求最小成本,那么可以肯定的是這個圖沒有環(如果有環的話無論如何都可以刪掉一條邊使得成本更小)。該算法就是基於這個特性:
按照邊的權重順序(從小到大)處理所有的邊,將邊加入到最小生成樹中,加入的邊不會與已經加入的邊構成環,直到樹中含有 N - 1 條邊為止。這些邊會由一片森林變成一個樹,這個樹就是圖的最小生成樹。
Union類:
class Union {
int count;//樹的個數
int[] root;//每個點的根節點
int[] size;//一棵樹的節點數
Union(int m) {
root = new int[m];
size = new int[m];
for (int i = 0; i < m; i++) {
root[i] = i;//初始點,每個點的根節點都是自己
size[i] = 1;//每棵樹只有1個節點
}
count = m;//總共有m棵樹
}
public void unionF(int i, int j) {
int x = find(i);//i的根節點
int y = find(j);//j的根節點
if (x != y) {
if (size[x] > size[y]) {//x樹更大,把y接上去
root[y] = x;
size[y] += size[x];
} else {//y樹更大,把x接上去
root[x] = y;
size[x] += size[y];
}
count--;
}
}
public int find(int j) {
while (root[j] != j) {
//這句是為了壓縮路徑,不要的話可以跑的通,但效率變低
root[j] = root[root[j]];
j = root[j];
}
return j;
}
public int count() {
return count;
}
public boolean connected(int i, int j) {
int x = find(i);
int y = find(j);
return x == y;
}
}
最小生成書代碼:
public int minimumCost(int N, int[][] connections) {
if (N <= 1)
return -1;
if (connections.length < N - 1)//邊數量小於點-1,不可能構成樹
return -1;
Arrays.sort(connections, Comparator.comparingInt(t -> t[2]));//按權重排序
Union u = new Union(N);
int count = 1;
int res = 0;
for (int[] connect : connections) {
if (u.connected(connect[0] - 1, connect[1] - 1))//兩點曾經連接過,沒必要再連
continue;
u.unionF(connect[0] - 1, connect[1] - 1);
count++;
res += connect[2];
if (count == N)//所有點都連上了
return res;
}
return -1;
}
Prim算法
Prim 算法是依據頂點來生成的,它的每一步都會為一顆生長中的樹添加一條邊,一開始這棵樹只有一個頂點,然后會添加 N - 1 條邊,每次都是將下一條連接樹中的頂點與不在樹中的頂點且權重最小的邊加入到樹中。
算法流程:
- 根據 connections 記錄每個頂點到其他頂點的權重;
- 設計一個flag,判斷是否被讀取過;;
- 每次讀取堆頂元素,如果曾經被讀取過就不再讀取,否則把其所有邊加入堆;
代碼:
public int minimumCost(int N, int[][] connections) {
if (N <= 1 || connections.length < N - 1)//邊數量小於點-1,不可能構成樹
return -1;
HashMap<Integer, ArrayList<int[]>> map = new HashMap<>();//頂和邊
for (int[] connect : connections) {
if (map.containsKey(connect[0])) {
ArrayList<int[]> array = map.get(connect[0]);
int[] c = new int[]{connect[1], connect[2]};
array.add(c);
map.put(connect[0], array);
} else {
ArrayList<int[]> array = new ArrayList<>();
int[] c = new int[]{connect[1], connect[2]};
array.add(c);
map.put(connect[0], array);
}
if (map.containsKey(connect[1])) {
ArrayList<int[]> array = map.get(connect[1]);
int[] c = new int[]{connect[0], connect[2]};
array.add(c);
map.put(connect[1], array);
} else {
ArrayList<int[]> array = new ArrayList<>();
int[] c = new int[]{connect[0], connect[2]};
array.add(c);
map.put(connect[1], array);
}
}
boolean[] flag = new boolean[N];
Arrays.fill(flag, false);//判斷是否讀取過
int start = connections[0][0];//起始點,可以隨意取
flag[start - 1] = true;
int count = 1;
PriorityQueue<int[]> pq = new PriorityQueue<>(Comparator.comparingInt(t -> t[1]));//設計堆
pq.addAll(map.get(start));
int res = 0;
while (!pq.isEmpty()) {//若堆為空,還沒把所有點讀入,說明是無法連接的
int[] c = pq.poll();
if (flag[c[0] - 1])//該邊另一個頂點已經讀取過
continue;
else {
pq.addAll(map.get(c[0]));
flag[c[0] - 1] = true;
count++;
res += c[1];
}
if (count == N)//所有點都被讀取過了
return res;
}
return -1;
}