愚人并查集

BASIC

引入:条件中给出了两两关系,且关系具有传递性,最后问某两是否有关系。

想法:让有关系的抱团,均指向团长。查询时,看是否是同一个团长即可。

基本操作:
*1. 初始化:每个人都是自己的团长。

for(int i = 0;i < n;i++)
    p[i] = i;

*2. 找团长:存在长链时,a指向的只是它的熟人,并不是这个团的团长。a想打破垄断,越级走访。顺带着链上的所有人都找到了团长。

int get(int a){
    if(p[a] != a) p[a] = get(p[a]);
    return p[a];
}

*3. 询问:a和b是不是同一个团的?问问团长!

bool ask(int a, int b){
    return get(a) == get(b);
}

*4. 合并:商量好了要团结,搞个大新闻,先联系自己团的团长再联合。

void merge(int a, int b){
    int x = get(a), y = get(b);
    p[x] = y;
    //有时可以加上资源的合并
}

EXERCISES

POJ 1611 感染病毒

先来切个菜。求与编号0一起被感染的人有多少个。

#include <cstdio>
using namespace std;
#define N 30010
int p[N], a[N], n, m, num[N];
int get(int x){
    if(x != p[x]) p[x] = get(p[x]);
    return p[x];
}
void merge(int x, int y){
    int s = get(x), r = get(y);
    if(s == r) return;
    p[s] = r;
    a[r] += a[s];
}
int main(){
    while(scanf("%d%d", &n, &m)){
        if(!n && !m) break;
        for(int i = 0;i < n+1;i++){
            p[i] = i;
            a[i] = 1;
        }
        while(m--){
            int k;
            scanf("%d", &k);
            for(int i = 0;i < k;i++)
                scanf("%d", &num[i]);
            for(int i = 1;i < k;i++)
                merge(num[i], num[0]);
        }
        printf("%d\n", a[get(0)]);
    }
    return 0;
}

POJ 1988 积木移动

合理构造有助于求解。每次移动以后,让最底下的积木作为团长。
p[i]: i所在堆的团长,初始为i
a[i]: i底下有多少个积木,初始为0
s[i]: i为团长时,表示这堆积木的总数,初始为1
每次get时,也要更新a的值,让a[i]为i的熟人到新团长的链上的a[j]之和,不含新团长。
“该链上只有i,老团长,新团长,所以j就是老团长这一个”,对吗?

int get(int x){
    if(p[x] != x){
        a[x] += a[p[x]];
        p[x] = get(p[x]);
    }
    return p[x];
}

事实上,多次merge可能会造成长链,上面的写法是错滴!比较好的递归写法如下:

int get(int x){
    if(p[x] != x){
        int t = get(p[x]);
        a[x] += a[p[x]];
        p[x] = t;
    }
    return p[x];
}

就差一丢丢,先后次序决定了p[x]已经被先行处理,a[p[x]]值发生了改变。写不出来的话,还有再傻一点的写法如下:

int get(int x){
    if(p[x] != x){
        int t = get(p[x]), y = x;
        while(p[y] != t){
            a[x] += a[p[y]];
            y = p[y];
        }
        p[x] = t;
    }
    return p[x];
}

会造成长链的重复计算,虽然没有超时,但不是最佳写法。

#include <cstdio>
using namespace std;
#define N 30010
int p[N], a[N], s[N];
int get(int x){
    if(p[x] != x){
        int t = get(p[x]), y = x;
        while(p[y] != t){
            a[x] += a[p[y]];
            y = p[y];
        }
        p[x] = t;
    }
    return p[x];
}
void merge(){
    int x, y;
    scanf("%d%d", &x, &y);
    int xp = get(x), yp = get(y);
    if(xp == yp) return;
    p[xp] = yp;
    a[xp] += s[yp];
    s[yp] += s[xp];
}
void ask(){
    int x;
    scanf("%d", &x);
    p[x] = get(x);
    printf("%d\n", a[x]);
}
int main(){
    int T;
    while(~scanf("%d", &T)){
        for(int i = 0;i < N;i++){
            p[i] = i;
            a[i] = 0;
            s[i] = 1;
        }
        while(T--){
            char str[10];
            scanf("%s", str);
            if(str[0] == 'M')
                merge();
            else
                ask();
        }
    }
    return 0;
}

睡个好觉,好题再续~

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值