You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

109 lines
4.0 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

## [$AcWing$ $1144$. 连接格点](https://www.acwing.com/problem/content/1146/)
### 一、题目描述
有一个 $m$ 行 $n$ 列的点阵,相邻两点可以相连。
一条纵向的连线花费一个单位,一条横向的连线花费两个单位。
某些点之间已经有连线了,试问 **至少** 还需要花费多少个单位才能使所有的点全部连通。
**输入格式**
第一行输入两个正整数 $m$ 和 $n$。
以下若干行每行四个正整数 $x_1,y_1,x_2,y_2$,表示第 $x_1$ 行第 $y_1$ 列的点和第 $x_2$ 行第 $y_2$ 列的点已经有连线。
输入保证$|x_1x_2|+|y_1y_2|=1$。
**输出格式**
输出使得连通所有点还需要的最小花费。
**数据范围**
$1≤m,n≤1000$
$0≤$已经存在的连线数$≤10000$
**输入样例**
```cpp {.line-numbers}
2 2
1 1 2 1
```
**输出样例**
```cpp {.line-numbers}
3
```
### 二、题目解析
$n*m$的点阵,则点数是$n*m$个,由于边权都是正数,所以当连接$n*m-1$条边时,才能都连通,然后边权和最小,其实是在求一个 **最小生成树** 问题。
>**注**:如果是边权可能为 **负数** ,则不是最小生成树问题,可以想象一个极端的例子,比如$5$个点,边权都是负数,
那么要想使边权和最小,可以想尽办法把所有边都连上,就会最小,这时就不是最小生成树问题了。
本题与上一题很类似,图中都是有两类边,处理方法也很类似,**考察的其实是想象能力**。图中的点是$ n*m $的点阵,连接纵向的两点花费$1$个单位,连接横向的两点消耗$2$个单位。我们其实并不需要显式的用一个结构体数组或者邻接矩阵去存储这个图,因为点阵是极具规律性的,自左而右,自上而下点$n*m$个点依次编号为$1$到$ n*m$。在读取已有的连线时,获取这两个点的编号,连接这两点即可。后面要考虑的是如何连接剩下的点?
既然连接纵向边的消耗小,我们自然是**先枚举纵向边****再枚举横向边**,按顺序尝试加入到并查集中。如何枚举纵向边,点阵中的纵向边是点$i$和点$i + m$的连线,其中$i$从$1$到$(n - 1) * m$。横向边则是$i$和$i + 1$的连线,当遍历到编号为$m$的倍数的点时跳过即可。
### $Code$
```cpp {.line-numbers}
#include <bits/stdc++.h>
using namespace std;
const int N = 1000 * 1000 + 10, M = N << 1;
int n, m;
// 二维转一维的办法,坐标从(1,1)开始
int get(int x, int y) {
// m为列宽
return (x - 1) * m + y;
}
struct Edge {
int a, b, c;
const bool operator<(const Edge &t) const {
return c < t.c;
}
} edge[M];
int el;
// 并查集
int p[N];
int find(int x) {
if (p[x] != x) p[x] = find(p[x]);
return p[x];
}
// 先连1的边,再连2的边
void create_edges() {
for (int i = 1; i <= (n - 1) * m; i++) // n-1行的每个点,都可以向下引一条边权为1的边
edge[el++] = {i, i + m, 1}; // i -> i+m,边权为1
for (int i = 1; i <= n * m; i++) { // 向右引边,注意最后一列不能向右引边
if (i % m == 0) continue; // 最后一列放过
edge[el++] = {i, i + 1, 2}; // i->i+1,边权为2
}
// 因为加进去就是按边权由小到大录入的,所以不用再排序了
}
int main() {
cin >> n >> m;
// 建边
create_edges();
// 并查集初始化
for (int i = 1; i <= n * m; i++) p[i] = i;
int x1, y1, x2, y2;
// 利用二维转一维办法,将(x,y)映射成节点编号
// 某些点之间已经有连线了=> 标识这些点已在并查集中
while (cin >> x1 >> y1 >> x2 >> y2) {
int a = get(x1, y1), b = get(x2, y2);
p[find(a)] = find(b);
}
int res = 0; // 用Kruskal算法即可
for (int i = 0; i < el; i++) {
int a = find(edge[i].a), b = find(edge[i].b), c = edge[i].c;
if (a != b) p[a] = b, res += c;
}
cout << res << endl;
return 0;
}
```