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