我本以为并查集是一个很简单的数据结构,刚开始看的时候就看明白了,后来我遇到了带权并查集…等我慢慢磨懂带权并查集之后,我以为并查集也就这样的难度了…后来我又遇到了离线操作…并查集优化搜索…反向建立并查集…当我把上面这些全写会后我又遇到了并查集+dp找方案数…我还是太年轻了…
-----------------------------------------------------------------
-----------------------------------------------------------------
本来不打算写这个专题.因为自己前一段时间就练过且寒假集训也有一套题…后来想想还是扎实一下基础再复习一下带权并查集就开了这个专题。。然后就被疯狂打脸,这个专题学到了很多并查集的好东西(sao cao zuo)
---------------------------------------------------------------------
专题地址
A - Wireless Network POJ - 2236
题意:给出通讯站的个数n和最短距离dis(只有距离不超过dis且均未被破坏的通讯站才可以互相通讯,具有传递性.ab,bc,等同于ac)
之后n行给出每个通讯站的坐标
然后就是多组输入处理到文件结尾,每行给出相应的操作
起初所有通讯站都是坏的
为O pos, 代表修好pos位置的通讯站
为S a,b代表询问a和b是否可以联系
简单题,初始化并查集每个根节点为自己,然后开个数组储存修好的通讯站,每当有一个通讯站被修好后,就放进数组内,且与修好的通讯站中相距距离小于dis都并在一起,查询时看ab根节点是否相同即可
const int maxn=1e3+5;
const int maxm=1e5+5;
int dad[maxn];
int seek(int x)
{
if(x==dad[x])
return x;
return dad[x]=seek(dad[x]);
}
struct node
{
double x,y;
}arr[maxn];
int good[maxn];
void mix(int a,int b)
{
int fa=seek(a),fb=seek(b);
dad[fa]=fb;
}
double get(int a,int b)
{
return sqrt(pow(arr[a].x-arr[b].x,2)+pow(arr[a].y-arr[b].y,2));
}
void solve()
{
int n=read();
double d;scanf("%lf",&d);
rep(i,1,n)
{
dad[i]=i;
cin>>arr[i].x>>arr[i].y;
}
ms(good,0);
char ope;
int num=0;
while(cin>>ope)
{
if(ope=='O')
{
int pos=read();
good[++num]=pos;
rep(i,1,num-1)
{
double dis=get(pos,good[i]);
if(dis<=d)
{
mix(good[i],pos);
}
}
}
else
{
int a=read(),b=read();
string ans="FAIL";
if(seek(a)==seek(b))
{
ans="SUCCESS";
}
cout<<ans<<endl;
}
}
}
B - The Suspects POJ - 1611
多组输入n,m代表学生数目和小组数目0,0结尾
然后是m行 每行先给一个小组成员数目num,再输入num个数为成员编号
0号是传染源,和0号一个组的都可能会感染,和可能感染的人一个组也可能被感染,求可能被感染的人的数目(包括0)
很简单的并查集,把每个组都合在一起,然后遍历学生,如果这个学生和0号学生的根节点一样,就计数,最后输出数目即可
const int maxn=3e4+5;
const int maxm=1e5+5;
int dad[maxn];
int cup[maxn];
int seek(int x)
{
if(x==dad[x])
return x;
return dad[x]=seek(dad[x]);
}
void mix(int a,int b)
{
int fa=seek(a),fb=seek(b);
dad[fa]=fb;
}
void solve()
{
int n,m;
while(~scanf("%d %d",&n,&m))
{
if(n==0&&m==0)
break;
rep(i,0,n-1)
{
dad[i]=i;
}
while(m--)
{
int num=read();
rep(i,1,num)
{
cup[i]=read();
if(i==1)continue;
mix(cup[1],cup[i]);
}
}
int ans=0;
int pos=seek(0);
rep(i,0,n-1)
{
ans+= seek(i)==pos ;
}
printf("%d\n",ans);
}
}
C - How Many Tables HDU - 1213
出门聚餐,人们只和自己朋友圈的人坐在一起,只要有相同的朋友就算是同一个朋友圈的,求问共有多少个群体
这道题我认为是最简单最基础的并查集模板题了,直接统计根节点的数目
const int maxn=3e4+5;
const int maxm=1e5+5;
int dad[maxn];
int cup[maxn];
int seek(int x)
{
if(x==dad[x])
return x;
return dad[x]=seek(dad[x]);
}
void mix(int a,int b)
{
int fa=seek(a),fb=seek(b);
dad[fa]=fb;
}
void solve()
{
int n=read(),m=read();
rep(i,1,n)
{
dad[i]=i;
}
while(m--)
{
int a=read(),b=read();
mix(a,b);
}
int ans=0;
rep(i,1,n)
{
ans+=seek(i)==i;
}
printf("%d\n",ans);
}
D - How Many Answers Are Wrong HDU - 3038
给出数的范围n和询问次数m
每次给出a,b,c代表从a到b的权值是c
求问有多少个与前面冲突的,没冲突的就算是正确的
这个是我写的第一道带权并查集,当初才开始写的时候边看题解马慢慢写的迷迷糊糊就过去了,现在又回来加强了一遍理解
我们可以用一个v数组代表每个数到自己根节点的距离,对于每一个给出的a,b,c我们使a–,这样得到两个节点a,b查询是否是同一个集合内的,是的话就看是否等于c,不等于就计数,不在同一个集合就合并并记录
const int maxn=2e5+5;
const int maxm=1e5+5;
int dad[maxn];
int val[maxn];
int seek(int x)
{
if(x!=dad[x])
{
int tmp=dad[x];
dad[x]=seek(dad[x]);
val[x]+=val[tmp];
}
return dad[x];
}
void solve()
{
int n,m;
while(cin>>n>>m){
rep(i,1,n)
{
dad[i]=i;
}
ms(val,0);
int ans=0;
while(m--)
{
int st=read(),ed=read(),tmp=read();
int fst=seek(st-1);
int fed=seek(ed);
if(fst==fed)
{
if(val[ed]-val[st-1]!=tmp)
ans++;
}
else
{
dad[fed]=fst;
val[fed]=val[st-1]+tmp-val[ed];
}
}
printf("%d\n",ans);
}
}
E - 食物链 POJ - 1182
emmmmm当初为了理解带权并查集,也去写这个题然后死活过不去,,现在一遍就过了。。果然进步了不少bksw
这道带权并查集和上面那个差不多,用val数组代表每个节点与根节点之间的关系,0代表同类,1代表可以吃根,2代表被根吃,注意相减的时候要+3再%3,不然可能会出现负数,
我觉得带权并查集和普通并查集的区别就是,原来的并查集,题目要合并就合并了,但是带权的就不一样了,内部之间需要有特定的关系,我们在合并的时候必须要维护内部的稳定
而带权并查集的难点我认为在于权值的设定。。。这个东西要多写题才能熟练
在这里插入代码片
wdnmd…vj突然崩溃了…代码和题面都在里面…
被迫停更…
回来了,继续更
const int maxn=5e4+5;
const int maxm=1e5+5;
int dad[maxn];
int val[maxn];
int seek(int x)
{
if(x!=dad[x])
{
int tmp=dad[x];
dad[x]=seek(dad[x]);
val[x]=(val[x]+val[tmp])%3;
}
return dad[x];
}
void solve()
{
int n=read(),m=read();
rep(i,1,n)
{
dad[i]=i;
val[i]=0;
}
ms(val,0);
int ans=0;
while(m--)
{
int d=read(),x=read(),y=read();
if(x>n||y>n||(x==y&&d==2))
{
ans++;
continue;
}
int fx=seek(x),fy=seek(y);
if(fx!=fy)
{
int tmp= d==1 ? 0:1;
dad[fx]=fy;
val[fx]=(tmp+val[y]-val[x]+3)%3;
}
else
{
if(d==1)
{
ans+= val[x]!=val[y];
}
else
{
ans+=((val[y]+1)%3!=val[x]);
}
}
}
printf("%d\n",ans);
}
F - True Liars POJ - 1417
这个是这套题最难的一个了…我是看着题解慢慢写的…理解是理解了…但自己完全脱离出来动手还是有难度的…
首先是并查集分组,然后对于不同的组我们想象成一个01背包,求出组成p1个人的方案数,方案数大于1就不唯一输出no。反之就从小到大输出神族的人…我不明白为什么我写不等于1就是wa 改成大于1就ac…难道方案不存在也算???
讲不了,讲不了…直接放代码,以后有能力再补(主要是输出方案这一块太搞人心态了)
const int maxn=1e3+5;
const int maxm=3e2+5;
int n,p1,p2;
int dp[maxn][maxn];//前i个人组成j的方案数
int v[maxn],dad[maxn],cup[maxn][2],chose[maxn][maxn],num=0;
int ans[maxn][2];
int seek(int x){
if(dad[x]==x)return x;
int t=dad[x];
dad[x]=seek(dad[x]);
v[x]=(v[x]+v[t]+2)%2;
return dad[x];
}
void init(){
rep(i,0,p1+p2){
dad[i]=i;
v[i]=0;
}
ms(ans,0);
ms(cup,0);
ms(chose,0);
ms(dp,0);
num=0;
}
void mix(int a,int b,int d){
int fa=seek(a),fb=seek(b);
if(fa==fb)return ;
dad[fa]=fb;
v[fa]=(d+v[b]-v[a]+2)%2;
}
void solve(){
while(~scanf("%d %d %d",&n,&p1,&p2)){
if(n==0&&p1==0&&p2==0)break;
init();
rep(i,1,n){
int a,b;
char say[10];
scanf("%d %d %s",&a,&b,say);
//getchar();
//de(a),de(b),de(say);
int f= say[0]=='y' ? 0:1;
mix(a,b,f);
}
map<int,int>mp;
n=p1+p2;
rep(i,1,n){
int fa=seek(i);
// de(i),de(fa),de(0);
if(fa==i){
mp[i]=++num;
}
}
rep(i,1,n){
int fa=seek(i);
cup[mp[fa]][v[i]]++;
}
ms(dp,0);
dp[0][0]=1;
rep(i,1,num)
{
for(int j=p1;j>=1;j--)
{
if(j>=cup[i][0]&&dp[i-1][j-cup[i][0]]){
dp[i][j]+=dp[i-1][j-cup[i][0]];
chose[i][j]=cup[i][0];
}
if(j>=cup[i][1]&&dp[i-1][j-cup[i][1]]){
dp[i][j]+=dp[i-1][j-cup[i][1]];
chose[i][j]=cup[i][1];
}
}
}
//de(dp[num][p1]);
//de(num);
if(dp[num][p1]>1){
puts("no");
}
else{
int res=p1;
for(int i=num;i>=1;i--){
int pp=chose[i][res];
if(pp==cup[i][0]){
ans[i][0]=1;
}
else{
ans[i][1]=1;
}
res-=pp;
}
rep(i,1,n){
int fa=seek(i);
if(ans[mp[fa]][v[i]]){
printf("%d\n",i);
}
}
puts("end");
}
}
}
G - Supermarket POJ - 1456
这个就是我在开头提到的并查集查找优化贪心
题意: 给出物品数目n 以及每个物品的利润和销售截至日期,比如为day,那么你最晚最晚要在day-1天的时候开始卖,且一天只能卖一种物品,求出最大利润之和
我们可以很轻易的想到这是一个贪心,优先考虑就、利润大的物品,并把它们尽可能晚的卖出去,来不占用空间,我们记录下哪些天已经被安排上了,每一次找一个当前天数前最近的一个天数来卖出去,但是依靠遍历来查找天数太浪费时间了,我们可以用并查集来优化这一个过程
如果当前天数的根节点使它自己,就代表这一天没有被用过,然后就把它的根节点改为它的前一天,指向,这样你下一次查找到这一天时就会自动跳转到这一天的前一天,以此类推,加上路径压缩来优化并查集就OK了。我记得在cf做过类似的关于移动书的问题也是这个思路
先sort优先安排价值大的先处理,然后尽可能往后靠,再并查集,根节点为0代表不可卖出了
const int maxn=1e4+5;
const int maxm=1e5+5;
struct node
{
int st,val;
}arr[maxn];
int dad[maxn];
int seek(int x)
{
if(dad[x]==-1)
return x;
else
return dad[x]=seek(dad[x]);
}
bool cmp(node a,node b)
{
if(a.val==b.val)
return a.st>b.st;
else
return a.val>b.val;
}
void solve()
{
int n;
while(~scanf("%d",&n))
{
int ans=0;
ms(dad,-1);
ms(arr,0);
rep(i,1,n)
{
arr[i].val=read();
arr[i].st=read();
}
sort(arr+1,arr+n+1,cmp);
rep(i,1,n)
{
int t=seek(arr[i].st);
if(t>0)
{
ans+=arr[i].val;
dad[t]=t-1;
}
}
printf("%d\n",ans);
}
}
H - Parity game POJ - 1733
给出范围n,询问次数m,然后m行询问代表这一个区域内1的个数是偶数还是奇数,
这题类似于上面那个区间之和的题目。。这个甚至更简单一点,一个性质的
权val数组代表这个点到根节点1的个数的奇偶情况
因为数太大,可以用map改一下映射的值来开数组写
const int maxn=5e3+5;
const int maxm=1e5+5;
int dad[maxn<<1];
int dis[maxn<<1];
map<int,int>mp;
int seek(int x)
{
if(dad[x]==-1)
return x;
else
{
int tmp=dad[x];
dad[x]=seek(dad[x]);
dis[x]=(dis[x]+dis[tmp])%2;
}
return dad[x];
}
int num;
map<int,int>::iterator it;
int insert(int x)
{
it=mp.find(x);
if(it==mp.end())
{
return mp[x]=++num;
}
return mp[x];
}
void solve()
{
int n=read();
ms(dad,-1);
ms(dis,0);
num=0;
int m=read(),k=m;
rep(i,1,m)
{
int a,b,f;string ans;
cin>>a>>b>>ans;
f= ans=="even" ? 0:1;
if(k!=m)continue;
a=insert(a-1),b=insert(b);
//int fa=insert(a),fb=insert(b);
//de(fa),de(fb),de(10000000000);
int fa=seek(a),fb=seek(b);
if(fa!=fb)
{
dad[fa]=fb;
dis[fa]=(dis[b]+f-dis[a]+2)%2;
}
else
{
if(dis[a]!=((f+dis[b])%2))
{
k=i-1;
}
}
}
printf("%d\n",k);
}
这也是一个维护距离的带权并查集,区别是这个要维护x轴和y轴两个方向的权值…查询的时候,如果两点根节点一致,则可以确定关系,输出曼哈顿距离,反之输出-1
const int maxn=1e4+5;
const int maxm=1e5+5;
struct node
{
int rt,dx,dy;
}dad[maxm];
struct nu
{
int st,ed,dis;
char dir;
}build[maxm];
struct qu{
int st,ed,time;
}query[maxm];
int n,m,k;
int seek(int x)
{
if(dad[x].rt==x)
return x;
int t=dad[x].rt;
dad[x].rt=seek(dad[x].rt);
dad[x].dx+=dad[t].dx;
dad[x].dy+=dad[t].dy;
return dad[x].rt;
}
void mix(nu tmp)
{
int a=tmp.st,b=tmp.ed;
int fa=seek(a),fb=seek(b);
//if(fa==fb)return ;
dad[fb].rt=fa;
int dx=0,dy=0;
switch(tmp.dir)
{
case 'E': dx+=tmp.dis;break;
case 'W': dx-=tmp.dis;break;
case 'N': dy+=tmp.dis;break;
case 'S': dy-=tmp.dis;break;
}
dad[fb].dx=dad[a].dx-dad[b].dx+dx;
dad[fb].dy=dad[a].dy-dad[b].dy+dy;
}
void input()
{
cin>>n>>m;
rep(i,1,n){dad[i].rt=i,dad[i].dx=0,dad[i].dy=0;}
rep(i,1,m)
{
cin>>build[i].st>>build[i].ed>>build[i].dis>>build[i].dir;
}
cin>>k;
rep(i,1,k)
{
cin>>query[i].st>>query[i].ed>>query[i].time;
}
query[0].time=0;
}
void solve()
{
input();
int now=1;
rep(i,1,k)
{
for(int j=now;j<=query[i].time;j++)
{
mix(build[j]);
}
int a=query[i].st,b=query[i].ed;
int fa=seek(a),fb=seek(b);
if(fa!=fb)puts("-1");
else {
printf("%d\n",abs(dad[a].dx-dad[b].dx)+abs(dad[a].dy-dad[b].dy));
}
now=query[i].time;
}
}
其实输入是不用离线的…我写的丑了
虫子只能异性相交,维护一个集合,每次输入俩编号,代表两个虫子相交,即性别相异,求问是否存在同性恋的虫子
如果发现当前加入的虫子冲突了原有的,就代表有同性恋虫子,简单题
const int maxn=2e3+5;
const int maxm=1e5+5;
int dad[maxn],dis[maxn];
int seek(int x)
{
if(dad[x]==-1)
return x;
else
{
int tmp=dad[x];
dad[x]=seek(dad[x]);
dis[x]=(dis[x]+dis[tmp])%2;
return dad[x];
}
}
void solve()
{
int t=read();
rep(cas,1,t)
{
int n=read(),m=read();
ms(dad,-1),ms(dis,0);
string str="No suspicious bugs found!";
while(m--)
{
int a=read(),b=read();
int fa=seek(a),fb=seek(b);
if(fa!=fb)
{
dad[fa]=fb;
dis[fa]=(dis[b]+1-dis[a]+2)%2;
}
else
{
if((1+dis[b])%2!=dis[a])
{
str="Suspicious bugs found!";
}
}
}
if(cas!=1)
{
cout<<endl;
}
cout<<"Scenario #"<<cas<<":"<<endl;
cout<<str<<endl;
}
}
K - Rochambeau POJ - 2912
寒假集训题
大意是除了裁判以外其他的人只能出 剪刀石头布其中一种,且不能更换,求问是否可以推断出谁是裁判,且输出至少多少轮后必定可以推出
维护一个集合,然后我们枚举每个可能为裁判的数,遇到这个数就跳过,如果剩下的回合可以保证没有冲突,就代表这个人可能是裁判并计数,如果有冲突就记录下冲突的轮数取最大的
最后查看裁判的数目是否为1,为1就代表可以推断出,且轮数为最大的冲突轮数,为0就代表不可能,大于1就代表不确定
const int maxm=2e3+5;
const int maxn=5e2+5;
int dad[maxn],val[maxn];
struct node
{
int l,rel,r;
}arr[maxm];
int change(char x)
{
if(x=='=')
{
return 0;
}
else if(x=='<')
{
return 2;
}
else
{
return 1;
}
}
int seek(int x)
{
if(dad[x]==x)
return x;
int tmp=dad[x];
dad[x]=seek(dad[x]);
val[x]=(val[x]+val[tmp]+3)%3;
return dad[x];
}
void solve()
{
int n,m;
while(~scanf("%d %d",&n,&m))
{
rep(i,1,m)
{
char ch;
scanf("%d%c%d",&arr[i].l,&ch,&arr[i].r);
arr[i].rel=change(ch);
}
int pos=0,maxline=0,cnt=0;
rep(i,0,n-1)
{
bool flag=true;
// int tmp=-1;
rep(j,0,n-1)
{
dad[j]=j;
val[j]=0;
}
rep(j,1,m)
{
int l=arr[j].l,r=arr[j].r,rel=arr[j].rel;
if(l==i||r==i)continue;
int fl=seek(l),fr=seek(r);
if(fl!=fr)
{
dad[fl]=fr;
val[fl]=(rel+val[r]+3-val[l])%3;
}
else
{
if(val[l]!=((rel+val[r])%3))
{
flag=false;
//tmp=j;
maxline=max(maxline,j);
break;
}
}
}
if(flag)
{
cnt++;
pos=i;
}
if(cnt>1)break;
}
if(cnt>1)
{
puts("Can not determine");
}
else if(cnt==1)
{
printf("Player %d can be determined to be the judge after %d lines\n",pos,maxline);
}
else
{
puts("Impossible");
}
}
}
打cf了,剩下三个明天补上去
L - Connections in Galaxy War ZOJ - 3261
给出星球数目n,下一行输入每个星球的能量,再输入空间隧道数目m,然后m行输入两个数ab代表a和b联通了,之后给出m个操作,query x代表 x能求救的星球的坐标,不存在就输出-1,多个就输出最小的,为destory x y就代表断开x,y
每个星球只能向和自己相连的且能量大于自己(最大)的星球求救
关于输出最小序号且能量要大于自己的这一个要求,我们在维护并查集的时候,就更新能量大的为根,且相同能量取小结点为根
如果这题要求的是大于等于自己,且不能为自己,我们就要自行修改一下,再储存一个权值num代表和根节点相同大的点的个数,并用val存下 相同大的最小的值
这题的难点是并查集只涉及了合并与查询,对于删除这个操作办不到
然后我们就可以学到一个新知识了,离线反向建立骚操作
我们先用map和pair标记下每一条边的建立,之后在破坏输入的时候再取消被破坏的边,然后我们反向查询,从最后一个询问开始查询,当遇到破坏的时候,就再把边连起来,这样就可以等效于 破坏后面的处于破坏状态,破坏前面的没有被破坏了
const int maxn=1e4+5;
const int maxm=1e5+5;
int ans[maxn*5];
int val[maxn];
int dad[maxn];
pii P1[maxn*2],P2[maxn*5];
string ope[maxn*5];
int seek(int x)
{
if(dad[x]==x)
return x;
return dad[x]=seek(dad[x]);
}
void mix(int a,int b)
{
int fa=seek(a),fb=seek(b);
if(fa==fb)return ;
if(val[fa]>val[fb])
{
dad[fb]=fa;
}
else if(val[fa]<val[fb])
{
dad[fa]=fb;
}
else
{
if(fa<fb)
{
dad[fb]=fa;
}
else
{
dad[fa]=fb;
}
}
}
void solve()
{
int n;
int num=0;
while(~scanf("%d",&n))
{
rep(i,0,n-1)
{
dad[i]=i;
}
map<pii,int>mp;
rep(i,0,n-1)
{
val[i]=read();
}
int m=read();
rep(i,1,m)
{
P1[i].fi=read(),P1[i].se=read();
if(P1[i].fi>P1[i].se)
{
swap(P1[i].fi,P1[i].se);
}
mp[P1[i]]=1;
}
int q=read();
rep(i,1,q)
{
cin>>ope[i];
if(ope[i][0]=='d')
{
P2[i].fi=read(),P2[i].se=read();
if(P2[i].fi>P2[i].se)
{
swap(P2[i].fi,P2[i].se);
}
mp[P2[i]]=0;
}
else
{
P2[i].fi=read(),P2[i].se=-1;
}
}
rep(i,1,m)
{
if(mp[P1[i]])
{
mix(P1[i].fi,P1[i].se);
}
}
per(i,q,1)
{
if(ope[i][0]=='q')
{
int fa=seek(P2[i].fi);
if(val[fa]<=val[P2[i].fi])
{
ans[i]=-1;
}
else
{
ans[i]=fa;
}
}
else
{
int a=P2[i].fi,b=P2[i].se;
mix(a,b);
}
}
//int cas=0;
if(num++)puts("");
rep(i,1,q)
{
if(ope[i][0]=='q')
{
//if(cas++)printf(" ");
printf("%d\n",ans[i]);
}
}
// puts("");
}
}
M - 小希的迷宫 HDU - 1272
这个其实就是要检查是否形成了环
我们在每次合并的时候,看两个节点根是否一样。一样的话就说明出现了环…
同时有个坑点是,不能存在两个根,意思是只能有一个集合,所以我们还要检查根的数目,可以通过标记数组vis或者用rank代表树的高度来代表是否有两个根
const int maxn=1e5+5;
const int maxm=1e5+5;
int dad[maxn],vis[maxn];
int seek(int x)
{
if(dad[x]==x)
{
return x;
}
return dad[x]=seek(dad[x]);
}
void mix(int a,int b)
{
dad[a]=b;
}
void solve()
{
int a,b;
while(~scanf("%d %d",&a,&b))
{
ms(vis,0);
bool flag=true;
if(a==-1&&b==-1)
{
break;
}
rep(i,1,100000)
{
dad[i]=i;
}
while(a!=0&&b!=0)
{
vis[a]=vis[b]=1;
//mx=max(mx,max(a,b));
int fa=seek(a),fb=seek(b);
if(fa==fb)
{
flag=false;
}
else
{
mix(fa,fb);
}
a=read(),b=read();
}
int cnt=0;
for(int i=1;i<=100000;i++)
{
if(vis[i]&&dad[i]==i)
{
cnt++;
}
if(cnt>1)
{
flag=false;
break;
}
}
cout<<(flag ? "Yes":"No")<<endl;
}
}
N - Is It A Tree? POJ - 1308
这个和上面那个小希的迷宫是一模一样的。。
const int maxn=1e5+5;
const int maxm=1e5+5;
int dad[maxn],vis[maxn];
int seek(int x)
{
if(dad[x]==x)
{
return x;
}
return dad[x]=seek(dad[x]);
}
void mix(int a,int b)
{
dad[a]=b;
}
void solve()
{
int a,b;
int cas=0;
while(~scanf("%d %d",&a,&b))
{
ms(vis,0);
bool flag=true;
if(a==-1&&b==-1)
{
break;
}
rep(i,1,100000)
{
dad[i]=i;
}
while(a!=0&&b!=0)
{
vis[a]=vis[b]=1;
//mx=max(mx,max(a,b));
int fa=seek(a),fb=seek(b);
if(fa==fb)
{
//de(a),de(b);
flag=false;
}
else
{
mix(fa,fb);
}
a=read(),b=read();
}
int cnt=0;
for(int i=1;i<=100000;i++)
{
if(vis[i]&&dad[i]==i)
{
cnt++;
}
if(cnt>1)
{
flag=false;
break;
}
}
cout<<"Case "<<++cas<<" "<<(flag ? "is a tree.":"is not a tree.")<<endl;
}
}
并查集入门就告一段落吧