并查集好题选做
震惊,我突然发现我并查集题单是这样的:
于是我突然有一种深深的无力感。。。
说实话,有的题还是比较思维的(对于sbwzx来说)。
于是我打算刷一下。
1. luogu P1955 [NOI2015]程序自动分析
震惊,这题我竟然是看了题解才会,活到爆!
首先我上来就没动脑子,敲了两个并查集上去,拿到了30分的好成绩
WA代码:
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int maxn=1000000+10;
int fa0[maxn],fa1[maxn];
ll a[maxn<<1],b[maxn];
int cnt;
struct node{
ll x,y;
int xx,yy,flag;
}s[maxn];
int n;
bool cmp1(ll x,ll y){
return x<y;
}
int Find0(int x){
return x==fa0[x] ? x : (fa0[x]=Find0(fa0[x])) ;
}
void Merge0(int x,int y){
int rx=Find0(x);
int ry=Find0(y);
if(rx==ry) return;
fa0[rx]=ry;
}
bool is_linked0(int x,int y){
return Find0(x)==Find0(y) ;
}
int Find1(int x){
return x==fa1[x] ? x : (fa1[x]=Find1(fa1[x])) ;
}
void Merge1(int x,int y){
int rx=Find1(x);
int ry=Find1(y);
if(rx==ry) return;
fa1[rx]=ry;
}
bool is_linked1(int x,int y){
return Find1(x)==Find1(y) ;
}
void Solve(){
int T;scanf("%d",&T);
while(T--){
scanf("%d",&n);
cnt=0;
for(int i=1;i<=n;++i){
scanf("%lld%lld%d",&s[i].x,&s[i].y,&s[i].flag);
a[i*2-1]=s[i].x;
a[i*2]=s[i].y;
}
sort(a+1,a+n*2+1,cmp1);
for(int i=1;i<=n*2;++i) if(a[i]!=a[i-1]) b[++cnt]=a[i];
for(int i=1;i<=n;++i){
s[i].xx=lower_bound(b+1,b+cnt+1,s[i].x)-b;
s[i].yy=lower_bound(b+1,b+cnt+1,s[i].y)-b;
}
for(int i=1;i<=cnt;++i) fa0[i]=fa1[i]=i;
int ff=1;
for(int i=1;i<=n;++i){
if(s[i].flag){
if(s[i].xx!=s[i].yy&&is_linked0(s[i].xx,s[i].yy)){
ff=0;
break;
}else Merge1(s[i].xx,s[i].yy);
}else{
if(is_linked1(s[i].xx,s[i].yy)){
ff=0;
break;
}else Merge0(s[i].xx,s[i].yy);
}
}
if(ff) printf("YES\n");
else printf("NO\n");
}
}
int main(){
Solve();
return 0;
}
后来小编思考了一下,为什么会WA呢?
原来,是因为等于具有传递性,可以上并查集维护。但不等号并没有传递性,是不能用并查集维护的!
于是sbwzx就不会了。
看过题解以后,小编直呼,原来这么简单!活到爆!
原来,先把答案离线下来,排个序,先把等于的限制条件并在一起,然后对于每一个不等的限制条件,直接判断两个数是否在同一棵树里即可!
正确性显然。
震惊!折磨简单的做法,sbwzx竟然没想出来!
可能这就是sbwzx之所以是sb吧。
然后离散化一下,就A了。
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int maxn=1000000+10;
int fa1[maxn];
ll a[maxn<<1],b[maxn];
int cnt;
struct node{
ll x,y;
int xx,yy,flag;
}s[maxn];
int n;
bool cmp1(ll x,ll y){
return x<y;
}
int Find1(int x){
return x==fa1[x] ? x : (fa1[x]=Find1(fa1[x])) ;
}
void Merge1(int x,int y){
int rx=Find1(x);
int ry=Find1(y);
if(rx==ry) return;
fa1[rx]=ry;
}
bool is_linked1(int x,int y){
return Find1(x)==Find1(y) ;
}
bool cmp2(node A,node B){
return A.flag>B.flag;
}
void Solve(){
int T;scanf("%d",&T);
while(T--){
scanf("%d",&n);
cnt=0;
for(int i=1;i<=n;++i){
scanf("%lld%lld%d",&s[i].x,&s[i].y,&s[i].flag);
a[i*2-1]=s[i].x;
a[i*2]=s[i].y;
}
sort(a+1,a+n*2+1,cmp1);
sort(s+1,s+n+1,cmp2);
for(int i=1;i<=n*2;++i) if(a[i]!=a[i-1]) b[++cnt]=a[i];
for(int i=1;i<=n;++i){
s[i].xx=lower_bound(b+1,b+cnt+1,s[i].x)-b;
s[i].yy=lower_bound(b+1,b+cnt+1,s[i].y)-b;
}
for(int i=1;i<=cnt;++i) fa1[i]=i;
int ff=1;
for(int i=1;i<=n;++i){
if(s[i].flag) Merge1(s[i].xx,s[i].yy);
else{
if(is_linked1(s[i].xx,s[i].yy)){
ff=0;
break;
}
}
}
if(ff) printf("YES\n");
else printf("NO\n");
}
}
int main(){
Solve();
return 0;
}
2. luogu P1196 [NOI2002]银河英雄传说
经典好题。据说是国内并查集的开山之作。
但虽然是第一次出,却不是板子。。。
于是我又看了题解。。。Wtcl
后来是发现我不会带权并查集?
好家伙,第一次出并查集就带权,不愧是NOI
解析:
首先对于询问两个战舰是否在一个集合,以及合并两个集合的操作,显然可以用并查集解决。如果要询问两个战舰之间的数量。。。如果不会带权并查集的话,我选择LCT我就只能暴力跳,那显然T飞了。
这题因为钦定了把x放到y后面,所以显然不能按秩合并,那考虑怎么把询问和路径压缩放到一起。
我们可以选择在递归找祖先的时候把儿子的信息顺便维护一下。
路径压缩的基本思想大概是在回溯的时候更新每个节点的父亲为祖先,强行改变树的形态。
那可以维护两个数组front[],num[],分别表示该点距离祖先的距离和该点所在的子树的大小。
在回溯的时候顺便把这两个值更新一下,就搞定了。。。
代码:
#include <bits/stdc++.h>
using namespace std;
const int maxn=30000+10;
int fa[maxn],depth[maxn],front[maxn],num[maxn];
int n=30000;
int T;
int find(int x){
if(x==fa[x]) return x;
int fx=find(fa[x]);
front[x]+=front[fa[x]];
fa[x]=fx;
return fx;
}
void Merge(int x,int y){
int fx=find(x);
int fy=find(y);
front[fx]+=num[fy];
fa[fx]=fy;
num[fy]+=num[fx];
num[fx]=0;
}
void Solve(){
for(int i=1;i<=n;++i){
fa[i]=i;
num[i]=1;
}
scanf("%d",&T);
char ccc[5];
for(int i=1,x,y;i<=T;++i){
scanf("%s%d%d",ccc,&x,&y);
if(ccc[0]=='M') Merge(x,y);
else {
if(find(x)!=find(y)) printf("-1\n");
else printf("%d\n",abs(front[x]-front[y])-1);
}
}
}
int main(){
Solve();
return 0;
}
3. luogu P2024 [NOI2001]食物链
经典好题。我惊讶的发现我还不会扩展域并查集。于是学习了一下。
大概就是拆点吧,这个题可以令:
在x所在的集合中,属于(1,n]的元素都是x的同类;属于(n+1,2n]的元素都是x的天敌;属于(2n+1,3n]的元素都是x的猎物。
可以发现如果这样定义,两个点间的关系可能会有3种,这样判断一句话是不是真,只需要判断与这句话相对的两句话是不是真即可。
代码:
#include <bits/stdc++.h>
using namespace std;
const int maxn=50000+10;
#define gc() (p1 == p2 ? (p2 = buf + fread(p1 = buf, 1, 1 << 20, stdin), p1 == p2 ? EOF : *p1++) : *p1++)
#define read() ({ register int x = 0, f = 1; register char c = gc(); while(c < '0' || c > '9') { if (c == '-') f = -1; c = gc();} while(c >= '0' && c <= '9') x = x * 10 + (c & 15), c = gc(); f * x; })
char buf[1 << 20], *p1, *p2;
int fa[maxn*3];
int k,n,ans;
int find(int x){
return x==fa[x] ? x : (fa[x]=find(fa[x])) ;
}
bool is_linked(int x,int y){
return find(x)==find(y);
}
void Merge(int x,int y){
int rx=find(x);
int ry=find(y);
if(rx==ry) return;
fa[rx]=ry;
}
void Solve(){
scanf("%d%d",&n,&k);
for(int i=1;i<=n*3;++i) fa[i]=i;
for(int i=1,op,x,y;i<=k;++i){
scanf("%d%d%d",&op,&x,&y);
if(x>n||y>n){
ans++;
//printf("i==%d\n",i);
continue;
}
if(op==1){
if(is_linked(x,y+n)||is_linked(x,y+2*n)){
ans++;
//printf("i==%d\n",i);
continue;
}else{
Merge(x,y);
Merge(x+n,y+n);
Merge(x+n*2,y+n*2);
}
}else{
if(is_linked(x,y)||is_linked(x+n,y)){
ans++;
//printf("i==%d\n",i);
continue;
}else{
Merge(x,y+n);
Merge(x+n,y+n*2);
Merge(x+n*2,y);
}
}
}
printf("%d\n",ans);
}
int main(){
Solve();
return 0;
}