算法训练 安慰奶牛
问题描述
Farmer John变得非常懒,他不想再继续维护供奶牛之间供通行的道路。道路被用来连接N个牧场,牧场被连续地编号为1到N。每一个牧场都是一个奶牛的家。FJ计划除去P条道路中尽可能多的道路,但是还要保持牧场之间 的连通性。你首先要决定那些道路是需要保留的N-1条道路。第j条双向道路连接了牧场Sj和Ej(1 <= Sj <= N; 1 <= Ej <= N; Sj != Ej),而且走完它需要Lj的时间。没有两个牧场是被一条以上的道路所连接。奶牛们非常伤心,因为她们的交通系统被削减了。你需要到每一个奶牛的住处去安慰她们。每次你到达第i个牧场的时候(即使你已经到过),你必须花去Ci的时间和奶牛交谈。你每个晚上都会在同一个牧场(这是供你选择的)过夜,直到奶牛们都从悲伤中缓过神来。在早上 起来和晚上回去睡觉的时候,你都需要和在你睡觉的牧场的奶牛交谈一次。这样你才能完成你的 交谈任务。假设Farmer John采纳了你的建议,请计算出使所有奶牛都被安慰的最少时间。
输入格式
第1行包含两个整数N和P。
接下来N行,每行包含一个整数Ci。
接下来P行,每行包含三个整数Sj, Ej和Lj。
输出格式
输出一个整数, 所需要的总时间(包含和在你所在的牧场的奶牛的两次谈话时间)。
样例输入
5 7
10
10
20
6
30
1 2 5
2 3 5
2 4 12
3 4 17
2 5 15
3 5 6
4 5 12
样例输出
176
数据规模与约定
5 <= N <= 10000,N-1 <= P <= 100000,0 <= Lj <= 1000,1 <= Ci <= 1,000。
分析:
题目本身少了一组数据,以上补上了。
输入一个无向图,以及每个点安慰奶牛用时间,任务是寻找一个生成树,使你从这个树的某个节点开始按照某种顺序遍历这棵树花费的时间最少。花费时间定义为每条边上的时间和到达某一个节点时(无论你之前是否已经去过)该节点的时间。
首先我们着眼于生成一棵树。有点类似与最小生成树但又不是纯粹的最小生成树。然后注意到题目的关键在于遍历整棵树一次。这意味着什么?不就是每条边都会被走两次吗??!!
然后就是每个点的权值。用草稿纸分析可以得出,树中一个度数为d的节点在遍历的过程中一定走了d次!!!当然,起点会多一次,即d+1次(我们就选最小权值的点当起点)。
首先这个题是最小生成树问题,但是没有现成的生成树,所以我们就先构造一个。对于每条边,如果选的话,则要耗去来回一趟和访问2个端点所用的的时间总合,所以将图中各个边的权值改为边本来的权值的两倍加上访问两端点的值,这样一棵树就构造好。至于住在哪间房里面,选择Ci最小的那一间就可以了。
关于最小生成树的解释(学过的可以跳过)
最小生成树是什么:连接所有点,而且所有边的权值之和最小
1.概览
Kruskal算法是一种用来寻找最小生成树的算法,由Joseph Kruskal在1956年发表。
用来解决同样问题的还有Prim算法和Boruvka算法等。三种算法都是贪婪算法的应用。和Boruvka算法不同的地方是,
Kruskal算法在图中存在相同权值的边时也有效。
2.算法简单描述
1).记Graph中有v个顶点,e个边
2).新建图Graphnew,Graphnew中拥有原图中相同的e个顶点,但没有边
3).将原图Graph中所有e个边按权值从小到大排序
4).循环:从权值最小的边开始遍历每条边 直至图Graph中所有的节点都在同一个连通分量中
if 这条边连接的两个节点于图Graphnew中不在同一个连通分量中
添加这条边到图Graphnew中
第一种解法(也是目前找到通过率最高的解法-70%)
这种解法其实不难,只要理解了根节点那一部分其他的都还好,
解法来源: https://blog.csdn.net/qq_37184747/article/details/72778564
import java.util.Arrays;
import java.util.Scanner;
class Edge implements Comparable<Edge>{
int S;
int E;
int L;
public int compareTo(Edge o) {
if(this.L > o.L)
return 1;
else if(this.L == o.L)
return 0;
else
return -1;
}
}
public class 安慰奶牛 {
public static int[] C = new int[100005];//储存每个牧场里的谈话时间
public static int min = 100005;
public static Edge[] e = new Edge[100005];
public static int N,P;
public static int[] far = new int[100005];//根节点指向
private static int find(int x){
int i, k, r;
r = x;
while(far[r] >= 0)//far[r]就是r指向的根节点(farther节点)
r = far[r]; //跳出的时候,far[r]小于0说明r没有根节点,说明她就是最根的节点
k = x;
while(k != r){//让由x节点一直到最根节点中间所有的节点都指向最根节点
i = far[k];//far[k]已经被储存了
far[k] = r;
k = i;
}
return r;// 返回最根节点
}
static void Union(int S,int E){//将两个节点连接
int rS,rE;
int num;
rS = find(S);//S的最根节点
rE = find(E);
num = far[rS] + far[rE];// 最根节点的指向必为负数
if(far[rS] < far[rE]){//负的越多说明需要被连接的更多,更适合做根节点
far[rE] = rS; //
far[rS] = num; //
}
else{
far[rS] = rE;
far[rE] = num;
}
}
private static int Kruskal(){
int S,E;
int sumweight = 0,count = 0;
for(int i=0;i<N;i++)//初始化far,这是新建的图中父子关系的初始化
far[i] = -1;
//Arrays.sort(e); 这是不对的,因为这里e的数组中有NULL(100005个并没有完全填充完)
//Arrays.sort(e, 0, P-1); 这样也不对,最后一个没排
Arrays.sort(e, 0, P);//对边的权进行由小到大的排序
for(int i=0;i<P;i++){//贪心算法的核心:从权值最小的路开始依次选择,如果包含了所有的点,就退出
S = e[i].S;
E = e[i].E;
if(find(S) != find(E)){//如果S的根节点不等于E的根节点,说明没有生成通路,则选择这条边添加到路线中
sumweight += e[i].L;
Union(S,E);
count++;
if(count >= N-1)//如果有了N-1条通路,说明最小生成树生成了
break;
}
}
return sumweight;
}
public static void main(String[] args){
Scanner sc = new Scanner(System.in);
N = sc.nextInt();//N个牧场
P = sc.nextInt();//P条道路
for(int i=0;i<N;i++){//输入N个牧场奶牛需要谈话的时间
C[i] = sc.nextInt();
if(C[i] < min)//寻找睡觉的地方(最开始当然是需要谈话时间最短的牧场)
min = C[i];
}
for(int i=0;i<P;i++){//输入P条道路行走所需要的时间
e[i] = new Edge();//千万别忘记要new,否则会NoPoint
e[i].S = sc.nextInt()-1;//本身C输入的第一个是指S,E = 1,但是下标却是0
e[i].E = sc.nextInt()-1;// ↓
e[i].L = sc.nextInt()*2 + C[e[i].S] + C[e[i].E];//这里的时间要考虑所有的时间,包括从一个结点到另一个结点再返回所用时间(包括谈话时间)
}
System.out.println(min + Kruskal());
}
}
第二种解法
第二种解法是我找c++版的答案然后修改为Java版的,然而c++版的答案是可以达到100%的通过率,而Java版的只能达到20%,只能说Java和c++的运行速度有区别吧,这里的算法和上面类似,也是使用Kruskal算法构造最小生成树,这里仅为大家做一个思维参考吧
c++版答案地址:https://www.cnblogs.com/zllwxm123/p/8672512.html
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Scanner;
public class Main{
public static void main(String[] args){
Scanner sc = new Scanner(System.in);
int n = sc.nextInt(); // 一共有n头奶牛
int p = sc.nextInt(); // 一共有p条道路
int[] c = new int[n]; // 每头奶牛需要被安慰的时间
int min = 10010;
for(int i=0;i<n;i++){
c[i] = sc.nextInt();
if(c[i]<min)
min = c[i];
}
edge[] path = new edge[p];
int u,v,length; // 路径的起点,终点以及权重
for(int i=0;i<p;i++){
u = sc.nextInt();
v = sc.nextInt();
length = sc.nextInt();
length*=2;
length+=c[u-1]+c[v-1];
path[i] = new edge(u,v,length);
}
Arrays.sort(path); //根据路径权重从小到大排序
int[] flag = new int[n+1];
for(int i=0;i<=n;i++)
flag[i]=i;
int sum=0,num=1,k=0,tp;
while(num<n){
if(flag[path[k].u]!=flag[path[k].v]){
num++;
sum+=path[k].length;
tp = path[k].v;
for(int j=1;j<=n;j++){
if(flag[j]==tp){
flag[j] = flag[path[k].u];
break;
}
}
}
k++;
}
System.out.println(sum+min);
}
}
class edge implements Comparable<edge>{
int u,v;
int length;
public edge(int u,int v,int length){
this.u = u;
this.v = v;
this.length = length;
}
@Override
public int compareTo(edge e) {
return this.length-e.length;
}
}
这里其实还有一个知识点的,那就是怎样实现类排序,在俩种代码实现过程中都有所体现,还不熟悉的可以了解一下