带权并查集小结

带权并查集:通过一个演算来推算!

poj 2492

/**
题意:
找同性恋的昆虫!
分析:
需要在并查集中动态维护
值只有0,1 的数组!可以想象成 0为母的,1为公的!
**/
#include <iostream>
#include <stdio.h>
#include <string.h>

using namespace std;

#define LLEN 2005

int c, g;
int p[LLEN], f[LLEN];

void init() {
	for(int i = 0; i <= c+5; i++) {
		p[i] = i;
		f[i] = 0;
	}
}

int find(int x) {
	if(p[x] != x) {
		int k = p[x]; /**得到x原先祖先的值!**/
		p[x] = find(p[x]);
		f[x] = (f[x] + f[k])%2;/**每次find就动态维护f[x]的“性别”**/
	}
	return p[x];
}

int unionSet(int x, int y) {
	int r1 = find(x);
	int r2 = find(y);
	if(r1 == r2) {/**不同集合的昆虫交配都可以!因为可以通过维护数组f的值来使之成立!**/
		if(f[x] == f[y]) return 1;
		return 0;
	}
	p[r2] = r1;
	f[r2] =~(f[x]^f[y]) ;/**异或这两个交配的昆虫!改变其祖先值性别!使集合的交配成立!**/
	return 0;
}

int main() {
	int t;
	scanf("%d", &t);
	for(int cse = 1; cse <= t; cse++) {
		int flag = 0;
		scanf("%d%d", &c, &g);
		init();
		for(int i = 0; i < g; i++) {
			int a, b;
			scanf("%d%d", &a, &b);
			if(!flag && unionSet(a, b)) flag = 1;
		}
		printf("Scenario #%d:\n", cse);
		if(1 == flag) {
			printf("Suspicious bugs found!\n\n");
		}
		else {
			printf("No suspicious bugs found!\n\n");
		}
	}
	return 0;
}
/**
1 1
0 0
只对于不同集合的并,因为若来自同一集合,那么就找到了同性恋!
所以只是对于不同集合的昆虫,若是这两只昆虫在两个不同集合中,并且他们性别一样,
那么可以通过改变其中一只昆虫的祖先值,改0为1,再在后面的find中动态维护就行了。

0 1
1 0
则可以对于两只昆虫 来自不同或相同集合!
他们的性别相同,就不需要改变祖先的值!

其实这个问题,要找的就是在一个集合中是不是0,1,0,1分布的。
即是不是相邻节点的值是不同的。或者说每加一对昆虫进来,集合能不能达到0,1的平衡!

并且考察 合并 两个同时符合这种分布的集合!

这里的任意集合的祖先值 都是0,合并之后的集合祖先也是0。


怎么想到并查集?
首先又只有公母两种状态,转换为0,1状态!
然后可以想到这是一棵状态树。
但是如果只会有一棵树,那么这题不需要并查集。

这题可能是有多颗树的合并:
那么就有可能要改变根节点(祖先)来得到一棵新的满足条件的状态树!
那么就想到了 并查集了!因为它可以压缩路径来快速的到祖先值!并且可以在
压缩路径时动态维护沿途的 0,1 值!使之满足条件!
**/


poj 1703 --这个只需在上面基础上稍修改就行!

#include <iostream>
#include<cstdio>
using namespace std;
#define MAXN 100100
int  c,b;
int p[MAXN],f[MAXN];
void init() {
	for(int i = 0; i <= c+5; i++) {
		p[i] = i;
		f[i] = 0;
	}
}

int find(int x) {
	if(p[x] != x) {
		int k = p[x]; 
		p[x] = find(p[x]);
		f[x] = (f[x] + f[k])%2;
	}
	return p[x];
}

void unionSet(int x, int y) {
	int r1 = find(x);
	int r2 = find(y);
	p[r2] = r1;
	f[r2] =~(f[x]^f[y]) ;
}

int main()
{
    int t;scanf("%d",&t);getchar();
    while(t--)
    {
        scanf("%d%d",&c,&b);getchar();
        init();
        while(b--)
        {
            char op;
            int x,y;
            scanf("%c%d%d",&op,&x,&y);getchar();
           


            if(op=='A')
            {
               int fa=find(x);
               int fb=find(y);
               if(fa==fb)
               {
                   if(f[x]!=f[y]) printf("In different gangs.\n");
                   else printf("In the same gang.\n");
               }
               else
               {
                   printf("Not sure yet.\n");
               }
            }
            else unionSet(x,y);
        }
    }
    return 0;
}

食物链--通过向量 推算祖先权值!

详见收藏:http://blog.csdn.net/niushuai666/article/details/6981689

/** 收藏中下面这段注释最重要!
仔细再想想,rootx-x 、x-y、y-rooty,是不是很像向量形式?于是我们可以大胆的从向量入手:

tx       ty

|          |

x   ~    y

对于集合里的任意两个元素x,y而言,它们之间必定存在着某种联系,因为并查集中的元素均是有联系的(这点是并查集的实质,要深刻理解),否则也不会被合并到当前集合中。那么我们就把这2个元素之间的关系量转化为一个偏移量(大牛不愧为大牛!~YM)。

由上面可知:
x->y 偏移量0时 x和y同类

x->y 偏移量1时 x被y吃

x->y 偏移量2时 x吃y

有了这个假设,我们就可以在并查集中完成任意两个元素之间的关系转换了。

不妨继续假设,x的当前集合根节点rootx,y的当前集合根节点rooty,x->y的偏移值为d-1(题中给出的询问已知条件)

(1)如果rootx和rooty不相同,那么我们把rooty合并到rootx上,并且更新relation关系域的值(注意:p[i].relation表示i的根结点到i的偏移量!!!!(向量方向性一定不能搞错))

    此时 rootx->rooty = rootx->x + x->y + y->rooty,这一步就是大牛独创的向量思维模式

    上式进一步转化为:rootx->rooty = (relation[x]+d-1+3-relation[y])%3 = relation[rooty],(模3是保证偏移量取值始终在[0,2]间)

(2)如果rootx和rooty相同(即x和y在已经在一个集合中,不需要合并操作了,根结点相同),那么我们就验证x->y之间的偏移量是否与题中给出的d-1一致

    此时 x->y = x->rootx + rootx->y

    上式进一步转化为:x->y = (3-relation[x]+relation[y])%3,
    若一致则为真,否则为假。


**/
#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
#define N 50010

struct node
{
	int pre;
	int relation;
};
node p[N];

int find(int x) //查找根结点
{
	int temp;
	if(x == p[x].pre)
		return x;
	temp = p[x].pre; //路径压缩
	p[x].pre = find(temp);
	p[x].relation = (p[x].relation + p[temp].relation) % 3; //关系域更新
	return p[x].pre; //根结点
}

int main()
{
	int n, k;
	int ope, a, b;
	int root1, root2;
	int sum = 0; //假话数量
	scanf("%d%d", &n, &k);
	for(int i = 1; i <= n; ++i) //初始化
	{
		p[i].pre = i;
		p[i].relation = 0;
	}
	for(int i = 1; i <= k; ++i)
	{
		scanf("%d%d%d", &ope, &a, &b);
		if(a > n || b > n) //条件2
		{
			sum++;
			continue;
		}
		if(ope == 2 && a == b) //条件3
		{
			sum++;
			continue;
		}
		root1 = find(a);
		root2 = find(b);
		if(root1 != root2) // 合并
		{
			p[root2].pre = root1;
			p[root2].relation = (3 + (ope - 1) +p[a].relation - p[b].relation) % 3;
		}
		else
		{
			if(ope == 1 && p[a].relation != p[b].relation)
			{
				sum++;
				continue;
			}
			if(ope == 2 && ((3 - p[a].relation + p[b].relation) % 3 != ope - 1))
			{
				sum++;
				continue;}
		}
	}
	printf("%d\n", sum);
	return 0;
}


poj1988

/**
堆盒子:每M a b操作,都把有a的那一堆放到有b的一堆的上面。
根据题意可知,这个是明显的集合操作!所以用并查集!
怎么解;
我把栈顶作为 祖先!它作为代表元保存了这个栈里有多少元素。
然后只需要维护一个p数组就ok了。
即每个元素到栈顶的距离!
这样每次查找一个元素的下面有多少盒子时,
只需要把 总数-它到顶的距离就 得到答案了。

这里要注意几个问题:
1. find操作里的更新 只有当这个元素的祖先改变时才更新这个元素的p值。
且这个值得回溯得到,并且还要减去1,因为不减就会重复算一次它到原先祖先的值!
2.为什么只有当祖先改变才去改变呢?
首先在合并的时候,就已经把这个元素到祖先的距离P已经算好了。
所以如果祖先没有改变,就不能去再find更新不然会一直重复加这个元素到祖先的值!然而这个祖先又没变!
**/
#include <iostream>
#include<cstdio>
using namespace std;
#define MAXN 40000
int f[MAXN],p[MAXN],num[MAXN];
void init()
{
    for(int i=1;i<MAXN;i++)
    {
        f[i]=i;
        p[i]=1;//每个元素到祖先的距离!
        num[i]=1;//只有祖先才用这个数组
    }
}
int find(int x)
{
    if(x!=f[x]&&f[x]!=f[f[x]])//只有当祖先改变才去改变P!
    {
        int t=f[x];
        f[x]=find(f[x]);
        p[x]+=p[t]-1;//find完再更新,减去1是为了减去重复计算的到原祖先的距离!
    }
    return f[x];
}
void merge(int x,int y)
{
    int fa=find(x);
    int fb=find(y);
    f[fb]=fa;
    p[fb]=num[fa]+1;//把y的祖先到栈顶的值改变!加1就是把自己算进去的到顶的距离!
    num[fa]+=num[fb];//把现在新的栈顶的NUM值维护!
    //cout<<fa<<"  "<<num[fa]<<"  "<<endl;
}
int main()
{
    int n,x,y;scanf("%d",&n);getchar();
    char op;init();
    while(n--)
    {

        scanf("%c%d",&op,&x);getchar();
        if(op=='M')
        {
            scanf("%d",&y); getchar();
            merge(x,y);
        }
        else
        {
            int t=find(x);
            //cout<<"----------"<<t<<"  "<<num[t]<<"  "<<endl;
            //cout<<"----------"<<x<<"  "<<p[x]<<"  "<<endl;
            printf("%d\n",num[t]-p[x]);
        }
    }
    return 0;
}
/** 测试数据
60
M 1 6
C 1
M 2 4
M 2 6
C 3
C 4
M 9 8
C 9
C 8
M 10 8
C 10
C 9
C 8
M 11 8
C 11
C 10
C 9
C 8
M 12 8
C 12
C 11
C 10
C 9
C 8
M 13 8
C 13
C 12
C 11
C 10
C 9
C 8
M 6 8
C 2
C 4
C 1
C 6
C 13
C 12
C 11
C 10
C 9
C 8
C 6
C  12

*/


还有个水题!

hdu1213

#include <iostream>
#include<cstdio>
using namespace std;
int set[1900];
int flag[1900];
int a,b;
void init()
{
    for(int i=1;i<=a;i++) {set[i]=i;flag[i]=0;}
}
int find(int x)
{
    return x==set[x]?x:find(set[x]);
}
void merge(int x,int y)
{
    set[find(y)]=find(x);
}
int main()
{
    int t;scanf("%d",&t);
    while(t--)
    {
        int ans=0;
        scanf("%d%d",&a,&b);
        init();
        for(int i=1;i<=b;i++)
        {
            int x,y;
            scanf("%d%d",&x,&y);
            merge(x,y);
        }
        for(int i=1;i<=a;i++) if(set[i]==i) ans++;
        printf("%d\n",ans);
    }
    return 0;
}




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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值