算法笔记_177:历届试题 城市建设(Java)
目录
1 问题描述
问题描述
栋栋居住在一个繁华的C市中,然而,这个城市的道路大都年久失修。市长准备重新修一些路以方便市民,于是找到了栋栋,希望栋栋能帮助他。
C市中有n个比较重要的地点,市长希望这些地点重点被考虑。现在可以修一些道路来连接其中的一些地点,每条道路可以连接其中的两个地点。另外由于C市有一条河从中穿过,也可以在其中的一些地点建设码头,所有建了码头的地点可以通过河道连接。
栋栋拿到了允许建设的道路的信息,包括每条可以建设的道路的花费,以及哪些地点可以建设码头和建设码头的花费。
市长希望栋栋给出一个方案,使得任意两个地点能只通过新修的路或者河道互达,同时花费尽量小。
C市中有n个比较重要的地点,市长希望这些地点重点被考虑。现在可以修一些道路来连接其中的一些地点,每条道路可以连接其中的两个地点。另外由于C市有一条河从中穿过,也可以在其中的一些地点建设码头,所有建了码头的地点可以通过河道连接。
栋栋拿到了允许建设的道路的信息,包括每条可以建设的道路的花费,以及哪些地点可以建设码头和建设码头的花费。
市长希望栋栋给出一个方案,使得任意两个地点能只通过新修的路或者河道互达,同时花费尽量小。
输入格式
输入的第一行包含两个整数n, m,分别表示C市中重要地点的个数和可以建设的道路条数。所有地点从1到n依次编号。
接下来m行,每行三个整数a, b, c,表示可以建设一条从地点a到地点b的道路,花费为c。若c为正,表示建设是花钱的,如果c为负,则表示建设了道路后还可以赚钱(比如建设收费道路)。
接下来一行,包含n个整数w_1, w_2, …, w_n。如果w_i为正数,则表示在地点i建设码头的花费,如果w_i为-1,则表示地点i无法建设码头。
输入保证至少存在一个方法使得任意两个地点能只通过新修的路或者河道互达。
接下来m行,每行三个整数a, b, c,表示可以建设一条从地点a到地点b的道路,花费为c。若c为正,表示建设是花钱的,如果c为负,则表示建设了道路后还可以赚钱(比如建设收费道路)。
接下来一行,包含n个整数w_1, w_2, …, w_n。如果w_i为正数,则表示在地点i建设码头的花费,如果w_i为-1,则表示地点i无法建设码头。
输入保证至少存在一个方法使得任意两个地点能只通过新修的路或者河道互达。
输出格式
输出一行,包含一个整数,表示使得所有地点通过新修道路或者码头连接的最小花费。如果满足条件的情况下还能赚钱,那么你应该输出一个负数。
样例输入
5 5
1 2 4
1 3 -1
2 3 3
2 4 5
4 5 10
-1 10 10 1 1
1 2 4
1 3 -1
2 3 3
2 4 5
4 5 10
-1 10 10 1 1
样例输出
9
样例说明
建设第2、3、4条道路,在地点4、5建设码头,总的花费为9。
数据规模和约定
对于20%的数据,1<=n<=10,1<=m<=20,0<=c<=20,w_i<=20;
对于50%的数据,1<=n<=100,1<=m<=1000,-50<=c<=50,w_i<=50;
对于70%的数据,1<=n<=1000;
对于100%的数据,1 <= n <= 10000,1 <= m <= 100000,-1000<=c<=1000,-1<=w_i<=1000,w_i≠0。
对于50%的数据,1<=n<=100,1<=m<=1000,-50<=c<=50,w_i<=50;
对于70%的数据,1<=n<=1000;
对于100%的数据,1 <= n <= 10000,1 <= m <= 100000,-1000<=c<=1000,-1<=w_i<=1000,w_i≠0。
2 解决方案
此题主要考查Kruskal算法的运用,对于本题包含码头部分需要加一个状态顶点0与相应顶点连接,表示一条边。
注意:此处不单单是求最小生成树,其中权值为负数的边均要修,因为修这条边是百分之百赚钱的,在赚钱的基础上再求取最小生成树,确定每两个顶点均连通。
求取最小生成树时考虑两点:
(1)仅仅只有道路,此时共包含顶点1~n个顶点,顶点个数为n;
(2)包含码头,此处加一个额外状态顶点0,即顶点个数为n + 1,此时要特别注意码头最低要两个才有用,即包含顶点0的边最少要有两条。
下面代码在蓝桥系统中测评分为80分,原因:运行超时,用相应思想的C++代码为100分。(具体C++实现请见文末参考资料)
具体代码如下:
import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.Scanner; public class Main { public static int n, m; public static int[] id; public static ArrayList<edge> list = new ArrayList<edge>(); public static int result = Integer.MAX_VALUE; static class edge { public int a; public int b; public int value; public edge(int a, int b, int value) { this.a = a; this.b = b; this.value = value; } } class MyComparator implements Comparator<edge> { public int compare(edge o1, edge o2) { if(o1.value > o2.value) return 1; else if(o1.value < o2.value) return -1; return 0; } } public int find(int a) { int root = a; while(id[root] >= 0) { root = id[root]; } int i = 0, k = a; while(k != root) { i = id[k]; id[k] = root; k = i; } return root; } public void union(int a, int b) { int rootA = find(a); int rootB = find(b); if(rootA == rootB) return; int num = id[rootA] + id[rootB]; if(id[rootA] < id[rootB]) { id[rootB] = rootA; id[rootA] = num; } else { id[rootA] = rootB; id[rootB] = num; } } public void kruskal() { Collections.sort(list, new MyComparator()); int temp = 0; int count = 0; int count1 = 0;//计算加入状态顶点0时,包含的边数目,若包含状态顶点0,则只是包含两条边 for(int i = 0;i < list.size();i++) { edge p = list.get(i); int a = p.a; int b = p.b; if(find(a) != find(b) && count < n - 1) { temp += p.value; union(a, b); count++; if(a == 0 || b == 0) count1++; } else if(p.value < 0) //此时,修这条路是一定赚钱的,所以必修 temp += p.value; else if(count == n - 1 && p.value > 0) break; } if(count == n - 1 && (count1 == 0 || count1 > 1)) result = Math.min(result, temp); } public static void main(String[] args) { Main test = new Main(); Scanner in = new Scanner(System.in); n = in.nextInt(); m = in.nextInt(); id = new int[n + 1]; for(int i = 0;i <= n;i++) id[i] = -1; for(int i = 0;i < m;i++) { int a = in.nextInt(); int b = in.nextInt(); int c = in.nextInt(); list.add(new edge(a, b, c)); } test.kruskal(); //此处用于寻找不含码头的最后生成树得出的结果 for(int i = 0;i <=n;i++) id[i] = -1; int[] point = new int[n]; //使用码头连通 for(int i = 0;i < n;i++) point[i] = in.nextInt(); for(int i = 0;i < n;i++) { if(point[i] == -1) continue; list.add(new edge(0, i + 1, point[i])); //添加一个顶点0状态地点,所有码头均可到 } n = n + 1; //此处是因为增加了一个状态顶点0,所以n要加1 test.kruskal(); //此处用于寻找包含码头的最小生成树的出的结果 System.out.println(result); } }
参考资料:
每天一小步,成就一大步