AcWing 1144. 连接格点


题目传送门

\(n*m\)的点阵,则点数是\(n*m\)个,由于边权都是正数,所以当连接\(n*m-1\)条边时,边权和才能最小,
其实是在求一个最小生成树问题。

如果是边权可能为负数,则不是最小生成树问题,可以想象一个极端的例子,比如\(5\)个点,边权都是负数,
那么要想使边权和最小,可以想尽办法把所有边都连上,就会最小,这时就不是最小生成树问题了。

本题与上一题很类似,图中都是有两类边,处理方法也很类似,考察的其实是想象能力。图中的点是$ nm $的点阵,连接纵向的两点花费\(1\)个单位,连接横向的两点消耗\(2\)个单位。我们其实并不需要显式的用一个结构体数组或者邻接矩阵去存储这个图,因为点阵是极具规律性的,自左而右,自上而下点\(n*m\)个点依次编号为\(1\)到$ nm$。在读取已有的连线时,获取这两个点的编号,连接这两点即可。后面要考虑的是如何连接剩下的点?

既然连接纵向边的消耗小,我们自然是先枚举纵向边再枚举横向边,按顺序尝试加入到并查集中。如何枚举纵向边,点阵中的纵向边是点\(i\)和点\(i + m\)的连线,其中\(i\)\(1\)\((n - 1) * m\)。横向边则是\(i\)\(i + 1\)的连线,当遍历到编号为\(m\)的倍数的点时跳过即可。

一、原始方法

#include 
using namespace std;
const int N = 1000 * 1000 + 10;
const int M = 2 * N;
int n, m, k;

//二维转一维的办法,坐标从(1,1)开始
inline int get(int x, int y) {
    // m为列宽
    return (x - 1) * m + y;
}
struct Edge {
    int a, b, w;
    //因为没有执行排序动作,所以这个比较函数就用不上了
    bool operator<(const Edge &ed) {
        return w < ed.w;
    }
} e[M];

//并查集
int p[N];
int find(int x) {
    if (p[x] != x) p[x] = find(p[x]);
    return p[x];
}
//先连1的边,再连2的边
void get_edges() {
    for (int i = 1; i <= (n - 1) * m; i++)
        e[k++] = {i, i + m, 1}; // i~i+m是一条纵向边

    for (int i = 1; i <= n * m; i++) {
        if (i % m == 0) continue; //最后一列放过
        e[k++] = {i, i + 1, 2};
    }
    //因为加进去就是按边权由小到大录入的,所以不用再排序了
}
int main() {
    cin >> n >> m;
    //建边
    get_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 < k; i++) {
        int a = find(e[i].a), b = find(e[i].b), w = e[i].w;
        if (a != b) p[a] = b, res += w;
    }
    cout << res << endl;
    return 0;
}

二、优化后的方法

#include 
using namespace std;
const int N = 1010;
int n, m, p[N * N];
int find(int x) {
    if (p[x] != x) p[x] = find(p[x]);
    return p[x];
}
//二维转一维的办法,坐标从(1,1)开始
inline int get(int x, int y) {
    // m为列宽
    return (x - 1) * m + y;
}

int main() {
    cin >> n >> m;
    int a, b, x1, y1, x2, y2;
    //映射(1,1)~(n,m) --->  1 ~ n*m 这样才有点图的意思嘛,图中顶点用个坐标不好整
    for (int i = 1; i <= n * m; i++) p[i] = i; //并查集初始化

    int res = 0;
    while (~scanf("%d%d%d%d", &x1, &y1, &x2, &y2)) {
        //坐标与点号的转换关系(二维转一维)
        a = get(x1, y1), b = get(x2, y2);
        //已存在的边加入一个并查集
        p[find(a)] = find(b);
    }
    // Kruskal算法的核心思想:
    // 1、把边权由小到大排序
    // 2、能用小的连接上的,就不用大的去连

    //为了省一下排序的过程,也是无所不用其极
    //枚举前n-1行的每一个点,因为这些点都可以向下连出一条纵向边
    for (int i = 1; i <= (n - 1) * m; i++) {
        a = find(i), b = find(i + m);   // i~i+m是一条纵向边
        if (a != b) p[a] = b, res += 1; //如果两者没有连通则连接
    }

    //枚举所有节点,要扣除掉最后一列的,因为这一列无法引出横向的边
    for (int i = 1; i <= n * m; i++) {
        if (i % m == 0) continue; //最后一列放过
        a = find(i), b = find(i + 1);
        if (a != b) p[a] = b, res += 2;
    }
    //输出
    cout << res << endl;
    return 0;
}