安慰奶牛

如果对最小生成树的Kruskal有了解可以只看黄色字体部分斜体字方便小白加强理解能懂的省时间的可以不看

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 

30 
1 2 5 
2 3 5 
2 4 12 
3 4 17 
2 5 15 
3 5 6 

样例输出 
176 
数据规模与约定 
5 <= N <= 10000,N-1 <= P <= 100000,0 <= Lj <= 1000,1 <= Ci <= 1,000.

拿样例来说:

这里样例输入的初始道路p是7最后数据只给了6组,应该还有一组是4 5 12

5个牧场,和奶牛交谈的时间当作点权(图中黑色数字),通过连接牧场之间的道路时间当作边权(图中蓝色数字)。

最小生成树的Kruskal算法先忽略所有道路的存在,然后以边权从小到大排列来选择道路。因此可以通过对边筛选来确定最终要选择的N-1条道路。

这里的筛选要满足边的两个端点不在同一个连通块中连通块:连通块中的牧场可以通过一条或一条以上的道路自由通行。

    比如1号牧场直接到达2号牧场,则1、 2号牧场是一个连通块;1号牧场通过2号牧场再到达4号牧场,则1、 2、 4号牧场是一个连通块。一开始我们忽略所有边,所以N个牧场就是N个连通块。

如何知道两个端点是否在一个连通块中呢?可以使用并查集数组 father[n]并查集:通过判断两个端点的根结点数字是否相同来确定是否在一个连通块。

    比如1、 2号牧场一开始不是同一个连通块,它们的根结点数字分别是自己的牧场编号1、 2,由于根结点数字不同,所以得知这两个牧场不连通。当选择1、 2号之间的道路后需要让他们成为连通块,这时只需要修改2的根结点为1就行了。

由于每次经过某牧场都要花Ci时间(点权)和奶牛交谈,所以可以得知每经过一条边需要花的时间是边权+两个端点的点权。又因为最后保留的N-1条道路没有形成环,且走完所有牧场后需要回到你选择的某个牧场睡觉,因此可以保证每条边都会被走2次(可以自己画多几个图试试看)。为了方便计算,我们将所有边权更新为:边权 = 端点1的点权 + 端点2的点权 + 边权 × 2(见下图),这样做最后只需用边权相加即可得到最短时间

    这里先给出样例中算法得出的路线供理解:4→5→4→2→1→2→3→2→4,经过的道路有4条,拿边权(蓝色数字)来计算,最终共花30+40+40+60=170的时间(注意不是最终结果),睡觉的牧场是4号。

PS:有人可能疑问两个边权60,拿哪一个相加,注意算法此时已经得出3、 4号牧场是一个连通块了(因为前面选择边权40的道路时已经将3号加入连通块中),而5号未连通,所以选择的是4、 5号之间的60。

最后需要事先选择一个睡觉的牧场(起点)。由于已经解决全部计算问题,包括最终回到睡觉的牧场,所以只需决定最一开始在哪个牧场空降,当然是选择点权最小的牧场啦,所以样例中选择的是4号。

    代码补充说明:sort()函数:对给定区间的元素(e, e+m)进行排序,cmp是排序准则。

#include<iostream>
#include<algorithm>
using namespace std;

struct edge{
	int p1, p2;	//边的两个端点 
	int cost;	//边权 
}e[100010];

bool cmp(edge a, edge b){	//用排序函数将数组e按边权从小到大排序 
	return a.cost < b.cost;
}

int father[10010];      //并查集数组
int findFather(int x){	//判断两个端点根结点是否相同
	return father[x] == x? x: father[x] = findFather(father[x]);
} 

int kruskal(int n, int m){
	int ans = 0, edgeNum = 0;
	for(int i = 0; i < n; i++)	 
		father[i] = i;    //并查集初始化,每个牧场是独立的连通块
	sort(e, e+m, cmp);		
	for(int i = 0; i < m; i++){
		int fa_p1 = findFather(e[i].p1);  //获取端点1的根结点
		int fa_p2 = findFather(e[i].p2);  //获取端点2的根结点
		if(fa_p1 != fa_p2){ 
			father[fa_p2] = fa_p1;	//合并连通块(修改端点p2的根结点使其与p1的一致)
			ans += e[i].cost;
			edgeNum++;
			if(edgeNum == n-1) break;	//边数=顶点数-1时结束循环 
		}
	}
	return ans;
}

int main()
{
	int n, m;
	cin >> n >> m;
	int c[n], st = 1010;    //点权,起点 
	for(int i = 0; i < n; i++){
		cin >> c[i];
		if(c[i] < st)
			st = c[i];    //确保起点点权最小		
	}
	
	for(int i = 0; i < m; i++){
		cin >> e[i].p1 >> e[i].p2 >> e[i].cost;
		e[i].cost = c[e[i].p1-1] + c[e[i].p2-1] + e[i].cost * 2;
	}
	
	int ans = kruskal(n, m);
	cout << st + ans << endl;
	return 0;
}
发布了8 篇原创文章 · 获赞 12 · 访问量 7904

猜你喜欢

转载自blog.csdn.net/qq_45542674/article/details/100038645