【7.27训练——最小生成树】

T1.新的开始

题意分析

初看题,我们可能会以为:将输入的边建图,执行一次最小生成树操作,再加上建立发电站的最小值就是本题答案

但是,按照题意描述,有的矿井建发电站更为划算(不要求只建一个发电站),所以,我们应该思考的是

在不干扰电网的数值情况下又能把发电站融入电网中的办法


法一:建虚点

设置一个虚拟原点 0 或者 n + 1

将每个点与这个虚拟原点建立一条边,边权即为在该点修建发电站的费用

如果有一个矿站是需要单独建发电站更为划算的话,那么我们此时就将原点所在集合与该节点的所在集合合并

值得注意的是,因为有虚拟原点的存在,我们可以确定一个点是自修发电站或相互连接的关系,这样就避免了多算边的可能 性(这也正是虚拟原点的意义所在)

代码实现:

// 最重要的存边操作
for(int i = 1; i <= n; i ++) {
	for (int j = 1; j <= n; j ++) {
		int x;
		scanf("%d", &x);
		edge[++cnt].u = i;
		edge[cnt].v = j;
		edge[cnt].w = x;
	}
} 
for(int i = 1; i <= n; i ++) {
  	// 设置一个虚拟原点,并将每条边与其连接
	edge[++cnt].u = n + 1;
	edge[cnt].v = i;
	edge[cnt].w = f[i];
} 
// 之后是常规的kruskal算法求最小生成树

法二:直接替换

我们注意到,每一个节点都有它被连入时,所需耗费的代价,那么我们可以将其与在该节点设置发电站的代价相比

较,在存边时,如果直接建发电站更为划算,那么存入的权值更改为修建发电站的代价

具体可以理解为:因为发电站本身是可以为该矿井供电的,所以抽象成从i到j的边权为建站费用(不影响整棵树的连接情况)

代码实现:

for (int i = 1; i <= n; i ++) { 
	scanf("%d", &v[i]);
	Min = min(Min, v[i]);
  	// 确定一个起始点(基准点)
	if (Min == v[i]) k = i;
}
for (int i = 1; i <= n; i ++) {
	for (int j = 1; j <= n; j ++) {
		int x;
		scanf("%d", &x);
      	// 存入连接两点的费用
		if (x > v[j]) {
			add_edge(i, j, v[j]);
		} else {
			add_edge(i, j, x); // 直接建站更为划算,抽象理解成边权
		}
	}
}
// 最后是正常的prim算法和累加操作
// 更正(补充),最终的答案注意要加上在基准点建边的费用
// printf("%d", MST + MIN);
prim(k);

T2.最小花费

题意分析,实际上是kruskal算法的模板题,但须注意的是,这道题的读入和每一次数据的初始化


代码参考:

while (scanf("%d", &n) != EOF && n) {
	MST = 0;
	cnt = 0;
	for (int i = 1; i < n; i ++) {
		int x, y;
      	// cin的输入太慢 %c的输入不好调试 可以考虑使用一个短的字符串方式输入
      
		scanf("%s%d", ch, &x);
		for (int j = 1; j <= x; j ++) {
			scanf("%s%d", s, &y);
			edge[++cnt].u = (ch[0] - 'A' + 1);
			edge[cnt].v = (s[0] - 'A' + 1);
			edge[cnt].w = y;
		}
	}
	sort(edge + 1, edge + cnt + 1, cmp);
	kruskal();
	printf("%lld\n", MST);
}

T3.棋盘上的守卫

题意分析:

1.题目大意为:一个守卫通过支付一定的代价可以维护一行或一列,每个位置只能放置一个守卫,求维护一个nn * mm 矩阵的最小代价;

2.分析:

对于每个士兵,我们可以令其维护一行或一列,将图中的行和列看作节点,将当前点看作一条边,计算最小生成树求出最小代价

3.维护:

但是,图中最终存储的是n + m个节点,体现出的便是n + m - 1条边,显然,这与题目要求我们使用n + m个士兵来守卫这个矩阵是不匹配的,这时候就要引入一个新的概念:基环树 ,(稍后会在代码中提及)

4.建图:

将行与列看做图的节点,每一个士兵,看做一条连接其维护的行与列的边;

但由于行与列的编号会重复,所以可以将列数加上行数 n 以维护图,使编号不重复,即对于(i, j)处守卫花费 c,可建立一条边(i, j + n, c)


代码实现:

建图:

for (int i = 1; i <= n; i ++) {
	for (int j = 1; j <= m; j ++) {
		int x;
		scanf("%d", &x);
		edge[++cnt].u = i;
		edge[cnt].v = j + n;
		edge[cnt].w = x;
	}
}

kruskal算法:

void kruskal() {
	MakeSet();
	for (int i = 1; i <= cnt; i ++) {
		int x = FindSet(edge[i].u);
		int y = FindSet(edge[i].v);
      	// 构成一条边的两个节点均被访问过,代表图中已经构成有一个环:基环树
		if (vis[x] && vis[y]) {
			continue; 
		} else if (x == y) { // 一条边的两个节点被合并在了同一集合,加入这条边时,构成基环树
			vis[x] = 1;
			MST += (long long) edge[i].w;
		} else { // 加入这条边,不构成环
			fa[x] = y;
			vis[y] |= vis[x];// 若x与y原集合中已有基环树,则新集合中同样有
			MST += (long long) edge[i].w;
		}
	}
}

// 最后,请注意数据范围->long long


T4. Kuglarz

题意分析:

首先考虑,如果我们想要知道单独一个杯子的奇偶,有两种方式:

1.直接询问c[i][i]

2.由已知的推向未知:(c[i + 1][j], c[i][j]) -> i

如果我们将这个推导方式具体的表现在图上就可以发现:我们将点化为边,i变为从i - 1到i的一条边,当整张图联通时,就可以求得任意一个点的奇偶性


代码实现:

存边:

for (int i = 1; i <= n; i ++) {
	for (int j = 1; j <= n - i + 1; j ++) {
		int x;
		scanf("%d", &x);
		edge[++cnt].u = i - 1;
		edge[cnt].v = i + j - 1;
		edge[cnt].w = x;
	}
}

kruskal算法:

void kruskal() {
	MakeSet();
	for (int i = 1; i <= cnt; i ++) {
		int x = FindSet(edge[i].u);
		int y = FindSet(edge[i].v);
		if (x == y) continue;
		fa[x] = y;
		MST += (long long)edge[i].w;
	}
}

// 注意数据范围->long long


完结

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值