并查集专题题解

版权声明:吸吸 https://blog.csdn.net/walk_dog/article/details/82793476

#1程序自动分析

在实现程序自动分析的过程中,常常需要判定一些约束条件是否能被同时满足。

考虑一个约束满足问题的简化版本:假设x1,x2,x3…代表程序中出现的变量,给定n个形如xi=xj或xi≠xj的变量相等/不等的约束条件,请判定是否可以分别为每一个变量赋予恰当的值,使得上述所有约束条件同时被满足。例如,一个问题中的约束条件为:x1=x2,x2=x3,x3=x4,x4≠x1,这些约束条件显然是不可能同时被满足的,因此这个问题应判定为不可被满足。

现在给出一些约束满足问题,请分别对它们进行判定。

Input

第1行包含1个正整数t,表示需要判定的问题个数。注意这些问题之间是相互独立的。

对于每个问题,包含若干行:

第1行包含1个正整数n,表示该问题中需要被满足的约束条件个数。接下来n行,每行包括3个整数i,j,e,描述1个相等/不等的约束条件,相邻整数之间用单个空格隔开。若e=1,则该约束条件为xi=xj;若�e=0,则该约束条件为xi≠xj;

Output

输出文件包括t行。

输出文件的第 k行输出一个字符串“ YES” 或者“ NO”(不包含引号,字母全部大写),“ YES” 表示输入中的第k个问题判定为可以被满足,“ NO” 表示不可被满足。

Sample Input

#1
2
2
1 2 1
1 2 0
2
1 2 1
2 1 1

#2
2
3
1 2 1
2 3 1
3 1 1
4
1 2 1
2 3 1
3 4 1
1 4 0

Sample Output

#1
NO
YES
#2
YES
NO

分析

这道题是一道很明显的并查集的题, 也是我做过的最简单的一道并查集 我们对于两个相等的变量就将两个变量加到同一个集合里, 若对于所有“不等”的指令,我们就判断两个数是否在同一个集合里,如果存在则不满足条件。
这道题中x<=1e9, 但是约束条件最大也只有1e5, 所以我们用离散化处理一下。

洛谷传送门

#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cstring>
using namespace std;

const int MAXN = 1e6+7;
int T, n, flag, cnt;
struct Node{
    int x, y, e;
}q[MAXN];
int Tmp[MAXN*3];
int father[MAXN];

//bool comp1 (const int &a, const int &b){ return a < b; }

bool comp (Node const &a, Node const &b) { return a.e > b.e; }

int find(int son)
{
    if(son == father[son]) return son;
    return father[son] = find(father[son]);
}

int main()
{
    scanf("%d", &T);
    while(T--)
    {
        scanf("%d", &n);
        cnt = 0;
        flag = 1;
        memset(q, 0, sizeof(q));
        memset(Tmp, 0, sizeof(Tmp));
        memset(father, 0, sizeof(father));
        for(int i = 1; i <= n; i++)
        {
            scanf("%d%d%d", &q[i].x, &q[i].y, &q[i].e);
            Tmp[++cnt] = q[i].x;
            Tmp[++cnt] = q[i].y;
        }
        sort(Tmp+1, Tmp+cnt+1);
        int Len = unique(Tmp+1, Tmp+cnt+1) - Tmp - 1;
        for(int i = 1; i <= n; i++)
        {
            q[i].x = lower_bound(Tmp+1, Tmp+Len+1, q[i].x) - Tmp - 1;
            q[i].y = lower_bound(Tmp+1, Tmp+Len+1, q[i].y) - Tmp - 1;
        }
        for(int i = 1; i <= Len; i++)
            father[i] = i;
        sort(q+1, q+n+1, comp);
        for(int i = 1; i <= n; i++)
        {
            int fa1 = find(q[i].x);
            int fa2 = find(q[i].y);
            if(q[i].e)
            {
                father[fa1] = fa2;
            }				
            else if(fa1 == fa2)
                {
                    printf("NO\n");
                    flag = 0;
                    break;	
                }
        }
        if(flag) printf("YES\n");
    }
    return 0;
}

#2银河英雄传说

题目背景

背景请移步洛谷
有很多艘银河战舰,对于这些战舰,有两种指令, M i j 表示将i所在的舰队接到j所在的舰队末尾, C i j 表示查询i与j之间相隔了多少艘战舰。

Input

第一行有一个整数T(1≤T≤500,000),表示总共有T条指令。

以下有T行,每行有一条指令。指令有两种格式:

M i j​:i和j是两个整数(1≤i,j≤30000),表示指令涉及的战舰编号。该指令是莱因哈特窃听到的杨威利发布的舰队调动指令,并且保证第ii号战舰与第jj号战舰不在同一列。
C i j:i和j是两个整数(1≤i,j≤30000),表示指令涉及的战舰编号。该指令是莱因哈特发布的询问指令。

Output

依次对输入的每一条指令进行分析和处理:

如果是杨威利发布的舰队调动指令,则表示舰队排列发生了变化,你的程序要注意到这一点,但是不要输出任何信息;

如果是莱因哈特发布的询问指令,你的程序要输出一行,仅包含一个整数,表示在同一列上,第i号战舰与第j号战舰之间布置的战舰数目。如果第i号战舰与第j号战舰当前不在同一列上,则输出−1。

Sample Input

4
M 2 3
C 1 2
M 2 4
C 4 2

Sample Output

-1
1

分析

这道题题目很明显是一道带权并查集的题, 我们定义一个Dist[]数组, 初始化为0, 两艘战舰之间的距离就为(设为x, y) abs(Dist[x]-Dist[y]) - 1。但是在执行M指令的时候需要注意我们需要保存以y为根的子树的大小,将x合并到y的时候,Dist[x] = Size[y], Size[y] += Size[x]。

#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cstring>
using namespace std;

const int MAXN = 5e5+5;
int n, u, v;
char x;
int fa[MAXN], Dist[MAXN], Size[MAXN];

int find(int son)
{
	if(son == fa[son]) return son;
	int root = find(fa[son]);
	Dist[son] += Dist[fa[son]];
	return fa[son] = root;
}

void merge(int x, int y)
{
//	x = find(x), y = find(y);
	fa[x] = y;
	Dist[x] = Size[y];
	Size[y] += Size[x];
}

int main()
{
	scanf("%d", &n);
	for(int i = 1; i <= n; i++)
	{
		fa[i] = i;
		Dist[i] = 0;
		Size[i] = 1;
	}
	for(int i = 1; i <= n; i++)
	{
		cin >> x >> u >> v;
		int fa1 = find(u), fa2 = find(v);
		if(x == 'M')
			merge(fa1, fa2);
		else {
			if(fa1 != fa2)
			{
				printf("-1\n");
				continue;
			}
			printf("%d\n", abs(Dist[u]-Dist[v])-1);
		}
	}
	return 0;
}

#3Parity game

现在小A的手上有一个长度为n(n≤1000000000)的01序列,现在小A给了小Bm个指令形容这个序列,可是我们小B的智商高达1000000%10, 他当然发现小A有可能说谎!那么我们智商高达1000000%10的小B想请智商更高的你来帮助他。如果小A在撒谎,请你帮他求出一个值,满足存在一个序列满足1~k-1个回答, 但不满足1~k个回答。如果小A没有撒谎,就输出m。

Input

第一行一个整数n,表示这个01序列的长度。
接下来一行一个整数m,表示有m个指令。
接下来m行,每行的指令形如l, r, even/odd
even代表[l, r]这个区间内有偶数个1, odd表示[l, r]这个区间内有奇数个1。

Output

一个整数

Sample Input

10
5
1 2 even
3 4 odd
5 6 even
1 6 even
7 10 odd

Sample Output

3

POJ传送门

边带权做法分析

看到这道题,我们第一步应该想到使用并查集+离散化, 然后我们可以得出三个简单易懂的结论:
1.若x1与x2奇偶性相同, x2与x3奇偶性相同,那么x1与x3奇偶性相同。
2.若x1与x2奇偶性相同, x2与x3奇偶性不同,那么x1与x3奇偶性不同。
3.若x1与x2奇偶性不同, x2与x3奇偶性不同,那么x1与x3奇偶性相同。
这样我们就可以得出下面两个结论:
1.[l - r]有偶数个1,就是[1 - l-1]和[l - r]奇偶性相同。
2.[l - r]有奇数个1, 就是[1 - l-1]和[l - r]奇偶性不同。
于是我们用Dist[x] = 0表示x与fa[x]奇偶性相同,Dist[x] = 1表示x与fa[x]奇偶性不同。路径压缩时就用异或运算。
对于第i个l和r,设它们离散化后分别对应x和y,这个区间中有Num个1(0为偶数个,1为奇数个), 若find(x) == find(y), 则判断Dist[x] ^ Dist[y]是否等于Num就行了。若他们不在一个集合内,find[x] = p, find[y] = q; fa[p] = q;Dist[p] = Dist[x] ^ Dist[y] ^ Num;

STD

#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cstring>
using namespace std;

const int MAXN = 2e4+5;
int n, m, cnt, Ans;
char Str[5];
struct Node {
	int Left, Right;
	int Num;
}q[MAXN];
int Tmp[MAXN], fa[MAXN], Dist[MAXN];

int find (int son)
{
	if(son == fa[son]) return son;
	int root = find(fa[son]);
	Dist[son] ^= Dist[fa[son]];
	return fa[son] = root;
}

int main()
{
	scanf("%d%d", &n, &m);
	for(int i = 1; i <= m; i++)
	{
		scanf("%d%d%s", &q[i].Left, &q[i].Right, &Str);
		q[i].Num = (Str[0]=='o' ? 1:0);
		Tmp[++cnt] = q[i].Left-1;
		Tmp[++cnt] = q[i].Right;
	}
	sort(Tmp+1, Tmp+cnt+1);
	n =unique(Tmp+1, Tmp+cnt+1) - Tmp - 1;
	for(int i = 1; i <= n; i++)
		fa[i] = i;
	for(int i = 1; i <= m; i++)
	{
		int x = lower_bound(Tmp+1, Tmp+n+1, q[i].Left-1) - Tmp;
		int y = lower_bound(Tmp+1, Tmp+n+1, q[i].Right) - Tmp;
		int fa1 = find(x), fa2 = find(y);
		if(fa1 == fa2)
		{
			if( (Dist[x]^Dist[y]) != q[i].Num)
			{
				printf("%d", i-1);
				return 0;
			} 
		}
		else {
			fa[fa1] = fa2;
			Dist[fa1] = Dist[x] ^ Dist[y] ^ q[i].Num;
		}
	}
	printf("%d", m);
	return 0;
}

扩展域做法分析

把每个节点x拆成两个节点x_odd表示x的偶数域, x_even表示x的奇数域。
若Num = 0, 合并x_odd和y_odd,x_even和y_even。
若Num = 1, 合并x_odd和y_even, x_even和y_odd。

#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cstring>
using namespace std;

const int MAXN = 2e4+5;
int n, m, cnt, Ans;
char Str[5];
struct Node {
	int Left, Right;
	int Num;
}q[MAXN];
int Tmp[MAXN], fa[MAXN], Dist[MAXN];

int find (int son)
{
	if(son == fa[son]) return son;
	return fa[son] = find(fa[son]);
}

int main()
{	
	scanf("%d%d", &n, &m);
	for(int i = 1; i <= m; i++)
	{
		scanf("%d%d%s", &q[i].Left, &q[i].Right, &Str);
		q[i].Num = (Str[0]=='o' ? 1:0);
		Tmp[++cnt] = q[i].Left-1;
		Tmp[++cnt] = q[i].Right;
	}
	sort(Tmp+1, Tmp+cnt+1);
	n =unique(Tmp+1, Tmp+cnt+1) - Tmp - 1;
	for(int i = 1; i <= 2*n; i++)
		fa[i] = i;
	for(int i = 1; i <= m; i++)
	{
		int x = lower_bound(Tmp+1, Tmp+n+1, q[i].Left-1) - Tmp;
		int y = lower_bound(Tmp+1, Tmp+n+1, q[i].Right) - Tmp;
		int x_odd = x, x_even = x+n;
		int y_odd = y, y_even = y+n;
		if(q[i].Num == 0)
		{
			if(find(x_odd) == find(y_even))
			{
				printf("%d", i-1);
				return 0;
			}
			fa[find(x_odd)] = find(y_odd);
			fa[find(x_even)] = find(y_even);
		} else {
			if(find(x_odd) == find(y_odd))
			{
				printf("%d", i-1);
				return 0;
			}
			fa[find(x_odd)] = find(y_even);
			fa[find(x_even)] = find(y_odd);
		}
		
	}
	printf("%d", m);
	return 0;
}

总结

个人拓展域有些时候比边带权要好做一些,但是消耗的空间更大。

#4 关押罪犯[NOIP2010]

题目

S城现有两座监狱,一共关押着N名罪犯,编号分别为1-N。他们之间的关系自然也极不和谐。很多罪犯之间甚至积怨已久,如果客观条件具备则随时可能爆发冲突。我们用“怨气值”(一个正整数值)来表示某两名罪犯之间的仇恨程度,怨气值越大,则这两名罪犯之间的积怨越多。如果两名怨气值为c的罪犯被关押在同一监狱,他们俩之间会发生摩擦,并造成影响力为c的冲突事件。
在详细考察了N 名罪犯间的矛盾关系后,警察局长觉得压力巨大。他准备将罪犯们在两座监狱内重新分配,以求产生的冲突事件影响力都较小,从而保住自己的乌纱帽。假设只要处于同一监狱内的某两个罪犯间有仇恨,那么他们一定会在每年的某个时候发生摩擦。
那么,应如何分配罪犯,才能使Z 市长看到的那个冲突事件的影响力最小?这个最小值是多少?

Input

第一行为两个正整数N,MN,M,分别表示罪犯的数目以及存在仇恨的罪犯对数。接下来的MM行每行为三个正整数a_j,b_j,c_j,表示a_j号和b_j 号罪犯之间存在仇恨,其怨气值为c_j 。数据保证 1<aj≤bj≤N,0<cj≤1,000,000,000,且每对罪犯组合只出现一次。

Output

共1行,为Z市长看到的那个冲突事件的影响力。如果本年内监狱中未发生任何冲突事件,请输出0。

Sample Input

4 6
1 4 2534
2 3 3512
1 2 28351
1 3 6618
2 4 1805
3 4 12884

Sample Output

3512

分析

这道题是一道扩展域并查集,我们要尽量让两个怨气值大的罪犯在两个监狱,假如现在有一个人 i,还有一个人叫 j,如果 i 和 j 的怨气值很高,那么 i 和 j 肯定不能在一个监狱,那么和 j 在一个监狱的罪犯也不能和 i 在一个监狱(李某东 : 请读者自行证明此结论)

#include<iostream>
#include<cstdio>
#include<algorithm>
using namespace std;

typedef long long ll;
const int MAXN = 1e5+5;
ll n, m;
int fa[MAXN];
struct name {
    ll f, s, c;
}Tree[MAXN];

bool comp(name x, name y){ return x.c > y.c; }

int find(int x)
{
    if(fa[x]==x)return x;
    return fa[x]=find(fa[x]);    
}

int merge(int x,int y)
{
    int ff=find(x),fff=find(y);
    fa[ff]=fff;
}

int main()
{
//	freopen("prison.in","r",stdin);
//	freopen("prison.out","w",stdout);
    scanf("%lld%lld", &n, &m);
    for(int i = 1; i <= 2*n; i++)
        fa[i] = i;
    for(int i = 1; i <= m; i++)
        scanf("%lld%lld%lld", &Tree[i].f, &Tree[i].s, &Tree[i].c);
    sort(Tree+1, Tree+m+1, comp);
    for(int i = 1; i <= m; i++)
    {
        int x = find(Tree[i].f), y = find(Tree[i].s);
        if(x == y)
        {
            cout << Tree[i].c;
            return 0;
        } 
        merge(Tree[i].f, Tree[i].s+n);
        merge(Tree[i].s, Tree[i].f+n);
    }
    cout << 0;
    return 0;
}

#5[待编辑]

猜你喜欢

转载自blog.csdn.net/walk_dog/article/details/82793476