并查集的删除操作

UVA11987--Almost Union-Find

zz:https://blog.csdn.net/scut_pein/article/details/8660719?depth_1-utm_source=distribute.pc_relevant.none-task&utm_source=distribute.pc_relevant.none-task

I hope you know the beautiful Union-Find structure. In this problem, you're to implement something similar, but not identical.

The data structure you need to write is also a collection of disjoint sets, supporting 3 operations:


1 p q : Union the sets containing p and q. If p and q are already in the same set, ignore this command.


2 p q : Move p to the set containing q. If p and q are already in the same set, ignore this command


3 p : Return the number of elements and the sum of elements in the set containing p.


Initially, the collection contains n sets: {1}, {2}, {3}, ..., {n}.


Input Description
There are several test cases. Each test case begins with a line containing two integers n and m (1<=n,m<=100,000), the number of integers, and the number of commands. Each of the next m lines contains a command. For every operation, 1<=p,q<=n. The input is terminated by end-of-file (EOF). The size of input file does not exceed 5MB.
Output Description
For each type-3 command, output 2 integers: the number of elements and the sum of elements.
题目大意:有n个集合,提供三种操作
1 p q:将p所在的集合和q所在的集合并起来
2 p q:将p元素移到集合q所在的集合
3 p:求出p所在集合有多少个元素并输出这些元素的和


Sample Input

5 7
1 1 2  //集合1与2合并(集合操作)
2 3 4  //元素3放到元素4所在的集合中
1 3 5  //将[3,4]与[5]合并,得到[3,4,5]
3 4   //查询4所在的集合
2 4 1 //将元素4移动到1所在集合,得到[1,2,4]
3 4
3 3  //查询3所在的集合[3,5]

Sample Output

3 12
3 7
2 8

 

SOL:

如果进行删除一个点,将其加入到另一个集合时。

会发现如果这个点是开始那个集合中的非叶子点,就很麻烦了。因为“上有老下有小”,“牵一发而动全身”

但如果为叶子点,那就好办多了。

于是对于每个点i,给它“上面”再加一个点N+i出来

这样点i就不会是叶子点了

为了维护这个性质,当点i与点j合并时,我们同时也要保持点J的“叶子”的状态

于是设i的父亲点为N+j就好了。

//假设有5个点,则对1到5这些点,每个点上面还有一个N+i的点
//1上面是6,2上面是7,3上面是8,4上面是9,这样就可以保证我们在删除点的时候
//不会删除根结点了
//如果将1,2,3并在一个集合里面,则(1,2,3,6,7,8)就在一个集合中了
//如果要移动1到4那集合,则先消除它在原集合中的影响 
//然后将1的父亲设为9 
#include <cstdio>
#include <cstring>
const int N = 200005;
int n, m, parent[N], num[N], sum[N];
 
int find(int x) 
{
    return x == parent[x] ? x : parent[x] = find(parent[x]);
}
void init() {
    for (int i = 0; i <= n; i++) 
	{
	parent[i] = parent[i + n] = i + n;
	sum[i] = sum[i + n] = i;
	num[i] = num[i + n] = 1;
    }
}
int main() 
{
    while (~scanf("%d%d", &n, &m)) 
	{
	int q, a, b;
	init();
	while (m--) {
	    scanf("%d", &q);
	    if (q == 1) {
		scanf("%d%d", &a, &b);
		int pa = find(a);
		int pb = find(b);
		if (pa == pb) continue;
		parent[pa] = pb;
		num[pb] += num[pa];
		sum[pb] += sum[pa];
	    }
	    else if (q == 2) {
		scanf("%d%d", &a, &b);
		int pa = find(a);
		int pb = find(b);
		if (pa == pb) continue;
		parent[a] = pb;
		num[pa]--;
		num[pb]++;
		sum[pa] -= a;
		sum[pb] += a;
	    }
	    else {
		scanf("%d", &a);
		int pa = find(a);
		printf("%d %d\n", num[pa], sum[pa]);
	    }
	}
    }
    return 0;
}

  

 Sol:

开始时如最一般的并查集一样开点,对于涉及移动一个元素到另一个集合的操作即第二种操作时.由于不知道这个元素是根结点还是一般的叶子点(叶子点可以直接移过去,所以可以将这个点的在原集合中的影响力清零,然后另新开一个结点将其copy过去,再将新结点并到目标集合中去。

#include <iostream>
#include <cstdio>
using namespace std;
#define maxn 200018
int father[maxn],idx[maxn],num[maxn];
//num来多少个,idx才存
long long int sum[maxn];
int n,m,cnt;
int find(int x)
{
    if(x==father[x])return x;
    return find(father[x]);
}
void init()
{
    for(int i=1;i<=n;i++)
    {
        father[i]=idx[i]=sum[i]=i;
        num[i]=1;
    }
    cnt=n;
}
void Union(int p,int q)
{
    int pp=find(idx[p]),qq=find(idx[q]);
    //统一使用idx[i]代表i目前在哪个集合中
    father[pp]=qq;
    num[qq]+=num[pp];
    sum[qq]+=sum[pp];
}
void Delete(int p)
{
    int pp=idx[p];
    //取出p所在集合的真实的编号,因为存在删除操作,p所在的集合编号是不断变化的
    sum[find(pp)]-=p; //消除其影响力
    num[find(pp)]--;
    idx[p]=++cnt; //新加一个集合出来
    sum[idx[p]]=p; //以下如传统操作一样
    num[idx[p]]=1;
    father[idx[p]]=idx[p];
}
int main()
{
    while(scanf("%d%d",&n,&m)==2)
    {
        init();
        int ope,p,q;
        for(int i=1;i<=m;i++)
        {
            scanf("%d",&ope);
            if(ope==1)
            {
                scanf("%d%d",&p,&q);
                if(find(idx[p])==find(idx[q]))
                continue;
                else
                Union(p,q);
            }
            else if(ope==2)
            {
                scanf("%d%d",&p,&q);
                if(find(idx[p])!=find(idx[q]))
                {
                    Delete(p);
                    Union(p,q);
                }
            }
            else
            {
                int u;
                scanf("%d",&u);
                int fuck=find(idx[u]);
                printf("%d %lld\n",num[fuck],sum[fuck]);
                //这里我用I64d既然WA。。。
            }
        }
    }
    return 0;
}

 另一个题目:Hdu2473

5 6//5个数字(编号从0开始),6个操作
M 0 1//将0与1所在集合,进行合并
M 1 2//将2与1所在集合,进行合并
M 1 3//将3与1所在集合,进行合并
S 1//将1从所在集合中抽出来
M 1 2
S 3
3 1 //第二组数据了
M 1 2
0 0

Sample Output

Case #1: 3 //[0,1,2],[3],[4]这三个集合
Case #2: 2

Sol:感觉没什么好写的。就是开虚点吧
最开始时每个数字所在集合的编号就是其本身的值
但涉及“踢"操作的时候,就将那个点的集合编号换成另一个没有用过的编号。

#include<iostream>
#include<string.h>
#include<stdlib.h>
#include<algorithm>
#include<set>
using namespace std;
int par[100005];
int vis[100005];//记录节点 
int flag;
set<int> S;
void init(int x) 
{
	for (int i = 0; i<x; i++) 
	{
		vis[i] = i;
	}
}
int find(int x) 
{
	if (par[x] == x) 
	    return x;
	else 
		return par[x]=find(par[x]);
}
void unite(int x, int y) 
{
	int a = find(x);
	int b = find(y);
	if (a == b)
	{
			return ;
		}
	else 
	{
		par[a] = b;
	}
	return ;
}
int main()
{  
    int n,m;  
    int sase = 0;  
    while(scanf("%d%d",&n,&m))
	{  
        if(n == 0 && m == 0) break;  
        sase++;  
        int cnt = n;  
        for(int i = 0;i < 100005;i++) 
		   par[i] = i;  
        for(int i = 0;i < n;i++) 
		   vis[i] = i;  
        while(m--)
		{  
            char s[3];  
            int a,b;  
            scanf("%s",s);  
            if(s[0] == 'M')
			{  
                scanf("%d%d",&a,&b);  
                unite(vis[a],vis[b]);
            }  
            else{  
                scanf("%d",&a);  
                vis[a] = cnt++;  
            }  
        }  
        S.clear();  
        for(int i = 0;i < n;i++)
		{  
            S.insert(find(vis[i]));  
//对比一般并查集的写法..if(a[i]==i)ans++,可知vis[]数组的作用 } printf("Case #%d: %d\n",sase,S.size()); } return 0; } ———————————————— 版权声明:本文为CSDN博主「Hugo5332」的原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接及本声明。 原文链接:https://blog.csdn.net/xxxslinyue/article/details/55259798

 Sol:

正确的方法是每一个点都设立一个虚拟父亲比如1,2,3的父亲分别是4,5,6,现在合并1,2,3都在一个集合,那他们的父亲都是4,现在删除1,那就给1重新申请一个节点7

现在2,3的父亲是4,1的父亲是7,删除成功。

#include <stdio.h>
#include <algorithm>
#include <math.h>
#include <string.h>
#include <vector>
#include <queue>
#include <map>
#include <stack>
#include <iostream>
#define pi acos(-1.0)
#define INF 0x3f3f3f3f
using namespace std;
#define ll long long
const int maxn=5000010;
int pre[maxn],id,vis[maxn];
int found(int x)
{
    if(x!=pre[x]) 
	    pre[x]=found(pre[x]);
    return pre[x];
}
void Merge(int a,int b)
{
    int fx=found(a),fy=found(b);
    if(fx!=fy)
        pre[fx]=fy;
}
void del(int x)
{
    pre[x]=id++;
}
int main()
{
   
    int n,m,Case=0;
    while(scanf("%d%d",&n,&m),n+m)
    {
        for(int i=0;i<=n;i++)
            pre[i]=i+n;
        for(int i=n;i<=n+n+m;i++) 
        //本只有N个点,每个点多开一个“上面的”点出来
		//另还有M次操作,预先再多开出M个点来 
            pre[i]=i;
        id=n+n;
        int a,b; char ch[5];
        for(int i=0;i<m;i++)
        {
            scanf("%s",ch);
            if(ch[0]=='M')
            {
                scanf("%d%d",&a,&b);
                Merge(a,b);
            }
            else
            {
                scanf("%d",&a);
                del(a);
            }
        }
        int ans=0;
        memset(vis,0,sizeof vis);
        for(int i=0;i<n;i++)
        {
            int x=found(i);
            if(!vis[x])
                ans++,vis[x]=1;
        }
        printf("Case #%d: %d\n",++Case,ans);
    }
    return 0;
}

  

posted @ 2020-03-11 23:14  我微笑不代表我快乐  阅读(186)  评论(0编辑  收藏  举报