并查集大体分为三个:普通的并查集,带种类的并查集,扩展的并查集(主要是必须指定合并时的父子关系,或者统计一些数据,比如此集合内的元素数目。)
一篇不错的科普文http://hi.baidu.com/czyuan%5Facm/blog/item/531c07afdc7d6fc57cd92ab1.html
普通并查集:
几道裸体大水:
poj 1611The Suspects记录并查集中元素数量
poj 2524 Ubiquitous Religions并查集个数
poj 1308 Is It A Tree判断给出的图是否为树
-------------------------------------------------------------------------------------------------------------------------------
poj 1456 Supermarket
题意:超市有n种产品(一天卖完),每种产品有各自的利润和保质期每天只能出售一种商品,问最大利润是多少;
解法:明显的贪心策略,商品按利润大小排序,如果保质期前还有空闲的天,那么就可以出售,并且占用尽可能靠后的那天。由于保质期和商品都是10000的数据量,每个商品都延保质期向前查询会T。因此需要数据结构优化。把已占用的连续区间作为一个集合,根为左端点-1(可利用的最优点),因此每次查询即是查询deadline的根节点,如果根节点=0说明deadline之前的点均被占用。由于是处理与跟的关系,因此可以用并查集解决:
for(int i=0;i<n;i++){ int f=find(pp[i].deadline); if(f!=0) { ans+=pp[i].value; father[f]=f-1; } }
hud3635 DragonBalls
题意:给出n个龙珠,开始的时候第i个龙珠在第i个城市,然后下面有q个操作:T a b:把第a个龙珠所在城市的所有龙珠移动到第b个龙珠所在的城市;Q a :询问第a个龙珠所在的城市,这个城市有多少颗龙珠,这颗龙珠被移动多少次。
解法:对于合并两个集合的操作,可以看成是第一个集合的根结点的移动次数加1,当一个节点加入到另外一个集合后,它随它父节点的移动而移动,因此在路径压缩后维护time[x]+=time[temp];类似于种类并查集的维护,但是原理不同。
Ural 1671. Anansi's Cobweb
题意:给出一张无向图,然后顺次删除一些边,问每次删边后图中有几个连通分量。
解法:逆向思维,先根据不删除的边构图,然后按照删除顺序的反序一次添加边,可以维护添加后的集合数,即删除对应边后连通分量数。
-------------------------------------------------------------------------------------------
种类并查集
ps:做完种类并查集后的整体感受:只有想不到,没有做不到,很多想法很抽风。
除father[] 数组记录每个节点的父亲外还需要relation[]数组记录每个节点与根节点之间的关系,如果father[a]=fathrt[b],那么a、b间的关系可根据 relation[a]与relation[b]之间某种运算转换,如果不相等则进行合并操作,合并操作只改变两个根节点之间的关系,子树与新父节点之间的关系在路径压缩时更改(类似于线段树的更改是pushdown中传递,不查询不需更改)
以poj 1182 食物链 为例:(转自czyuan)
题目告诉有3种动物,互相吃与被吃,现在告诉你m句话,其中有真有假,叫你判断假的个数(如果前面没有与当前话冲突的,即认为其为真话)
首先,集合里的每个点我们都记录它与它这个集合(或者称为子树)的根结点的相对关系relation。0表示它与根结点为同类,1表示它吃根结点,2表示它被根结点吃。
那么判断两个点a, b的关系,我们令p = Find(a), q = Find(b),即p, q分别为a, b子树的根结点。
1. 如果p != q,说明a, b暂时没有关系,那么关于他们的判断都是正确的,然后合并这两个子树。这里是关键,如何合并两个子树使得合并后的新树能保证正确呢?这里我们规定只能p合并到q(刚才说过了,启发式合并的优化效果并不那么明显,如果我们用启发式合并,就要推出两个式子,而这个推式子是件比较累的活...所以一般我们都规定一个子树合到另一个子树)。那么合并后,p的relation肯定要改变,那么改成多少呢?这里的方法就是找规律,列出部分可能的情况,就差不多能推出式子了。这里式子为 : tree[p].relation = (tree[b].relation - tree[a].relation + 2 + d) % 3; 这里的d为判断语句中a, b的关系。还有个问题,我们是否需要遍历整个a子树并更新每个结点的状态呢?答案是不需要的,因为我们可以在Find()函数稍微修改,即结点x继承它的父亲(注意是前父亲,因为路径压缩后父亲就会改变),即它会继承到p结点的改变,所以我们不需要每个都遍历过去更新。
2. 如果p = q,说明a, b之前已经有关系了。那么我们就判断语句是否是对的,同样找规律推出式子。即if ( (tree[b].relation + d + 2) % 3 != tree[a].relation ), 那么这句话就是错误的。
3. 再对Find()函数进行些修改,即在路径压缩前纪录前父亲是谁,然后路径压缩后,更新该点的状态(通过继承前父亲的状态,这时候前父亲的状态是已经更新的)。
我的种类并查集模板:
int p[],k[], kind; int find(int x) { if (x == p[x]) return x; int temp = p[x]; p[x] = find(p[x]); k[x] = (k[x] + k[temp]) % kind; //k[x]=k[x]^k[temp]; return p[x]; } boolean merge(int a,int b,int d){ int pa=find(a); int pb=find(b); if(pa==pb) return true; p[pb]=pa; k[pb]=((k[a]-k[b]-d)%kind+kind)%kind; //k[pb] = d^(k[a]^k[b]); d==1-->a,b不属于同一集合 return false; } void init(int n){ p=new int[n+1]; k=new int[n+1]; for(int i=1;i<=n;i++) p[i]=i; } // 出现矛盾 // if(merge(a,b,d)) // if((k[a]-k[b]-d)%kind!=0) // //if(k[a]^k[b]!=d)
poj 1703 Find them, Catch them
题意:某地有两个黑帮,有n个社会青年,给出m个关系,表示某两个社会青年不属于同一黑帮,在线查询某两个人是否在同一黑帮中。
解法:种类数为2的种类并查集,题中提及的“每个黑帮至少有一个人”这个条件数据中貌似没有,但是需要考虑一下怎么实现。对于种类为2的并查集可以使用位运算加速:
路径压缩时,relation[x] = relation[x] ^ relation[temp];
合并时father[pb] = pa; relation[pb] =d ^(relation[a] ^ relation[b]);
poj 2492 A Bug's Life
直接贴上题的代码,会。。。。PE
poj 1988 Cube Stacking
题意: 有几个stack,初始里面有一个cube。支持两种操作:
路径压缩时:dis[x]+=dis[temp];(这句话要放在路径压缩之前,放在递归后面会WA,大意了!)
查询时:ans=num[find(x)] - dis[x] - 1;
POJ 1733 Parity game
题意:有长为m的01序列,给出n句话,告诉[a,b]区间内有奇数个1还是偶数个1,问前多少句话时不矛盾的。
解法:对于当前给出的区间[a,b]当且仅当区间内所有子区间都被叙述过时才可判断是否为真,因此首先想到了线段树来处理区间问题,但是只告诉了区间内1的个数是奇数还是偶数因此不满足向下的传递性,因此线段树不好处理。
用并查集可以巧妙的处理:对于每次输入的(a,b)如果a和b+1在同一集合内,那么判断relation[a]^relation[b+1]是否与给出的条件相等,如果相等就合并a 和b+1,否则说明这句话为假,直接break;
注意:原来区间太大需要离散化,因为区间内1的奇偶性有长度无关;
不能直接处理a和b,这点是一开始没想到用并查集的原因也是此题的巧妙之处(是不是个模型?待总结)。
POJ 1417 True Liars
题意:有两个部落,一个部落的人只说真话另一个部落的人只说假话,已知两个部落各自的人数p1、p2,现有n条问询,a回答b是否属于说真话那个部落的。
解法:如果回答是yes则ab两人属于同一部落否则属于不同部落,对于每一个集合统计与根节点在同一部落的人数及不在同一部落的人数,问题转化为有n个集合,每个集合都有a、b两个数字,先要从每个集合中挑出一个数字,使得总和等于p1的方案数,如果方案数等于1则可以唯一确定。
于是可以用类似背包问题的dp解决dp[i][j]=dp[i-1][j-a]+dp[i-1][j-b];dp[i][j]代表点i个集合选出j的方案数。由于我们只关心dp[i][j]等于0、1、2的情况,因此如果dp[i][j]>2则dp[i][j]=2;如果dp[i][j]=1则记录是从哪转移来的,然后从后向前沿记录的路径判断第i个集合的根节点属于哪个部落,这样就得出了所有点属于哪个部落。
注意:1.题意没有给出数据范围,要特判n,p1,p2等于0的情况;n等于0时如果p1、p2都不等于零则无 解。p1或p2等于零时如果所有节点与根节点都属于同一部落时有解;
2.并查集处理后一定要进行一个类似于缩点的操作,不可直接对原有节点进行处理。
poj 2912 Rochambeau
题意:n个孩子分出三个组玩“剪子包袱锤”,每组的成员只能出一种动作,其中有一个裁判不属于任意一种并且可以出任何动作。给出m个胜负关系,问是否能确定谁是裁判。
解法:跟“食物链”很像,只不过多了一个特殊元素,由于元素个数只有500,因此可以枚举每个小孩是裁判,因为只有裁判才能使得关系出现矛盾,因此对于每个小孩,如果除去与他相关的胜负关系外仍有矛盾关系则说明他不是裁判,并记录出现错误是在第多少句话之后err[i]。如果每个小孩都不是裁判(去除后都有矛盾)则给出的胜负关系是不可能的;如果多个小孩是裁判,则裁判是这多个中的一个,因此不能确定。若果只有一个小孩是裁判,则在Max(err)句之后得到结论。(很绕口,需要多理几遍。。。而且对这种假设一直心存怀疑)
移点并查集
除加边操作外有时还需将并查集中的某点删除(变为孤立),为不影响其他节点的状态,通常采用建立一个新节点的方式实现孤立,同时元节点变为虚节点,用hash[i]代表i节点的实际位置,此后对每个节点的hash值进行合并操作。
HDU2473
题意:给你一些相连的两点(带传递,显然是集合),并会对某些点进行删除操作,最终问你集合的数目
int p[]=new int[1100010],num[]=new int[1100010],n,m; int hash[]=new int[1100010],cnt; int find(int x){ if(p[x]!=x) p[x]=find(p[x]); return p[x]; } void merge(int a,int b){ int fa=find(a); int fb=find(b); if(fa!=fb){ p[fa]=fb; num[fb]+=num[fa]; } } void init() throws IOException{ cnt=n=next(); m=next(); for(int i=0;i<n+m;i++){ hash[i]=p[i]=i; num[i]=1; } } void run() throws IOException{ int cas=0; while(true){ init(); cas++; if(m+n==0) break; while(m-->0){ in.nextToken(); String s=in.sval; if(s.charAt(0)=='M') { int a=next(),b=next(); merge(hash[a],hash[b]); } else { int x=next(); num[find(hash[x])]--; hash[x]=cnt++; } } int ans=0; for(int i=0;i<cnt;i++) if(find(i)==i&&num[i]>0) ans++; System.out.println("Case #"+cas+": "+ans); } }