并查集小结
---------------------------------------------------------------------------------------------------by Milkor -----------------------------2015.7.12
int delta[maxn];
int f[maxn];
void init()
{
int i;
for(i=0;i<maxn;i++)
{
f[i] = 1;
//init delta
}
}
int find(int a)
{
if(f[a]==a){return a;}
int ta = find(f[a]);
//ta 为 root. f[a] 为直接父亲节点
//update the delta[a]
return f[a] = ta;
//路径压缩。
}
void bing(int a,int b)
{
int ta = find(a);
int tb = find(b);
if(ta!=tb) //不是同一个集合中
{
f[ta] = tb;
//update the dalta[ta]
}
}
以上是并查集的常见写法。(没有维护deep数组,把深度低root 连接到 深度高的root上。)
考虑几个并查集常见的操作以及问题。
操作1:统计当前产生的集合数目。
//假设已经出现了1~N的关系
for(i=1;i<=N;i++)
{
if(f[i] = i)
{
++cnt;
}
}
因为初始化f[i] = i.而root会保留 f[i] = i.统计集合个数其实就是在统计root个数。
操作2:判断是否成环。
//在加入a和b的时候,判断a和b 是否是同根的。如果是同根就是成环了。
bool bing(int a,int b)
{
int ta = find(a);
int tb = find(b);
if(ta!=tb)
{
f[ta] = tb;
return true;
}
else
{
return false;
}
}
//false 是成环了。true是没有成环。并且合并。
操作3:统计集合数目。
//维护一个cou数组。这个维护只要维护在根节点处。不用像种类并查集。要获得各个节点对之间的关系。
init : cou[i] = 1;
bing : cou[tb] += cou[ta];
操作4:实现并查集的删除节点的操作。
//维护一个id数组。对于要删除的节点。就把该节点的id指向没有使用的节点处。
/*
原理稍微解释一下:
假如我们访问一个节点是通过一个id数组去访问的。
一开始:init:id[i] = i.是和我们通常的是一样的。
输入a,b. 那合并操作就是bing(id[a],id[b]).
对于删除操作 a.那就是令id[a] = cnt++.
//其中cnt一开始赋值为n.n表示节点数值范围。这样cnt指向的就是一个没有使用的节点。
试想一下。如果此时再次输入a.是否就能访问到一个新的节点。并且曾经是a的孩子节点也全部都不受影响。并且也不再是a的孩子了。因为a已经是一个新的节点了。
*/
操作5:统计一个节点的合并次数。
提出这个是为了提出并查集维护节点信息的一个核心思想:依托根节点。
这个也是为了能比较好理解食物链之类的种类并查集做的铺垫。
考虑一下这样的问题。
一个并查集中有 ta - ab - ac - ad 这样四个元素。其中ta是根节点。
另外一个并查集中有 tb - bb - bc -bd 这样四个元素。其中tb是根节点。
现在要让f[ta] = tb. 让ta作为tb的孩子节点连接上去。
那么必定ta 集合中的元素都要增加 1 次移动次数。
那么如何做到集合中每个元素都增加 1 次移动次数呢?我们自然而然会想到整个集合的关系枢纽ta.
如果我们让move[ta] += 1.
并且当我们find的时候。再把根节点的信息更新到我们需要的孩子上。就能获得孩子节点的完整信息了。这有点像线段树里的lazy标志。
int find(int a)
{
if(f[a]==a){return a;}
ta = find(f[a]);
move[a] += move[f[a]];
// 是把信息一层一层传下去的。所以一定是 += move[f[a]].获得完整信息之后。再路径压缩是符合结果的。
// 如果你考虑一个问题就是如果我一直做find(a)操作。那么move[a]是否会发生变化呢?答案是不会的。这个问题就留给你自己考虑吧。
return f[a] = ta;
}
void bing(int a,int b)
{
int ta = find(a);
int tb = find(b);
if(ta!=tb){f[ta] = tb;move[fa]=1;}
//因为作为根节点只可能移动一次。所以只用更新=1.
}
操作6:关于delta[]数组的更新和维护。即所谓带权值的并查集
delta[i] 表示 i和ti的关系。// ti = find(i).
这里体现了并查集维护节点信息的一个核心思想:依托根节点。
因为题目要求的是我们能获得任意对节点的关系。而我们只要获得节点对各自的根节点的关系。
就能经过一系列转换来获得两者的关系。
重点:
维护delta[i]:
提一下种类并查集中的类向量运算:
一个问题:A和B是不同的。B和C是不同的。那么A和C是同类的。
这个问题是:Find them, Catch them 中的关系联系。
我们令X和Y是不同的为1。令X和Y是相同的为0。
我们看看对于这个问题这样的设法是否满足所谓的类向量运算
A->B 为不同为1. (在本问题中A->B和B->A是一样的(因为A和B不相同,与B和A是不相同是一个意思。同样A和B相同也是如此)。在这点上其实是不符合向量运算。对于这个问题我们下面再谈)
B->C 为不同为1.
那么A->C?.
A->C = A->B + B->C = 1 + 1 = 2 对于这个我们对2取模。这是为了保证让我们的关系是有意义的同时。
其实也是为了让运算满足上述关系。
A->C = 0 是同类。所以满足我们的运算定义。
以上要获得A->C 的关系。可以类似向量中的绕一圈加法即A->C = A->A1 + A1->A2 + A2->A3 + A3... + An->C 称之为满足类向量加法。
如果有了这个。那么我们就可以很方便的做一个节点的delta的更新。
int find(int a)
{
if(f[a]==a){return a;}
int ta = find(a);
delta[a] = (delta[a] + delta[f[a]]) % 2;
return f[a] = ta;
}
对于这句delta[a] = (delta[a] + delta[f[a]]) % 2;
而这里的f[a] 其实是原来的根节点ta'.
不然的话 delta[a] != (delta[a] + delta[f[a]]) % 2 。
画个图模拟一下并查集从无到有的创建过程就可以知道这点。
一开始
1->2
delta[1] 存储的是1->2 的关系信息。
1->2->3
2连接到3.
更新delta[2]的信息存储的是2->3的关系信息。而此时delta[1]始终还是1-2的。
所以delta[1] 更新为( delta[1] + delta[2] ) % 2.
当然你也可以多连接几次之后。然后再find 1 会发现1的信息更新的还是正确的。
原理同就是。
a->ta = a->ta' + ta'->ta
bing函数。更新根节点(信息维护:依赖根节点)
void bing(int a,int b)
{
int ta = find(a);
int tb = find(b);
if(ta != tb)
{
f[ta] = tb;
delta[ta] = ( delta[a] + delta[b] + (a-b的关系) ) % 2;
}
}
delta[ta] = ( delta[a] + delta[b] + (a-b的关系) ) % 2;
理解为: ta->a + a->b + b->tb.
在本题中ta->a 实际意义上其实就是a->ta.
给出两个节点的关系判断:
此时如果两个节点在一个并查集里面并不是代表着这两个节点是同类关系。而是说这两个节点能够判断出关系。
要获得a->b的关系。
首先判断是否是同根。如果不是,说这两个节点还不能得出关系。
如果是:
我们可以通过a->b = a->t + t->b = a->t + b->t = (delta[a] + delta[b]) % 2 如果是0 那么就是相同。如果是1那么就是不同了。
考虑完这个问题。再考虑一个经典的食物链的问题。
问题核心是这样的:A吃B B吃C 那么C就吃A
我们该如何设值能够比较好地让我们进行关系的更新呢?如果设出来的值能过满足类向量加法运算是最最好不过了。
其实这是有的。
让 X吃Y 即 X->Y 为 1.
X和Y同类 就是 0.
Y吃X 即 X->Y 为 2.
这个时候你可以尝试验证一样是否满足类向量加法运算。答案当然是满足的。
并且比上题目有意思的是。这个还满足向量的自反性。
X->Y = 1.
Y->X = -1.为了让其在0~2范围内。
Y->X = (-1+3)%3 = 2.满足我们上述的关系。(这里的操作其实是数论上的同余定理的小使用)
程序怎么写就不用说了吧。因为这样设值满足了类向量加法了已经。就是如果取反。要减去那个值。
其实如果能理解到这。上面留下来的问题也就已经解决掉了。我们并不需要整套的满足向量运算。甚至我们其实不用满足所谓的向量加法。
我们只是为了我们的关系能够比较好地发生转移。
比如食物链那个题目。如果改为A->B,B->C 那么可以获得A->C。你要怎么做处理?
其实很简单。只要设X->Y为1.
A->C = A->B|B->C
虽然还是满足形式上的类向量加法。
但是你必须知道的是我们只是为了我们的关系更好地转移而这样做的。而且可以适应多元的运算。
否则的话你用逻辑穷举所有可能性。也是可以的。只不过这里把逻辑运算做成了类似的加法运算。
比如随意地、
我要给A->C赋关系的时候。我可以特判。A->B 的关系如何如何。B->C的关系如何如何。那么A->C的关系就是如何如何。
当然这样会很麻烦。但是也不是不可以。所以本质上就是为了更好的关系转移而已。然后加上维护并查集信息的核心思想。
其实我很想能够有一个方法比较好地推导出我们该设的值。而不是无意义地尝试(尽管你会发现我们要设的值总是0开始然后几个自然数)。