并查集基础练习
普通并查集
普通并查集有两个优化,这里再记一笔复杂度。
路径压缩,均摊 O(logn)
1
| int find(int u) {return fa[u]==u?u:fa[u]=find(fa[u]);}
|
按秩合并,均摊 O(logn) ,“秩” 取最大深度或者结点个数
1 2 3 4 5
| void ut(int u,int v) { int fu=find(u),fv=find(v); if(sz[fu]>sz[fv]) swap(fu,fv); fa[fu]=fv;sz[fv]+=sz[fu]; }
|
两个都加,均摊 O(α(n)) ,α(n) 为反阿克曼函数,增长极慢,可视为常数。
UVA1664 - Conquer a New Region
题意
一个树 N 个点,N−1 条带边权的边,定义两点之间价值为路径上边权的最小值。你雪要找一个中心结点,使它到所有点的价值之和最大化,输出这个最大的价值之和。
1≤N≤2⋅105
题解
将边权从大到小排序,此时最大的边对答案不会有任何影响,考虑合并两结点。
故用并查集维护已经合并的连通块,并记录当前连通块的答案,记为 val 。考虑如何合并 u,v,两种情况,中心定在 u 的连通块或中心定在 v 的连通块。因为现在这条边的边权肯定 ≤ 连通块内的任意边权,故换中心的代价就是 −valu+szu⋅w ,u,v 取大的即可。
CODE
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
| #include <bits/stdc++.h> #define int long long using namespace std; typedef long long ll; const int N = 200010; struct edge{ int u,v,w; bool operator<(const edge a)const { return w>a.w; } }e[N]; int fa[N],n,sz[N]; ll val[N]; int find(int u) {return fa[u]==u?u:fa[u]=find(fa[u]);} signed main() { while(cin>>n) { for(int i=1;i<n;i++) scanf("%d%d%d",&e[i].u,&e[i].v,&e[i].w); for(int i=1;i<=n;i++) fa[i]=i,sz[i]=1,val[i]=0; sort(e+1,e+n); ll ans=0; for(int i=1;i<n;i++) { int fu=find(e[i].u),fv=find(e[i].v); if(fu!=fv) { ll c=max(1ll*sz[fu]*e[i].w+val[fv],1ll*sz[fv]*e[i].w+val[fu]); fa[fu]=fv; val[fv]=c; sz[fv]+=sz[fu]; } } printf("%lld\n",val[find(1)]); } }
|
可删点并查集
SP5150 - Junk-Mail Filter
SP5150
题意
N 个元素,M 次操作,维护一个并查集,支持两种操作:
M x y
: 合并 x 和 y 所在的集合
S x
:把 x 从所在的集合删除,并独立成为一个新的集合
对于每一组测试数据,输出最后还剩多少个集合
1≤N≤105,1≤M≤106
题解
这是一个可删点并差集,合并、寻根与普通并查集无异,重在删点。
如果这个点是叶子结点那么直接 fau=u 即可,但如果不是,那就会很麻烦,要把它的所有孩子连到它父亲那里。
于是我们干脆不实际删点,而是再开一个新点来表示 u,而把原来的点当作一个虚点,这只要在开一个数组 mpu 映射一下 u 当前的实际结点即可。
CODE
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40
| #include <bits/stdc++.h> using namespace std; const int N = 100010; const int M = 2000000; int fa[M],n,m,cs,mp[M],tot,sz[M]; int find(int u) {return fa[u]==u?u:fa[u]=find(fa[u]);} int main() { while(scanf("%d%d",&n,&m)!=EOF) { if(n==0&&m==0) break; tot=n-1; for(int i=0;i<n;i++) fa[i]=i,sz[i]=1,mp[i]=i; int ans=n; while(m--){ char s[4];int u,v; scanf("%s",s); if(s[0]=='S') { scanf("%d",&u); int uu=mp[u]; int fu=find(uu); if(sz[fu]==1) ans--; sz[fu]--; mp[u]=++tot; fa[tot]=tot;sz[tot]=1; ans++; }else { scanf("%d%d",&u,&v); int uu=mp[u],vv=mp[v]; int fu=find(uu),fv=find(vv); if(fu!=fv) { fa[fv]=fu; sz[fu]+=sz[fv]; ans--; } } } printf("Case #%d: %d\n",++cs,ans);
} return 0; }
|
HDU6109 - 数据分割
HDU6109
题意
小w来到百度之星的赛场上,准备开始实现一个程序自动分析系统。
这个程序接受一些形如 xi=xj 或 xi=xj 的相等/不等约束条件作为输入,判定是否可以通过给每个 w 赋适当的值,来满足这些条件。
输入包含多组数据。然而粗心的小w不幸地把每组数据之间的分隔符删掉了。他只知道每组数据都是不可满足的,且若把每组数据的最后一个约束条件去掉,则该组数据是可满足的。请帮助他恢复这些分隔符。
题解
使用种类并查集是错的。不等于符号不具有传递性,这里没说 xi 只有 0,1 两种取值,x1=x2,x2=x3 不能取定 x1,x3 的关系。
考虑 x1=x2 ,x1,x2 所在的集合并能合并,那么维护的并查集上再开一个 vector
存相同的标记,在合并两集合时判断两 vector
有无交集,有交则矛盾。vector
需要启发式合并 (小合大),以保证 O(nlogn) 的复杂度。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76
| #include <bits/stdc++.h> #define _max(x,y) ((x>y)?x:y) #define _min(x,y) ((x<y)?x:y) #define SZ(a) ((int)a.size()) using namespace std; typedef long long ll; template<typename T> inline void red(T &x) { x=0;bool fg=0;char ch=getchar(); while(ch<'0'||ch>'9') {if(ch=='-') fg^=1;ch=getchar();} while(ch>='0'&&ch<='9') x=(x<<1)+(x<<3)+(T)(ch^48),ch=getchar(); if(fg) x=-x; } template<typename T> bool umax(T &x,T y) {return (x<y)?(x=y,true):(false);} template<typename T> bool umin(T &x,T y) {return (x>y)?(x=y,true):(false);} typedef set<int>::iterator si; const int N = 100010; int tot,L,T,a[N],fa[N]; vector<int> val[N]; int st[N<<1],top; void clear() { tot=0; while(top) fa[st[top]]=st[top],val[st[top]].clear(),top--; } int find(int u) { return u==fa[u]?u:fa[u]=find(fa[u]); } bool check(int a,int b) { for(int i=0,j=0;i<SZ(val[a]);i++) { while(j<SZ(val[b])&&val[b][j]<val[a][i]) j++; if(j<SZ(val[b])&&val[a][i]==val[b][j]) return 0; } return 1; } void merge(int a,int b) { if(SZ(val[a])>SZ(val[b])) swap(a,b); fa[a]=b; for(int i=0;i<SZ(val[a]);i++) val[b].push_back(val[a][i]); val[a].clear(); sort(val[b].begin(),val[b].end()); } int main() { scanf("%d",&L); for(int i=1;i<=L;i++) fa[i]=i; for(int i=1;i<=L;i++){ int x,y,e;scanf("%d%d%d",&x,&y,&e); start: st[++top]=x,st[++top]=y; bool fail=0; if(e) { int fx=find(x),fy=find(y); if(fx==fy) goto e; if(check(fx,fy)) { merge(fx,fy); } else fail=1; }else { int fx=find(x),fy=find(y); if(fx==fy) fail=1; else { ++tot; val[fx].push_back(tot); val[fy].push_back(tot); } } e: a[T]++; if(fail) { T++; clear(); } } printf("%d\n",T); for(int i=0;i<T;i++) printf("%d\n",a[i]); return 0; }
|
带权并查集
结点带上权值,表示与祖先的相对关系。
HDU3038 - How Many Answers Are Wrong
HDU3038
题意
有一个长度为 N 整数的序列不知具体何值,有 M 个限定,告诉你 [li,ri] 的和为 wi,请你判断有多少个限定是与前面矛盾的。(按照顺序来,不引起矛盾的即认为正确,矛盾的忽略)。
1≤N≤2⋅105,1≤M≤4⋅104
题解
把序列 a,前缀和一下转化为 s ,每个限定相当于 sli−1+w=sri ,虽然不确定 sli−1 和 sri 的具体值,但是这两数的数量关系十分明确。故可以用并查集将其合并,并带上权值表示其数量关系。
一般的 fa[u]=v,val[u]=vl 就表示 su+vl=sv 。
改写 find(u)
,在路径压缩时加上父亲的权值:
1 2 3 4 5 6 7
| int find(int u) { if(u==fa[u]) return u; int p=fa[u]; fa[u]=find(fa[u]); val[u]+=val[p]; return fa[u]; }
|
对于对限制,如果两点在同一连通块中,需要判断两点的 val
差值是否为 w。
若不在同一连通块中,雪要将其父亲连起来,并改变父亲的权值使 val[l−1]+w=val[r]
CODE
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
| #include <bits/stdc++.h> using namespace std; const int N = 200010; int n,m,fa[N],val[N]; int find(int u) { if(u==fa[u]) return u; int p=fa[u]; fa[u]=find(fa[u]); val[u]+=val[p]; return fa[u]; }
int main() { while(scanf("%d%d",&n,&m)!=EOF) { for(int i=0;i<=n;i++) fa[i]=i,val[i]=0; int ans=0; for(int i=1;i<=m;i++) { int l,r,s;scanf("%d%d%d",&l,&r,&s); l--; int fu=find(l),fv=find(r); if(fu!=fv) { fa[fu]=fv; val[fu]=val[r]-s-val[l]; } else if(val[r]!=val[l]+s) ans++; } printf("%d\n",ans); } return 0; }
|
POJ1733 - Parity game
POJ1733
题意
长度为 N 的零一序列,不知具体值,现告诉你 M 个限制,形如 [L,R] 的和为 奇/偶。让你判断第几个限制之后会出现矛盾。
1≤N≤109,1≤M≤5000
题解
离散化一下,与上一题无异
CODE
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49
| #include <iostream> #include <cstdio> #include <algorithm> using namespace std; const int N = 5010; int fa[N],val[N],b[N<<1],tot,n,QAQ; struct que{ int x,y;bool op; }q[N]; int find(int u) { if(u==fa[u]) return u; int p=fa[u]; fa[u]=find(fa[u]); val[u]+=val[p]; return fa[u]; } int main() { scanf("%d%d",&QAQ,&n); for(int i=1;i<=n;i++) { char s[10]; scanf("%d%d%s",&q[i].x,&q[i].y,s); q[i].x--; b[++tot]=q[i].x,b[++tot]=q[i].y; q[i].op=(s[0]=='o'); } sort(b+1,b+tot+1); tot=unique(b+1,b+tot+1)-b-1; for(int i=1;i<=tot;i++) fa[i]=i; for(int i=1;i<=n;i++) { q[i].x=lower_bound(b+1,b+tot+1,q[i].x)-b; q[i].y=lower_bound(b+1,b+tot+1,q[i].y)-b; } int ans=n; for(int i=1;i<=n;i++) { int fx=find(q[i].x),fy=find(q[i].y); if(fx==fy) { if(((val[q[i].x]-val[q[i].y])&1)!=q[i].op) { ans=i-1; break; } }else { fa[fx]=fy; val[fx]=val[q[i].y]-q[i].op-val[q[i].x]; } } printf("%d\n",ans); return 0; }
|
种类并查集
拆点,一个点分成多点表多种类型,两种类型的话 “敌人的敌人就是朋友”
POJ1417 - True Liars
POJ1417
题意
一个岛上有神与恶魔两个种族,神会说真话,恶魔会说假话。已知神 p1 个,恶魔 p2 个,但不知道具体个人是属于哪个。这个人问 n 次 ,每次的问题为 xi,yi,a ,问 xi :“yi 是否为神?” a 为 yes/no
。注意 xi,yi 可能为同一个人。输入保证不矛盾。
若最终可得出哪些是神则从小到大输出神的编号,并最终输出end
,否则输出no
。多组测试数据,n≤1000,p1,p2≤300
题解
首先,显然:
- x 是神,y 是神 yes
- x 是神,y 不是神 no
- x 不是神,y 是神 no
- x 不是神,y 不是神 yes
所以 yes⟺x,y同身份 ,no⟺x,y不同身份 。
考虑拆点 x,x′ 称之为正点和反点,表示两种身份,种类并查集,对于 yes :merge(x,y),merge(x',y')
,对于 no :merge(x',y),merge(x,y')
。
最终的图是对称的,我们管一边的就行了。
在同一连通块中的正点之间身份应该一样,反点之间身份应该也一样。假设这个连通块中有 a 个正点 b 个反点,也就是说要么 a 个圣人,b 个恶魔,要么 b 个圣人 a 个恶魔。
凑成圣人个数为 p1 的方案数唯一才能确定,这个东西跑个dp即可,输出方案只要记录前驱及当前更新的是那个连通块的 正/反 点。
注意 DP 数组开两维,不能在之前的上面覆盖,会有连通块更新0个圣人,容易出问题,只是因为我太菜,调了半天
CODE
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72
| #include <iostream> #include <cstdio> #include <algorithm> #include <cmath> #include <vector> using namespace std; const int N = 705; int fa[N<<1],sz[2][N<<1],n,p1,p2,cnt,m; int dp[N][N],pr[N][N],id[N][N],ty[N][N],tp[N][N]; bool vis[N]; int can[N]; int find(int u) {return u==fa[u]?u:fa[u]=find(fa[u]);} void ut(int fx,int fy) { if(fx==fy) return; fa[fy]=fx; sz[0][fx]+=sz[0][fy]; sz[1][fx]+=sz[1][fy]; cnt++; } void ssolve() { int tot=0; for(int i=0;i<=m;i++) pr[0][i]=id[0][i]=ty[0][i]=-1,vis[i]=dp[0][i]=0; pr[0][0]=0;dp[0][0]=1; for(int i=1;i<=m;i++) if(!vis[(find(i)-1)%m+1]) { int _id=(find(i)-1)%m+1; vis[_id]=1;tot++; for(int i=0;i<=p1;i++) pr[tot][i]=id[tot][i]=ty[tot][i]=-1,dp[tot][i]=0; for(int j=min(sz[0][_id],sz[1][_id]);j<=p1;j++) { if(j-sz[0][_id]>=0&&dp[tot-1][j-sz[0][_id]]!=0) { dp[tot][j]+=dp[tot-1][j-sz[0][_id]]; pr[tot][j]=j-sz[0][_id],id[tot][j]=_id,ty[tot][j]=0; } if(j-sz[1][_id]>=0&&dp[tot-1][j-sz[1][_id]]!=0) { dp[tot][j]+=dp[tot-1][j-sz[1][_id]]; pr[tot][j]=j-sz[1][_id],id[tot][j]=_id,ty[tot][j]=1; }
} } if(dp[tot][p1]!=1) printf("no\n"); else { for(int i=0;i<=2*m;i++) can[i]=-1; for(int x=p1;x!=0;x=pr[tot][x],tot--) { can[id[tot][x]]=ty[tot][x]; } vector<int> ans; for(int i=1;i<=2*m;i++) { if(can[find(i)]==(i<=m)) ans.push_back((i-1)%m+1); } sort(ans.begin(),ans.end()); for(int i=0;i<ans.size();i++) printf("%d\n",ans[i]); printf("end\n"); } } int main() { while(cin>>n>>p1>>p2&&n+p1+p2) { m=p1+p2; for(int i=1;i<=2*m;i++) fa[i]=i,sz[i<=m][i]=1,sz[!(i<=m)][i]=0; cnt=0; for(int i=1;i<=n;i++) { int u,v;char s[5]; scanf("%d%d%s",&u,&v,s); int fu0=find(u),fu1=find(u+m),fv0=find(v),fv1=find(v+m); if(s[0]=='n') { ut(fu0,fv1);ut(fu1,fv0); }else { ut(fu0,fv0);ut(fu1,fv1); } } ssolve(); } }
|