问题描述
栋栋居住在一个繁华的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无法建设码头。
输入保证至少存在一个方法使得任意两个地点能只通过新修的路或者河道互达。
输出格式
输出一行,包含一个整数,表示使得所有地点通过新修道路或者码头连接的最小花费。如果满足条件的情况下还能赚钱,那么你应该输出一个负数。
样例输入
5 5
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。
解题思路:
这题最麻烦的地方就是河道和建码头。题目说所有建了码头的点都可以都过河道链接。所以我们可以把河道虚拟成一个点,所有可以建码头的点都与这个虚拟点有一条边相连接,边的权值就是这个点建码头需要的钱。
建了码头的点就与这个虚拟的点有了链接,它就能与其他建了码头的点相通。这样就把河道和建码头的问题转换为图的一部分,这就变成了纯粹的最小生成树问题。
应当注意:如果选取的边中,只有一条是链接着虚拟点(0点),应当将这条边删除或不算这条边的权值
- 因为如果只有一条边与虚拟点链接,也就是说只有一个点建立了码头
- 而只有一个点建立码头是没有用的,两个点之间要连通需要两个点都有码头
- 这个虚拟点是我们虚拟出来的,它的作用只是更多的点连通,虚拟点本身并不是必须要连通的
- 所以当虚拟点不能帮助我们连通更多点时(即选出的边只有一条与它相连),我们就不需要链接虚拟点
解题代码:
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Scanner;
public class 城市道路建设 {
static int n; //点的数量
static int m; //边的数量
static int[] parent; //用于并查集,存储节点对应的父节点
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
n = scanner.nextInt()+1; //多加一个河道虚拟成的点
m = scanner.nextInt();
parent = new int[n]; //点的序号是从1开始计算的,可以用0号位置作为虚拟点的表示虚拟点
Arrays.fill(parent, -1); //初始化
ArrayList<Edge1> arr = new ArrayList<Edge1>(); //存储边集
ArrayList<Edge1> res = new ArrayList<Edge1>(); //存储选出的边
//接收边的数据
for(int i=0; i<m; i++){
Edge1 temp = new Edge1();
temp.start = scanner.nextInt();
temp.end = scanner.nextInt();
temp.distance = scanner.nextInt();
arr.add(temp);
}
//接收最后一行数据, 每一个数据表示对应点与虚拟点的边的权值,不包含虚拟点(0位置的点)
for(int i=1; i<n; i++){
int t = scanner.nextInt();
if(t==-1){
continue;
}
Edge1 temp = new Edge1();
temp.start = i;
temp.end = 0;
temp.distance = t;
arr.add(temp);
}
Collections.sort(arr); //对边集进行排序
int count = 0; //统计已经选取边的数量
for(int i=0; count<n-1; i++){
Edge1 temp = arr.get(i);
if(union(temp.start, temp.end)){ //并查集判断是否有环
res.add(temp); //添加边
count++;
}
}
//应当注意:如果选取的边中,只有一条是链接着虚拟点(0点),应当将这条边删除或不算这条边的权值
//因为如果只有一条边与虚拟点链接,也就是说只有一个点建立了码头
//而只有一个点建立码头是没有用的,两个点之间要连通需要两个点都有码头
//这个虚拟点是我们虚拟出来的,它的作用只是更多的点连通,虚拟点本身并不是必须要连通的
//所以当虚拟点不能帮助我们连通更多点时(即选出的边只有一条与它相连),我们就不需要链接虚拟点
int c = 0;
int flag = 0; //记录与虚拟点相连边的序号
for(int i=0; i<res.size(); i++){
Edge1 temp = res.get(i);
if(temp.start==0 || temp.end==0){
c++;
flag = i;
}
}
if(c==1){ //只有一条边与虚拟点相连时,flag也只记录了一次
res.remove(flag);
}
//计算总的花费,即计算选取出边的权值之和
int sum = 0;
for(Edge1 e:res){
sum = sum + e.distance;
}
System.out.println(sum);
}
//合并x和y所在的树,合并失败返回false
private static boolean union(int x, int y) {
int xroot = findRoot(x);
int yroot = findRoot(y);
if(xroot!=yroot){ //x和y不在同一个集合(树)
parent[xroot] = yroot; //将x的根节点指向y的根节点
return true;
}
return false;
}
//查找x的根节点
private static int findRoot(int x) {
int t = x;
while(parent[t] != -1){
t = parent[t];
}
return t;
}
}
//边, 实现接口comparable方便排序
class Edge1 implements Comparable<Edge1>{
int start; //边的起点
int end; //边的终点
int distance; //边的权值
public Edge1() { //无参构造
}
public Edge1(int start, int end, int distance) {
super();
this.start = start;
this.end = end;
this.distance = distance;
}
@Override
public int compareTo(Edge1 o) {
//实现升序排序
if(this.distance>o.distance){
return 1;
}
if(this.distance<o.distance){
return -1;
}
return 0;
}
}
注:上面的代码在蓝桥杯官网上测试,只得了60分,有四个测试点没有通过。我认为我的解题思路应该是没有问题,只是某些地方没有考虑到,欢迎各位大佬指出我的bug。