王道机考系列——图论之并查集与最小生成树

王道论坛机考系列——图论之并查集与最小生成树

并查集

相关概念与操作

用集合来表示一种数据结构,用以实现如确定某个集合中含有哪些元素、判断某两个元素是否在同一个集合中、求集合的数量等。使用双亲节点表示法来表示一棵树,即每个节点都保存其双亲节点,如用数组来表示以下的图,得到的结果为:

图片
保存结果:

1234
-1113

两棵树的合并就是让其中一棵树称为另一颗树根节点的子树,在合并的过程中为避免极端的情况,还需要一遍合并一遍压缩路径。详情如下所示:

首先定义一个数组,用双亲表示法来表示各棵树,所有元素集合总数为N:

    int Tree[N]

查找某个节点的根节点,边查找,边压缩路径:

int FingRoot(int x) {
    if (Tree[x] == -1) return x;
    else {
        int tmp = FindRoot(x);
        Tree[x] = tmp;
        return tmp;
    }
}

畅通工程

分析:这道题目就是求将图中连通分量的个数。

方法一:

#include <stdio.h>
#define N 1000
#define NU -2
int Tree[N] = {0};

void Find(int n, int &road) {
    //  将图中非连通的图合并起来
    int num = 0;
    int root[2] = {0};
    for (int i = 1; i <= n; i++) {
        if (num == 2) {
            //  找到了两个非连通的图,合并起来;
            //  其中一个图根节点成为另一个图根节点的子树
            Tree[root[1]] = root[0];
            num -= 1;
            road++;
            //  每合并两个图,路径就加1
        }
        if (Tree[i] == -1) {
            //  寻找根节点,并记录根节点下标
            root[num++] = i;
        } else if (Tree[i] == -2) {
            road++;
        }
    }
    if (road == n)
        road -= 1;
}

int main() {
    int n, m;
    
    while(scanf("%d%d", &n, &m) != EOF) {
        if (n != 0) {
            for(int i = 0; i < N; i++)
                Tree[i] = NU;  //  最开始的每个节点都没有保存任何节点
            int road = 0;
            int tmp = m;
            while (tmp--) {
                int n1, n2;
                scanf("%d %d", &n1, &n2);
                //第一个节点已经在图中,那么把第二个节点添加上去就好了
                if (Tree[n1] != -2)
                    Tree[n2] = Tree[n1];  //  n2的根节点与n1相同
                else if (Tree[n2] != -2)
                    Tree[n1] = Tree[n2];  //  n1的根节点与n2相同
                else if (Tree[n1] == -2 && Tree[n2] == -2) {
                    Tree[n1] = -1;
                    Tree[n2] = n1;
                    //  两个节点都还没有在图中,设置其中一个为根节点
                }
            }
            
            Find(n, road);
            
            printf("%d\n", road);
        } 
        else break;
    }
    return 0;
}

方法二:

#include <stdio.h>
#define N 1000
#define NU -2
int Tree[N] = {0};

int FindRoot(int x) {
    if (Tree[x] == -1) return x;
    else {
        //  寻找父节点的根
        int tmp = FindRoot(Tree[x]);
        //  根节点设置为父节点的根
        Tree[x] = tmp;
        return tmp;
    }
}

int main() {
    int n, m;
    
    while(scanf("%d", &n) != EOF && n != 0) {
        scanf("%d", &m);
        for (int i = 0; i < N; i++)
            Tree[i] = -1;  //  最开始的时候每个节点都是孤立的
        while(m--) {
            int n1, n2;
            int root1, root2;
            scanf("%d %d", &n1, &n2);
            root1 = FindRoot(n1);
            root2 = FindRoot(n2);
            //  如果两个节点不在一个集合,就放到一起
            if (root1 != root2)
                Tree[root1] = root2;
        }

        int road = 0;  // 计算连通分量的个数
        for (int i = 1; i <= n; i++)
            if (Tree[i] == -1)
                road++;
        
        //  需要新加的路径数等于连通分量的个数减1
        printf("%d\n", road - 1);
    }
    return 0;
}

more is better

#include <stdio.h>
#define N 10000001
int Tree[N];   // 记录的是父亲节点
int num[N];   //  表示每个连通分量的节点个数

int FindRoot(int x) {
    if (Tree[x] == -1) return x;
    else {
        //  寻找父节点的根
        int tmp = FindRoot(Tree[x]);
        //  根节点设置为父节点的根
        Tree[x] = tmp;
        return tmp;
    }
}

int main() {
    int n;
    
    while(scanf("%d", &n) != EOF && n != 0) {
        for (int i = 0; i < N; i++) {
            Tree[i] = -1;  //  最开始的时候每个节点都是孤立的
            num[i] = 1;    //  每个集合的节点个数为1
        }

        int tmp = n;
        while(tmp--) {
            int n1, n2;
            int root1, root2;
            scanf("%d %d", &n1, &n2);
            root1 = FindRoot(n1);
            root2 = FindRoot(n2);
            if (root1 != root2) {
                //  如果两个节点不在一个集合,就放到一起
                //  root1的父亲节点现在变成了root2.
                Tree[root1] = root2;
                //  root2的节点数量增加
                num[root2] += num[root1];
            }
        }

        int ans = 1;  //  节点的数量为1
        for (int i = 1; i <= N; i++)
            if (Tree[i] == -1 && ans < num[i])
                ans = num[i];
        
        //  需要新加的路径数等于连通分量的个数减1
        printf("%d\n", ans);
    }
    return 0;
}

最小生成树

还是畅通工程——Kruskal算法

#include <stdio.h>
#include <limits.h>
#define N 100

int Tree[N];   // 记录的是父亲节点
int arr[N][N]; //存储两个村庄之间的距离
bool mark[N][N];  //标记是否选过路径<vi, vj>
int FindRoot(int x) {
    if (Tree[x] == -1) return x;
    else {
        //  寻找父节点的根
        int tmp = FindRoot(Tree[x]);
        //  根节点设置为父节点的根
        Tree[x] = tmp;
        return tmp;
    }
}

int Kruskal(int n) {
    int road = 0;  // 记录已经选定的路径的条数
    int minlen = 0;  // 记录最小的公路长度
    int min = INT_MAX;
    int n1 = 0, n2 = 0;
    while (road < n - 1) {
        //  寻找没有被选择过的最小的边
        min = INT_MAX;
        for (int i = 1; i <= n; i++)
            for (int j = 1; j <= n; j++)
                if (!mark[i][j] && min > arr[i][j]) {
                    n1 = i;
                    n2 = j;
                    min = arr[i][j];
                }
        
        mark[n1][n2]= mark[n2][n1] = true;
        int root1 = FindRoot(n1);
        int root2 = FindRoot(n2);
        if (root1 != root2) {
            //  如果两个节点不在一个集合,就放到一起
            //  root1的父亲节点现在变成了root2.
            Tree[root1] = root2;
            //  root2的节点数量增加
            road++;
            minlen += arr[n1][n2];
        }
    }
    return minlen;
}

int main() {
    int n;
    
    while(scanf("%d", &n) != EOF && n != 0) {
        
        for (int i = 0; i < N; i++) {
            Tree[i] = -1;  //  最开始的时候每个节点都是孤立的
            for (int j = 0; j < N; j++) {
                arr[i][j] = INT_MAX;  //  最开始任意两个村庄之间是不连通的
                mark[i][j] = false;  //  最开始的时候任意一条路径都还未被选择
            }
        }
            
        int tmp = n * (n - 1) / 2;
        while(tmp--) {
            int n1, n2, len;
            scanf("%d %d %d", &n1, &n2, &len);
            arr[n1][n2] = len;
            arr[n2][n1] = len;
        }
       
        printf("%d\n", Kruskal(n));
    }
    return 0;
}

Freckles(点)——Kruskal算法

题目: 平面上有若干个点,需要使用线段将这些点连接起来,使得任意两个点之间能够通过一系列的点相连,求一种连接方式使得所有线段的长度和最小。也即,这是一个求最小生成树的问题,首先需要把输入转化为图。

#include <stdio.h>
#include <limits.h>
#include <algorithm>
#include <math.h>
#define N 101
using namespace std;

int Tree[N];   // 记录的是父亲节点
struct DOT {
    //  表示点的结构体
    double x;
    double y;
} Dot[N];

struct EDGE {
    //  表示边的结构体
    int a;  //节点a编号
    int b;  //节点b编号
    double dis;  //节点a,b之间的距离
    bool operator < (const EDGE &E) const {
        return dis < E.dis;
    }
} Edge[N * N];

int FindRoot(int x) {
    if (Tree[x] == -1) return x;
    else {
        //  寻找父节点的根
        int tmp = FindRoot(Tree[x]);
        //  根节点设置为父节点的根
        Tree[x] = tmp;
        return tmp;
    }
}

double Dis(double x1, double y1, double x2, double y2) {
    double dist = sqrt((y2 - y1) * (y2 - y1) + (x2 - x1) * (x2 - x1));
    return dist;
}

double Kruskal(int n) {
    int road = 0;  // 记录已经选定的路径的条数
    double minlen = 0;  // 记录最小的公路长度
    int n1 = 0, n2 = 0;
    int len = n * n;
    for (int i = 1; i <= len; i++) {
        n1 = Edge[i].a;
        n2 = Edge[i].b;
        n1 = FindRoot(n1);
        n2 = FindRoot(n2);
        if (n1 != n2) {
            Tree[n1] = n2;
            minlen += Edge[i].dis;
        }
    }
    return minlen;
}

int main() {
    int n;
    
    while(scanf("%d", &n) != EOF && n != 0) {
        for (int i = 0; i < N; i++)
            Tree[i] = -1;    //最开始所有节点是孤立的
            
        for (int i = 1; i <= n; i++) 
            scanf("%lf %lf", &Dot[i].x, &Dot[i].y);
        
        int index = 1;
        for (int i = 1; i < n; i++) {
            //  计算任意两个节点之间的距离
            for (int j = i + 1; j <= n; j++) {
                Edge[index].a = i;
                Edge[index].b = j;
                Edge[index].dis = Dis(Dot[i].x, Dot[i].y, Dot[j].x, Dot[j].y);
                index++;
            }
        }

        sort(Edge + 1, Edge + index);

        printf("%.2lf\n", Kruskal(index - 1));
    }
    return 0;
}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值