[究极好题][最小生成树]野餐规划 AcWing347

48 篇文章 0 订阅

一群小丑演员,以其出色的柔术表演,可以无限量的钻进同一辆汽车中,而闻名世界。

现在他们想要去公园玩耍,但是他们的经费非常紧缺。

他们将乘车前往公园,为了减少花费,他们决定选择一种合理的乘车方式,可以使得他们去往公园需要的所有汽车行驶的总公里数最少。

为此,他们愿意通过很多人挤在同一辆车的方式,来减少汽车行驶的总花销。

由此,他们可以很多人驾车到某一个兄弟的家里,然后所有人都钻进一辆车里,再继续前进。

公园的停车场能停放的车的数量有限,而且因为公园有入场费,所以一旦一辆车子进入到公园内,就必须停在那里,不能再去接其他人。

现在请你想出一种方法,可以使得他们全都到达公园的情况下,所有汽车行驶的总路程最少。

输入格式

第一行包含整数 n,表示人和人之间或人和公园之间的道路的总数量。

接下来 n 行,每行包含两个字符串 A、B 和一个整数 L,用以描述人 A 和人 B 之前存在道路,路长为 L,或者描述某人和公园之间存在道路,路长为 L。

道路都是双向的,并且人数不超过 20,表示人的名字的字符串长度不超过 10,公园用 Park 表示。

再接下来一行,包含整数 s,表示公园的最大停车数量。

你可以假设每个人的家都有一条通往公园的道路。

输出格式

输出 Total miles driven: xxx,其中 xxx 表示所有汽车行驶的总路程。

输入样例:

10
Alphonzo Bernardo 32
Alphonzo Park 57
Alphonzo Eduardo 43
Bernardo Park 19
Bernardo Clemenzi 82
Clemenzi Park 65
Clemenzi Herb 90
Clemenzi Eduardo 109
Park Herb 24
Herb Eduardo 79
3

输出样例:

Total miles driven: 183

题意: 给出n个点的无向图,求s点的度数小于等于k的最小生成树。

分析: 原本的题意比较绕,但是多读几遍就可以转化为上面这个比较简单的题意了。如果删去s的所有连边就会形成若干个连通块,如果连通块个数大于k,那说明这个问题无解,这道题应该是保证了一定有解的,所以也不用考虑无解的情况了。设连通块个数为t,如果对这些连通块分别求最小生成树,然后对于每个连通块都找出一条从s到块内的边,保证这些边都选取尽量小的那条,这就得到了s的度数为t的最小生成树了,之后还有k-t次可以让答案更优的机会,对于每次机会都扫描一遍s的出边,每次机会就只加入最优的那条出边,如何判断它是最优的呢,如果把某条出边加到生成树中,那这时候这条边和其终点to到s的树链一定会形成一个环,而树中是无环的,所以这条边终点to到s的树链就需要断开一条边,显然为了让答案最小需要断开边权最大的边,所以这里还要处理出来每个点到s的树链上边权最大边的编号,这样如果加入这条出边会带来(其权值w减刚才的最大权值)的贡献,这个贡献如果是负数就说明这条边让答案更小了,反之就说明会让答案变大。计算完所有出边能带来的最小贡献后,如果这个最小值是负数那就进行加边和删边操作,否则的话就不进行操作,并且可以直接退出循环了,因为之后一定不可能出现一条出边让贡献为负数(反证法很容易得到这个结论),当执行完k-t次循环后或者中途最小贡献为非负数就退出循环,这时候的答案就是最终答案了。外层的k-t次循环每次一定会加入一条出边,也就是说不会把已有的出边删掉,这里同样用反证法,如果删掉了已有的那条出边就说明当前这条出边权值比已有的那条小,那上一轮选边时就应该选当前这条而不是已有的那条。

接下来证明下为什么这样就能保证最优了。首先要知道一条性质,最小生成树的子树仍然是最小生成树。在每个连通块和s间各加入一条s的出边后,这时候毋庸置疑得到了度为t时的答案,之后每加入一条出边就需要把一个连通块分割成两个小连通块,但无论怎么分割都是把一颗大的最小生成树分成了两颗小的最小生成树,最终每个小连通块内部都是最小生成树了。

具体代码如下: 

#include <iostream>
#include <cstdio>
#include <algorithm>
#include <cstring>
#include <cmath>
#include <string>
#include <map>
#include <vector>
#define inf 0x3f3f3f3f
#define pii pair<int, int>
using namespace std;
//题目相当于求s点的度小于等于k情况下的最小生成树 
int m, k, cnt, s, fa[25], num3, ans, _max[25];//_max[i]记录从s到i树链上最大边编号 
vector<int> g[25];//保存生成树 
map<string, int> mp;
struct edge{
	int u, v, w, pos;//pos表示排序前在e3数组中的下标 
}e3[1005], cpy[1005];//e3保存所有的边 
vector<int> e2;//保存park连边在e3中的编号  

int find(int x){
	if(x == fa[x]) return x;
	return fa[x] = find(fa[x]);
} 

bool cmp(edge a, edge b){
	return a.w < b.w;
}

void kruskal(){
	sort(e3+1, e3+num3+1, cmp);
	for(int i = 1; i <= num3; i++){
		int u = find(e3[i].u), v = find(e3[i].v), w = e3[i].w;
		if(u != v && u != s && v != s){
			fa[u] = v;
			g[e3[i].u].push_back(e3[i].pos);
			//这样建图只是为了方便遍历 
			if(e3[i].pos&1)
				g[e3[i].v].push_back(e3[i].pos+1);
			else
				g[e3[i].v].push_back(e3[i].pos-1);
			ans += w;
		}
	}
}

void dfs(int now, int f){
	if(f != s)
		fa[now] = f;
	for(int i = 0; i < g[now].size(); i++){
		int to = e3[g[now][i]].v, w = e3[g[now][i]].w;
		if(to == f) continue;
		if(e3[_max[now]].w < w) _max[to] = g[now][i];
		else _max[to] = _max[now];
		dfs(to, now);
	}
}

signed main()
{
	cin >> m;
	e2.push_back(0);//方便后续从1开始遍历 
	for(int i = 1; i <= m; i++){
		string u, v;
		int w;
		cin >> u >> v >> w;
		if(v == "Park")
			swap(u, v);
		if(!mp[u])
			mp[u] = ++cnt;
		if(!mp[v])
			mp[v] = ++cnt;
		//方便图的遍历 	
		e3[++num3].u = mp[u], e3[num3].v = mp[v], e3[num3].w = w, e3[num3].pos = num3;
		if(u == "Park") {
			e2.push_back(num3); 
		}
		e3[++num3].v = mp[u], e3[num3].u = mp[v], e3[num3].w = w, e3[num3].pos = num3;
	}
	cin >> k;
	for(int i = 1; i <= cnt; i++)
		fa[i] = i;
	s = mp["Park"];
	memcpy(cpy, e3, sizeof e3);
	kruskal();
	memcpy(e3, cpy, sizeof e3);
	int _min[25] = {0};//到连通块i的最小边的编号 
	e3[0].w = inf;
	for(int i = 1; i < e2.size(); i++){//遍历park的连边
		int id = find(e3[e2[i]].v);//终点连通块编号 
		if(e3[_min[id]].w > e3[e2[i]].w)
			_min[id] = e2[i];
	}
	bool flag[1005] = {false};//表示这条park连边是否已经被选中
	//由遍历的性质,被选中的边不可能被放弃 
	int size = 0;//记录连通块数目 
	for(int i = 1; i <= cnt; i++){
		if(_min[i] != 0){
			int v = e3[_min[i]].v;
			g[s].push_back(_min[i]);
			ans += e3[_min[i]].w;
			flag[_min[i]] = true;
			size++; 
			//park出边的编号一定是奇数 
			g[v].push_back(_min[i]+1); 
		}
	}
	e3[0].w = 0;
	for(int i = 1; i <= k-size; i++){
		memset(_max, 0, sizeof _max);
		for(int j = 1; j <= cnt; j++)
			fa[j] = j;
		dfs(s, 0);
		int get_w = inf, id1, id2;//求最小的贡献,如果是负数就可以更新,id1记录park连边编号,id2记录边的编号 
		for(int j = 1; j < e2.size(); j++){
			if(flag[e2[j]]) continue;
			int to = e3[e2[j]].v, w = e3[e2[j]].w;
			if(get_w > w-e3[_max[to]].w){
				get_w = w-e3[_max[to]].w;
				id1 = e2[j];
				id2 = _max[to];
			}
		}
		//get_w应该一轮比一轮大,否则就矛盾了 
		if(get_w >= 0) break;
		ans += get_w;
		//删边
		int u = e3[id2].u, v = e3[id2].v, pos = e3[id2].pos;
		vector<int> temp;
		for(int j = 0; j < g[u].size(); j++){
			if(e3[g[u][j]].pos != pos)
				temp.push_back(g[u][j]);
		}
		g[u] = temp;
		temp.clear();
		for(int j = 0; j < g[v].size(); j++){
			if((pos&1) && e3[g[v][j]].pos != pos+1)
				temp.push_back(g[v][j]);
			else if(!(pos&1) && e3[g[v][j]].pos != pos-1)
				temp.push_back(g[v][j]);
		}
		g[v] = temp;
		//加边 
		g[s].push_back(id1);
		g[e3[id1].v].push_back(id1+1); 
	}
	printf("Total miles driven: %d\n", ans);
    return 0;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值