Sicily: 连通性问题(算法逐层优化)

5 篇文章 0 订阅

Description:
关系R具有对称性和传递性。数对p q表示pRq,p和q是0或自然数,p不等于q。
要求写一个程序将数对序列进行过滤,如果一个数对可以通过前面数对的传递性得到,则将其滤去。例如:
输入 输出 连通性
3 4— 3 4
4 9— 4 9
8 0— 8 0
2 3— 2 3
5 6— 5 6
2 9— XX— 2-3-4-9
5 9— 5 9
7 3— 7 3
4 8— 4 8
5 6— XX— 5-6
0 2— XX— 0-8-4-3-2
6 1— 6 1

其中数对2 9和0 2可由之前数对的连通关系得到,故不做输出。

Input
输入共有m行(0<=m<=1000000),每行一个数对,数对的数字之间以1个空格分隔;数对的数字为0或n=100000以内的自然数。

Output
输出包含过滤之后的数对序列。每行输出一个数对,数对的数字之间以1个空格分隔。

Sample Input
3 4
4 9
8 0
2 3
5 6
2 9
5 9
7 3
4 8
5 6
0 2
6 1
Sample Output
3 4
4 9
8 0
2 3
5 6
5 9
7 3
4 8
6 1

看完题目,第一反应就是动态构造图,然后每次输入时进行连通性检查。但是仔细一看,发现根本行不通。

如题目所示,节点数可能达到100000,构造一个邻接矩阵即使是bool类型也显然超出内存限制,同时输入数据可能达到1000000,每一次都进行BFS或DFS连通性检测显然会严重超时。

那么只有走其他方法!

对于连通性检测,如已知图2-3-4-5,显然2-3,2-4,2-5,3-4,3-5,4-5都是需要过滤的,不难发现:2是3,4,5的共同祖先,或者反过来,5是2,3,4的共同祖先。由此可以联想到Union-find,对结点进行分组。

我采用的第一种方法代码如下:

# include <iostream>
# define MAX 100001
using namespace std;

int data[MAX];

int main(void) {
    for (int i = 0; i < MAX; i++) {
        data[i] = i;
    }
    int point1, point2;
    while(cin >> point1 >> point2) {
        int p = data[point1];
        int q = data[point2];
        if (data[point1] != data[point2]) {
            /*cout << "point1" << data[point1] << endl;
            cout << "point2" << data[point2] << endl;*/
            cout << point1 << " " << point2 << endl;
            for (int i = 0; i < MAX; i++) {
                if (data[i] == p) {
                    data[i] = q;
                }
            }
        }
    }
    return 0;
}

起初每个节点都属于自己的组,我就暂且将它们各自的编号定义为它们的index。

当要查询的两个index在数组中的值不同时,说明它们不属于同一个组,应当连通;否则,说明它们已经连通,需要过滤掉。

但有一点要注意,在连通时,应当把两个组内所有成员都进行连通,但是我们又不知道哪些需要连通,只能把数组遍历一遍进行连通。

但是提交后发现代码超时了!

接下来就是改进我的代码了。

很显然就是在连通时遍历数组耗时太长了,每一次连通都把100001个数据遍历一次,实在是资源浪费,因为有时并没有那么多结点。

所以我改进了代码,如下:

# include <iostream>
# define MAX 100001
using namespace std;

int data[MAX];
int _max = 0;

int main(void) {
    for (int i = 0; i < MAX; i++) {
        data[i] = i;
    }
    int point1, point2;
    while(cin >> point1 >> point2) {
        if (point1 > _max) _max = point1;
        if (point2 > _max) _max = point2;
        int p = data[point1];
        int q = data[point2];
        if (data[point1] != data[point2]) {
            cout << point1 << " " << point2 << endl;
            for (int i = 0; i <= _max; i++) {
                if (data[i] == p) {
                    data[i] = q;
                }
            }
        }
    }
    return 0;
}

我用_max变量存储最大结点的值,这样一来就不用将所有数据遍历。

但是一提交,还是超时了!我估计测试数据里可能一早就用到了一个很大很大的结点。所以我改进后的代码还是废柴。

那么,还是需要改进!!!

问题是:怎么才能避免遍历数组?因为不知道哪些结点需要进行连通,才导致我们需要遍历整个数组。那么我们怎样才能找到这些结点而不需要遍历数组呢?

肯定要用到某种数据结构。链表?集合?更深一层想,查找最高效的就是树了。

使用树,我便能层层回溯,找到根结点,也就是它最终所属的组,这样我就可以避免遍历了!

于是我进一步改进了我的代码:

# include <iostream>
# define MAX 100001
using namespace std;

int data[MAX];
int _max = 0;

int find(int num) {
    return (num == data[num] ? num : find(data[num]));
}

int main(void) {
    for (int i = 0; i < MAX; i++) {
        data[i] = i;
    }
    int point1, point2;
    while(cin >> point1 >> point2) {
        int p = find(point1);
        int q = find(point2);
        if (p != q) {
            cout << point1 << " " << point2 << endl;
            data[p] = data[q];
        }
    }
    return 0;
}

本想着这次肯定万无一失能够通过,还是太嫩了!依然超时!

为什么会这样?说明树太深了,导致每次查找根结点的时候耗费的资源太多了!我们必须在查找的过程中将树简化。

于是我就修改了find函数,代码如下:

# include <iostream>
# define MAX 100001
using namespace std;

int data[MAX];
int _max = 0;

int find(int num) {
    return (num == data[num] ? num : data[num] = find(data[num]));  // 此处为修改的地方
}

int main(void) {
    for (int i = 0; i < MAX; i++) {
        data[i] = i;
    }
    int point1, point2;
    while(cin >> point1 >> point2) {
        int p = find(point1);
        int q = find(point2);
        if (p != q) {
            cout << point1 << " " << point2 << endl;
            data[p] = data[q];
        }
    }
    return 0;
}

然后,终于过了,哈哈哈~真是有趣的一道题!


以上内容皆为本人观点,欢迎大家提出批评和指导,我们一起探讨!


  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值