一、题目
想象一下你是个城市基建规划者,地图上有 N 座城市,它们按以 1 到 N 的次序编号。
给你一些可连接的选项 conections,其中每个选项 conections[i] = [city1, city2, cost] 表示将城市 city1 和城市 city2 连接所要的成本。(连接是双向的,也就是说城市 city1 和城市 city2 相连也同样意味着城市 city2 和城市 city1 相连)。
返回使得每对城市间都存在将它们连接在一起的连通路径(可能长度为 1 的)最小成本。该最小成本应该是所用全部连接代价的综合。
如果根据已知条件无法完成该项任务,则请你返回 -1。
二、示例
- 有结果
输入:N = 3, conections = [[1,2,5],[1,3,6],[2,3,1]]
输出:6
解释:选出任意 2 条边都可以连接所有城市,我们从中选取成本最小的 2 条。
- 无结果
输入:N = 4, conections = [[1,2,3],[3,4,4]]
输出:-1
解释: 即使连通所有的边,也无法连接所有城市。
三、分析
典型最小生成树问题。
图的生成树是一棵含有其所有的顶点的无环联通子图,一幅加权图的最小生成树( MST ) 是它的一颗权值(树中所有边的权值之和)最小的生成树。
根据题意,我们可以把 N 座城市看成 N 个顶点,连接两个城市的成本 cost 就是对应的权重,需要返回连接所有城市的最小成本。很显然,这是一个标准的最小生成树
注意,图中边的顶点是从1开始的,但我们一般从0开始,所以点在存储时常常要减1。
四、解法1-kruskal 算法
既然我们需要求最小成本,那么可以肯定的是这个图没有环(如果有环的话无论如何都可以删掉一条边使得成本更小)。
该算法就是基于这个特性:按照边的权重顺序(从小到大)处理所有的边,将边加入到最小生成树中,加入的边不会与已经加入的边构成环,直到树中含有 N - 1 条边为止。这些边会由一片森林变成一个树,这个树就是图的最小生成树。
package com.test;
import java.util.Arrays;
import java.util.Comparator;
public class Test {
public static void main(String[] args) {
int N = 3;
int[][] connections = { { 1, 2, 5 }, { 1, 3, 6 }, { 2, 3, 1 } };
System.out.println(minimumCost(N, connections));
}
public static int minimumCost(int N, int[][] connections) {
if (N <= 1) {
return -1;
}
// 边数量小于点-1,不可能构成树
if (connections.length < N - 1) {
return -1;
}
// 按权重排序
Arrays.sort(connections, Comparator.comparingInt(item -> item[2]));
Union union = new Union(N);
//总边数
int count = 0;
//总权重
int res = 0;
for (int[] connect : connections) {
int start = connect[0] - 1;
int end = connect[1] - 1;
// 判断两点曾经连接过,没必要再连
if (union.isConnected(start, end)) {
continue;
}
//两点建立连接
union.setConnected(start, end);
res += connect[2];
count++;
if (count == (N-1) ) {
return res;
}
}
return -1;
}
}
class Union {
// 树的个数
private int treeCount;
// 每个树的节点数
private int[] treeSize;
// 每个树的根节点
private int[] treeRoot;
public Union(int count) {
// 总共有count棵树
treeCount = count;
treeRoot = new int[count];
treeSize = new int[count];
for (int i = 0; i < count; i++) {
// 每棵树只有1个节点
treeSize[i] = 1;
// 初始点,每个点的根节点都是自己
treeRoot[i] = i;
}
}
//查找树的根节点
public int findRoot(int j) {
while (treeRoot[j] != j) {
// 这句是为了压缩路径,不要的话可以跑的通,但效率变低
//treeRoot[j] = treeRoot[treeRoot[j]];
j = treeRoot[j];
}
return j;
}
//判断两点是否存在连接
public boolean isConnected(int i, int j) {
int x = findRoot(i);
int y = findRoot(j);
//根节点一致,说明两点已经存在连接
return x == y;
}
public void setConnected(int i, int j) {
int x = findRoot(i);// i的根节点
int y = findRoot(j);// j的根节点
if (x != y) {
if (treeSize[x] > treeSize[y]) {// x树更大,把y接上去
treeRoot[y] = x;
treeSize[y] += treeSize[x];
} else {// y树更大,把x接上去
treeRoot[x] = y;
treeSize[x] += treeSize[y];
}
treeCount--;
}
}
}
五、解法2-Prim算法
Prim 算法是依据顶点来生成的,它的每一步都会为一颗生长中的树添加一条边,一开始这棵树只有一个顶点,然后会添加 N - 1 条边,每次都是将下一条连接树中的顶点与不在树中的顶点且权重最小的边加入到树中。
算法流程:
- 根据 connections 记录每个顶点到其他顶点的权重;
- 设计一个flag,判断是否被读取过;
- 每次读取堆顶元素,如果曾经被读取过就不再读取,否则把其所有边加入堆;
public class MinCityLineTest {
public static void main(String[] args) {
int N = 3;
int[][] connections = {{1, 2, 5}, {1, 3, 6}, {2, 3, 1}};
System.out.println(minimumCostPrim(N, connections));
}
public static int minimumCostPrim(int N, int[][] connections) {
//边数量小于点-1,不可能构成树
if (N <= 1 || connections.length < N - 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;
}
}