1. 问题描述:
有一个 m 行 n 列的点阵,相邻两点可以相连。一条纵向的连线花费一个单位,一条横向的连线花费两个单位。某些点之间已经有连线了,试问至少还需要花费多少个单位才能使所有的点全部连通。
输入格式
第一行输入两个正整数 m 和 n。以下若干行每行四个正整数 x1,y1,x2,y2,表示第 x1 行第 y1 列的点和第 x2 行第 y2 列的点已经有连线。输入保证|x1 − x2| + |y1 − y2| = 1。
输出格式
输出使得连通所有点还需要的最小花费。
数据范围
1 ≤ m,n ≤ 1000
0 ≤ 已经存在的连线数 ≤ 10000
输入样例:
2 2
1 1 2 1
输出样例:
3
来源:https://www.acwing.com/problem/content/description/1146/
2. 思路分析:
分析题目可以知道我们已知在二维矩阵中有n * m个点,在这n * m个点中存在一些必须选择的边,我们需要在剩余不是必选的边中选择权重之和最小的边使得所有的点阵联通,可以发现这道题目类似于1143题联络员,可以使用类似的思路解决,因为是二维平面所以涉及到点的两个坐标,为了方便代码的编写我们可以借助于1131题的思路,将所有二维坐标映射到一维中的一个数字,二维坐标与一维数字是一一对应的关系,这样一个数相当于是一个二维坐标,并且在创建相邻边的时候使用到的一个技巧是枚举上下左右四个方向,只有当往右延伸或者是往下延伸的时候才创建相应的边(a < b);具体的做法:首先将所有已经必选的边加入到并查集中,在并查集查找节点的时候进行路径压缩相当于是一个缩点的过程,将同一个联通块中所有节点的父节点都指向一个节点,然后我们需要创建出非必选的竖边和横边,考虑到边权只有两种,分别是1和2,所以这里使用一个技巧是外面增加多一层循环用来控制创建竖边和横边的顺序,先创建竖边然后再创建横边这样可以避免后面的对边权排序的操作,降低时间复杂度,可以使用kruskal算法来解决最小生成树问题,使用kruskal算法的好处是我们在创建非必选的竖边和横边的时候可以创建所有的竖边和横边,这样可以避免需要判断是否是必选的边,因为后面再做kruskal算法的时候可以通过并查集的find函数判断两个点是否在同一个集合,只有当两个点不在同一个集合才会执行合并操作,所以创建点阵中所有横边和竖边的方法也是正确的并且省略判断的代码,创建好了所有竖边和横边之后做一遍kruskal算法即可,判断当前两个点是否在同一个集合如果不在同一个集合说明需要将当前的边加入到最小生成树中使得两个点联通,并且累加当前边权到答案中即可。
3. 代码如下:
import sys
from typing import List
class Solution:
def getEdges(self, n: int, m: int, fa: List[int], mp: List[List[int]]):
# 下标为0,2属于建立竖的边, 1, 3属于横的边, 坐标与权重要一一对应, 竖的边权重为1横的边权重为2
dx = [-1, 0, 1, 0]
dy = [0, -1, 0, 1]
# 边的权重
dw = [1, 2, 1, 2]
w = list()
# 最外面循环是用来控制创建竖的边和横的边的顺序, z = 0时建立所有竖边, z = 1时建立所有横边, 这样自然避免了对边权排序的步骤, 降低时间复杂度
for z in range(0, 2):
for i in range(1, n + 1):
for j in range(1, m + 1):
# 枚举上下左右四个方向, 创建对应点边
for u in range(4):
# u % 2是控制建立的是竖边还是横边
if u % 2 == z:
x, y, t = i + dx[u], j + dy[u], dw[u]
if 1 <= x <= n and 1 <= y <= m:
a, b = mp[i][j], mp[x][y]
# 只有当a < b的时候才建边, 说明是向右边延伸或者是向下边延伸
if a < b:
w.append((mp[i][j], mp[x][y], t))
return w
# 并查集查找x的父节点并执行路径压缩: fa[x] = self.find(fa[x], fa)
def find(self, x: int, fa: List[int]):
if x != fa[x]:
fa[x] = self.find(fa[x], fa)
return fa[x]
# 这里使用到的一个技巧是先创建权重为1的边然后再创建权重为2的边
def process(self):
n, m = map(int, input().split())
mp = [[0] * (m + 1) for i in range(n + 1)]
count = 0
# 建立二维坐标与一维坐标的映射
for i in range(1, n + 1):
for j in range(1, m + 1):
mp[i][j] = count
count += 1
fa = [i for i in range(n * m + 10)]
while True:
# 用来判断是否存在输入当没有输入的时候退出循环
t = sys.stdin.readline().strip()
if not t: break
# 表示两个点具有联系, 必须需要选择的边
x1, y1, x2, y2 = map(int, t.split())
a, b = self.find(mp[x1][y1], fa), self.find(mp[x2][y2], fa)
if a != b:
fa[a] = b
# 创建竖的边和横的边(也即不是必须要添加的边)
w = self.getEdges(n, m, fa, mp)
res = 0
# kruskal算法有一个好处是在合并前先判断是否属于同一个集合, 所以之前的无脑加入所有的边也是正确的, 可以减少一些判断的代码, 下面只需要判断两个点是否属于同一个集合即可判断之前是否加入过边
for x in w:
a, b = self.find(x[0], fa), self.find(x[1], fa)
if a != b:
res += x[2]
fa[a] = b
return res
if __name__ == "__main__":
print(Solution().process())