带权并查集

带权并查集需要先理解一般的并查集,不明白的可自行先搜索有关内容

一般的并查集主要记录节点之间的链接关系,而没有其他的具体的信息,仅仅代表某个节点与其父节点之间存在联系,它多用来判断图的连通性,如下图所示,这是一个并查集,其中箭头表示父子关系,可以看到这些边没有记录其他的任何信息。

带权并查集_第1张图片

而有的时候在这些边中添加一些额外的信息可以更好的处理需要解决的问题,在每条边中记录额外的信息的并查集就是带权并查集。

不过在此之前先来看看并查集的路径压缩

在上边这个并查集中,如果对节点C做Find操作,最终会得到A,但是查找的过程中会先经过B,再通过Find(B)得到A,这是一个值得优化的地方,如果直接让C链接到A不是更好吗,这样就可以省去中间的操作,如果C跟A直接相隔很多节点,这个优化就极大地提升了查找的效率,也就是希望得到这样的结果:

带权并查集_第2张图片

将每个节点直接与其Find()操作最终得到的节点链接,就是所谓的路径压缩,这一操作可以直接在Find中完成,看下边的代码:

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

与一般的并查集相比,它只是在find(parent[x])前边加了一步赋值操作,将在查找过程中遇到的所有的节点的父节点都设为最终得到的那个节点。

基于路径压缩,带权并查集就是:

带权并查集_第3张图片

可以看到它的每一条边都记录了每个节点到根节点的一个权值,这个权值该设为什么由具体的问题而定,一般都是两个节点之间的某一种相对的关系,但是考虑到权值就会有两个问题:

1.每个节点都记录的是与根节点之间的权值,那么在Find的路径压缩过程中,权值也应该做相应的更新,因为在路径压缩之前,每个节点都是与其父节点链接着,那个Value自然也是与其父节点之间的权值

2.在两个并查集做合并的时候,权值也要做相应的更新,因为两个并查集的根节点不同。

下面就来看在这两个过程中,如何更新权值:

路径压缩:

int find(int x)
{
	if (x != parent[x])
	{
		int t = parent[x];
		parent[x] = find(parent[x]);
		value[x] += value[t];
	}
	return parent[x];
}

可以看到更新权值只多了两行代码,先记录下原本父节点的编号,因为在路径压缩后父节点就变为根节点了,再将当前节点的权值加上原本父节点的权值,此时父节点的权值已经是父节点到根节点的权值了,因此加上这个权值就会得到当前节点到根节点的权值。

 

合并:

		int px = find(x);
		int py = find(y);
		if (px != py)
		{
			parent[px] = py;
			value[px] = -value[x] + value[y] + s;
		}

带权并查集_第4张图片

现在是已知x所在的并查集根节点为px,y所在的并查集根节点为py,如果有了x、y之间的关系,要将px并到py上,如果不考虑权值直接修改parent就行了,但是现在是带权并查集,必须得求出px与py这条边的权值是多少,很显然x到py两条路径的权值之和应该相同,就不难得出上面代码所表达的更新式。但是需要注意并不是每个问题都是这样更新的,有时候可能会做取模之类的操作,这一点在后边的例题中可以体现。

 

以上便是带权并查集所需要理解的操作,下面来看三个具体的例子:

HDU-3038-How Many Answers Are Wrong

这个题题目的废话比较多,这里简述一下大意:有M个数,不知道它们具体的值,但是知道某两个数之间(包括这两个数)的所有数之和,现在给出N个这样的区间和信息,需要判断有多少个这样的区间和与前边已知的区间和存在矛盾。例如给出区间和[1,4]为20,[3,4]为15,再给出[1,2]为30,显然这个[1,2]的值就有问题,它应该为20-15=5。

这个题目需要分析什么时候会产生矛盾,不妨就具体看看上面的这个矛盾的例子

带权并查集_第5张图片

在这里这个图是画不了的,因为使用的都是闭区间,闭区间会加上端点的值,这就提醒我们这个题在处理的时候应该将闭区间的某一端变成开区间,比如将[1,4]变成(0,4],将[3,4]变成(2,4],将[1,2]变成(0,2]

带权并查集_第6张图片

如果已知(2,4],(0,4],那么(0,2]的值就可以求出来,如果说再给出(0,2]的值,而又不与(0,2]求出来的值相等,就产生了矛盾,这是矛盾产生的唯一情况:某个区间的值可以由前边的区间求出来,而又给出了这个区间错误的值。注意,有的地方对这个题的分析存在很大的误解,有的博客中说如果已知[1,10]为100,[1,5]为1000,[1,5]的值不可能大于100所以矛盾,这是不对的,因为本题题目中没有说所有的数都是正数,因此矛盾只有在不相等的时候产生。

带权并查集_第7张图片

那么需要考虑什么时候一个区间的值可以由之前的区间求出来呢?观察上边这张图,红色的这条边是求不出来的,而绿色的是可以求出来的,为什么呢?因为0、2属于同一个并查集,0、3不属于同一个并查集,因此不难得出本题的求解思路:如果给定区间的两个端点属于同一个并查集,判断这个区间的值是否与计算得到的值相等;如果给定区间的两个端点不属于同一个并查集,将这两个并查集合并。并查集边的权值就等于题干中的区间和。

 

#define _CRT_SECURE_NO_WARNINGS

#include 
#include 
using namespace std;

const int maxM = 200005;
int parent[maxM];
int sum[maxM];

int Find(int x)
{
	if (x != parent[x])
	{
		int i = parent[x];
		parent[x] = Find(parent[x]);
		sum[x] += sum[i];
	}
	return parent[x];
}


int main()
{
	int m, n;
	int ans = 0;
	while (scanf("%d%d", &m, &n) != EOF)
	{
		for (int i = 0; i <= m; i++)
		{
			parent[i] = i;
			sum[i] = 0;
		}
		ans = 0;
		while (n--)
		{
			int l, r, value;
			cin >> l >> r >> value;
			l--;
			int fl = Find(l);
			int fr = Find(r);
			if (fl == fr)
			{
				if ((sum[l] - sum[r]) != value)
				{
					ans++;
				}
			}
			else {
				parent[fl] = fr;
				sum[fl] = -sum[l] + sum[r] + value;
			}
		}
		cout << ans << endl;
	}
	return 0;
}

由此可见带权并查集实际上就类似于前缀和,通过已知的两个相对信息求出一个绝对信息。

 

HihoCoder-1515-分数调查

描述

小Hi的学校总共有N名学生,编号1-N。学校刚刚进行了一场全校的古诗文水平测验。  

学校没有公布测验的成绩,所以小Hi只能得到一些小道消息,例如X号同学的分数比Y号同学的分数高S分。  

小Hi想知道利用这些消息,能不能判断出某两位同学之间的分数高低?

输入

第一行包含三个整数N, M和Q。N表示学生总数,M表示小Hi知道消息的总数,Q表示小Hi想询问的数量。  

以下M行每行三个整数,X, Y和S。表示X号同学的分数比Y号同学的分数高S分。  

以下Q行每行两个整数,X和Y。表示小Hi想知道X号同学的分数比Y号同学的分数高几分。  

对于50%的数据,1 <= N, M, Q <= 1000  

对于100%的数据,1 <= N, M, Q<= 100000 1 <= X, Y <= N -1000 <= S <= 1000

数据保证没有矛盾。

输出

对于每个询问,如果不能判断出X比Y高几分输出-1。否则输出X比Y高的分数。

样例输入

10 5 3  
1 2 10  
2 3 10  
4 5 -10  
5 6 -10  
2 5 10  
1 10  
1 5  
3 5

样例输出

-1  
20  
0

这道题给的都是相对关系,显然可以用带权并查集来做,权值就是子节点比父节点多的分数,询问能求解的情况就是两个人同属于一个并查集的情况。这里不多解释了:

#define _CRT_SECURE_NO_WARNINGS
#include 
const int maxN = 100005;
int parent[maxN];
int score[maxN];

int find(int x)
{
	if (x != parent[x])
	{
		int t = parent[x];
		parent[x] = find(parent[x]);
		score[x] += score[t];
	}
	return parent[x];
}
int main()
{
	int n, m, q;
	scanf("%d%d%d", &n, &m, &q);
	for (int i = 1 ; i <= n ; i ++ )
	{
		parent[i] = i;
	}
	while ( m-- )
	{
		int x, y, s;
		scanf("%d%d%d", &x, &y, &s);
		int px = find(x);
		int py = find(y);
		if (px != py)
		{
			parent[px] = py;
			score[px] = -score[x] + score[y] + s;
		}
	}
	while ( q -- )
	{
		int x, y;
		scanf("%d%d",&x,&y);
		if (find(x) != find(y))
		{
			printf("-1\n");
		}
		else {
			printf("%d\n", score[x] - score[y]);
		}
	}
	
	return 0;
}

 

poj-1182-食物链

 

食物链

Time Limit: 1000MS   Memory Limit: 10000K
Total Submissions: 92490   Accepted: 27910

Description

动物王国中有三类动物A,B,C,这三类动物的食物链构成了有趣的环形。A吃B, B吃C,C吃A。 
现有N个动物,以1-N编号。每个动物都是A,B,C中的一种,但是我们并不知道它到底是哪一种。 
有人用两种说法对这N个动物所构成的食物链关系进行描述: 
第一种说法是"1 X Y",表示X和Y是同类。 
第二种说法是"2 X Y",表示X吃Y。 
此人对N个动物,用上述两种说法,一句接一句地说出K句话,这K句话有的是真的,有的是假的。当一句话满足下列三条之一时,这句话就是假话,否则就是真话。 
1) 当前的话与前面的某些真的话冲突,就是假话; 
2) 当前的话中X或Y比N大,就是假话; 
3) 当前的话表示X吃X,就是假话。 
你的任务是根据给定的N(1 <= N <= 50,000)和K句话(0 <= K <= 100,000),输出假话的总数。 

Input

第一行是两个整数N和K,以一个空格分隔。 
以下K行每行是三个正整数 D,X,Y,两数之间用一个空格隔开,其中D表示说法的种类。 
若D=1,则表示X和Y是同类。 
若D=2,则表示X吃Y。

Output

只有一个整数,表示假话的数目。

Sample Input

100 7
1 101 1 
2 1 2
2 2 3 
2 3 3 
1 1 3 
2 3 1 
1 5 5

Sample Output

3

这道题之前我有记录过用一般并查集的做法:开三倍空间的并查集,显然这个题给的又都是相对关系,不能够确定具体的某个动物属于哪个物种,因此才有开三倍空间来处理的那样的思路,但是带权并查集就是用来处理相对关系的,可以考虑用带权并查集来做。

在这个题中相对关系就是食物链上的关系,因此带权并查集中的权值就应该记录两个动物在食物链上的相对关系,A->B为0表示同类,为1表示A吃B,为2表示A被B吃。这个值不同于前面两个题中的区间合、分数差,它是不可以直接累加的,要考虑三个问题:

1.路径压缩时,如何更新Value

如果现在有A->B为1,B->C为1,怎么求A->C?显然A吃B,B吃C,那么由题意C应该吃A,那么A->C应该为2;

如果现在有A->B为2,B->C为2,怎么求A->C?显然B吃A,C吃B,那么由题意A应该吃C,那么A->C应该为1;

如果现在有A->B为0,B->C为1,怎么求A->C?显然A、B同类,B吃C,那么由题意A应该吃C,那么A->C应该为1;

找规律不难发现,A->C = (A->B + B->C) % 3,因此关系值的更新需要累加再模3。

2.区间合并时,如何更新Value

由1不难发现,本题的Value更新无非就是多了个取模操作,因此不难验证区间合并的更新操作应该为:

relationWithParent[fx] = (-relationWithParent[x] + relation + relationWithParent[y]) % 3

3.如何判断是否矛盾

不同于分数、区间和,可以直接相减得到计算结果,这里要解决如果已知A与根节点的关系,B与根节点的关系,如何求A、B之间的关系?由于关系值的计算要模3,因此A->B=(A->C - B->C + 3) % 3,加三是为了避免负数的影响。将A->B与题目给的Relation值判等比较即可。

注意,本题给的relation值都是1或2,在实际处理的时候,这个值应该减1。

#define _CRT_SECURE_NO_WARNINGS

#include 
#include 
using namespace std;
const int maxN = 50005;

int parent[maxN];
int relationWithParent[maxN];    // 0 : 同类 1:吃 2:被吃

// A吃B B吃C ==> A被C吃     Relation(a,c) = Relation(a,b)+Relation(b,c)
// A被B吃 B被C吃 ==> A吃C Relation(a,c) = (Relation(a,b)+Relation(b,c))%3
int find(int x )
{
	if ( x != parent[x])
	{
		int t = parent[x];
		parent[x] = find(parent[x]);
		relationWithParent[x] = (relationWithParent[x] + relationWithParent[t]) % 3;
	}
	return parent[x];
}

int main()
{
	int n, k;

	cin >> n >> k;
	int ans = 0;
	for (int i = 1 ; i <= n ; i ++ )
	{
		parent[i] = i;
	}
	while (k--)
	{
		int relation, x, y;
		scanf("%d%d%d",&relation,&x,&y);
		if ( x > n || y > n || (relation == 2 && x == y ) )
		{
			ans++;
		}
		else {
			int fx = find(x);
			int fy = find(y);
			if (fx == fy)
			{
				if ((relation - 1) != ((relationWithParent[x] - relationWithParent[y] + 3) % 3))
					ans++;
			}
			else {
				parent[fx] = fy;
				relationWithParent[fx] = (-relationWithParent[x] + relation  - 1 + relationWithParent[y]) % 3;
			}
		}
	}
	cout << ans;
	return 0;
}

你可能感兴趣的:(算法)