并查集

模板: 并查集

三个核心函数

int getf(int x){return fa[x]==x?x:getf(fa[x]);}
void merge(int v,int u){
	int temp1,temp2;
	temp1=getf(v); temp2=getf(u);
	if(temp2<temp1) swap(temp2,temp1);
	if(temp1!=temp2) fa[temp2]=temp1;
}
bool find(int u,int v){
	int temp1,temp2;
	temp1=getf(v); temp2=getf(u);
	return temp1==temp2;
}
  • 按秩合并:这里把 s i z e size size小的合并到的 s i z e size size大的上面去(我没写)

  • 路径压缩:其中 g e t f getf getf函数还可以进行路径压缩

int getf(int x){return fa[x]==x?x:fa[x]=getf(fa[x]);}

还能这么写

int getf(int x){
	while(x!=fa[x]) x=fa[x]=fa[fa[x]];
	return x;
}

注意要初始化哦

for(int i=1;i<=n;i++) fa[i]=i;

例题1: Mocha and Diana

这个题有一个简单版,就是 n 2 n^2 n2​​的枚举 i i i​​, j j j​​转移,看他们行不行.但是当 n < = 1 0 5 n<=10^5 n<=105的时候就不行了

首先的思路就是不能每次枚举 i i i的时候都枚举 j j j,然后我们又想,只要 j j j​有一次被合并了之后,或者他的森林被合并了之后,就最好不要去枚举了,节省时间.所以我们就找一种方式,让枚举有顺序,让枚举 j j j的时候稍微不那么暴力.

  • 我们先让所有能够合并在 1 1 1号节点的全部先合并到 1 1 1号节点上去
  • 所以现在我们再来枚举 i i i j j j的时候:
    • i i i​有三种情况: 1. 1. 1. i i i​是在并查集 1 1 1​里面的 2. 2. 2. i i i​​是在以前被合并过的并查集里,但不是并查集 1 1 1里面的 3. 3. 3. i i i没有被合并过,而且不能合并进集合 1 1 1里面
    • j j j也有和上面三种一模一样的情况
  • 现在考虑筛选完之后的点对 ( i , j ) (i,j) (i,j)的特点, i i i在Mocha和Diana的并查集中,一个连接着 1 1 1,一个没有连接着 1 1 1, j j j也是这样的,所以我们加了 ( i , j ) (i,j) (i,j)之后, i , j i,j i,j所在的森林全部都连接到了并查集 1 1 1上.举个例子,也就是样例三跑完第一次之后: 5 5 5在Mocha没连接 1 1 1,在Diana连接了 1 1 1,那为了满足 ( i , j ) (i,j) (i,j)的性质, 7 7 7在Mocha连接了1,在Diana没连接1,所以每次加入边 ( i , j ) (i,j) (i,j)之后 i , j i,j i,j所在的森林全部都连接到了并查集 1 1 1上,所以我们就把这个连通块(森林)标记,以后也没有枚举的必要了,因为连通块里面都不满足 ( i , j ) (i,j) (i,j)的性质,只能找新的 ( i , j ) (i,j) (i,j)
#include<cstdio>
#include<algorithm>
#include<cstring>
#define maxn 100100
using namespace std;
int n,m1,m2,k,mark1[maxn],mark2[maxn];
struct node{
	int fa[maxn];
	int getfa(int x){return fa[x]==x?x:fa[x]=getfa(fa[x]);}
	void merge(int u,int v){
		int temp1=getfa(u),temp2=getfa(v);
		if(temp1>temp2) swap(temp1,temp2);
		fa[temp2]=temp1; 
	}
}a,b;
struct Node{
	int x,y;
}ans[maxn];
int main(){
	scanf("%d %d %d",&n,&m1,&m2);
	for(int i=1;i<=n;i++) a.fa[i]=b.fa[i]=i;
	for(int i=1,u,v;i<=m1;i++) scanf("%d %d",&u,&v),a.merge(u,v);
	for(int i=1,u,v;i<=m2;i++) scanf("%d %d",&u,&v),b.merge(u,v);
	for(int i=1;i<=n;i++){
		if(a.getfa(i)==1 || b.getfa(i)==1) continue;
		a.merge(1,i); b.merge(1,i);
		ans[++k].x=1; ans[k].y=i;
	}
	for(int i=1,j=1;i<=n;i++){
		if(a.getfa(i)==1 || mark1[a.getfa(i)]) continue;
		while(j<=n && (b.getfa(j)==1 || mark2[b.getfa(j)])) j++;
		if(j>n) break;
		mark1[a.getfa(i)]=mark2[b.getfa(j)]=1;
		ans[++k].x=i; ans[k].y=j;
	}
	printf("%d\n",k);
	for(int i=1;i<=k;i++) printf("%d %d\n",ans[i].x,ans[i].y);
	return 0;
}

例题2:Cow and Snacks

题意:每个牛都有两种喜欢的食物,它必须吃到一种,不然就不高兴,问一共有几个牛牛不能被满足(安排好顺序)

  • 牛牛的顺序是肯定会影响答案的,例如 1   2 , 2   3 , 3   4 1\ 2,2\ 3,3\ 4 1 2,2 3,3 4 1   2 , 3   4 , 2   3 1\ 2,3 \ 4,2 \ 3 1 2,3 4,2 3这样安排顺序答案是不一样的
  • 贪心的来说,让一个牛牛只吃一种就是最好的,我们先来建边,看看会是什么样子(样例)

图一.png

  • 我们要是把顺序问题给解决就好办了

  • 可以发现,只要我们能够连接一条边,两条边都属于一个不同的并查集,那么我们就可以通过一些操作使这个牛牛被满足

    • 左右是一个并查集,那无论如何这个牛牛也不得行了
    • 左右不是一个并查集,左右为空,牛牛吃两个,左右不为空,左边牛牛两个,中间和右边牛牛吃一个,就行了,其他情况都同理.
#include<cstdio>
#include<algorithm>
#include<cstring>
#define maxn 100010
using namespace std;
int n,k,ans,fa[maxn];
int find(int x){
	while(x!=fa[x]) x=fa[x]=fa[fa[x]];
	return x;
}
int main(){
	scanf("%d %d",&n,&k);
	for(int i=1;i<=n;i++) fa[i]=i;
	for(int i=1,x,y;i<=k;i++) {
		scanf("%d %d",&x,&y);
		int fx=find(x);
		int fy=find(y);
		if(fx!=fy) fa[fx]=fy;
		else ans++;
	}
	printf("%d\n",ans);
	return 0;
}

例题3:Unstable String Sort
  • 首先,我们要知道 a i a_i ai b i b_i bi是可以整成一个环的,也就是说 a i → j a_{i\to j} aij b i → j b_{i\to j} bij两个合起来看是矛盾的,这个时候环内就是一个字母就行了.
  • 那问题就变成了怎么缩点了,缩点我们就用并查集来缩就行了,用两个指针,没有矛盾的时候就是 f a [ a [ h 1 ] ] = f a [ b [ h 2 ] ] fa[a[h1]]=fa[b[h2]] fa[a[h1]]=fa[b[h2]]的时候,但是我们要在 h 1 = h 2 h1=h2 h1=h2的时候标记,而且要标记 h 1 + 1 h1+1 h1+1这个位置,表示 h 1 h1 h1之后的位置是一个新的点了,这个时候就可以选择字母变一下,或者不变也行
#include<cstdio>
#include<algorithm>
#include<cstring>
#define maxn 500000
using namespace std;
int n,k,a[maxn],b[maxn],fa[maxn],pos[maxn],cnt;
char ans[maxn];
int find(int x) {return fa[x]==x?x:fa[x]=find(fa[x]);}
void merge(int x,int y){fa[find(x)]=find(y);}
signed main(){
	scanf("%d %d",&n,&k);
	for(int i=1;i<=n;i++) scanf("%d",&a[i]);
	for(int i=1;i<=n;i++) scanf("%d",&b[i]);
	for(int i=1;i<=n;i++) fa[i]=i;
	for(int h1=1,h2=1;h1<=n && h2<=n;h2++){
		while(find(a[h1])!=find(b[h2]) && h1<=n) merge(a[h1],a[h1+1]),h1++;
		if(h1==h2) cnt++,pos[h1+1]=1;
	}
	if(cnt<k) printf("NO\n"),exit(0);
	printf("YES\n");
	for(int i=1,now=0;i<=n;i++) now=min(now+pos[i],k-1),ans[a[i]]='a'+now;
	for(int i=1;i<=n;i++) printf("%c",ans[i]);
	return 0;
}

例题4:扩散(无链接)

题目描述
一个点每过一个单位时间就会向 4 4 4个方向扩散一个距离,两个点 a , b a,b a,b连通,记作 e ( a , b ) e(a,b) e(a,b),当且仅当 a , b a,b a,b的扩散区域有公共部分。连通块的定义是块内的任意两个点 u , v u,v u,v都必定存在路径 e ( u , a 0 ) , e ( a 0 , a 1 ) , … e ( a k , v ) e(u,a0),e(a0,a1),…e(ak,v) e(u,a0),e(a0,a1),e(ak,v).给定平面上的 n n n个点,问最早什么时候它们形成一个连通块。

输入
第一行一个数 n n n ,以下 n n n 行,每行一个点坐标。

输出
输出仅一个数,表示最早的时刻所有点形成连通块。

样例输入
2
0 0
5 5
样例输出
5

对于 100 % 100\% 100% 的数据,满足 1 ≤ n ≤ 50 , 1 ≤ X i , Y i ≤ 1 0 9 1≤n≤50,1≤Xi,Yi≤10^9 1n50,1Xi,Yi109.

这道题就是二分一个时刻,然后再 c h e c k check check一下是不是所有的点都连到了一起,举出这个题只是想说明并查集能够优化做题的代码难度和时间复杂度,以及引出下一题.

#include<cstdio>
#include<algorithm>
#include<cstring>
#define maxn 1010
using namespace std;
int n,fa[maxn];
struct posnode{
	int x,y;
}pos[maxn];
int find(int x){
	while(x!=fa[x]) x=fa[x]=fa[fa[x]];
	return x;
}
bool dis(int x,int y,int val) {return (abs(pos[x].x-pos[y].x)+abs(pos[x].y-pos[y].y))<=val;}
bool check(int x){
	int cnt=0;
	for(int i=1;i<=n;i++) fa[i]=i;
	for(int i=1;i<=n;i++) for(int j=i+1;j<=n;j++) if(dis(i,j,x*2)) fa[find(j)]=find(i);
	for(int i=1;i<=n;i++) if(fa[i]==i) cnt++;
	return cnt==1;
}
int main(){
	scanf("%d",&n);
	for(int i=1;i<=n;i++) scanf("%d %d",&pos[i].x,&pos[i].y);
	int ans=0;
	int l=0,r=2e8;
	while(l<=r){
		int mid=l+r>>1;
		if(check(mid)) ans=mid,r=mid-1;
		else l=mid+1;
	}
	printf("%d\n",ans);
	return 0;
}

以下两个题都不是用并查集做的,但是也是很有价值,很类似的题
例题5:喷水装置(无链接)

题目描述
L L L 米,宽 W W W米的草坪里装有 n n n个浇灌喷头。每个喷头都装在草坪中心线上(离两边各 W / 2 W/2 W/2 米)。我们知道每个喷头的位置(离草坪中心线左端的距离),以及它能覆盖到的浇灌范围。
请问:如果要同时浇灌整块草坪,最少需要打开多少个喷头?

输入
第一行一个整数 T T T表示数据组数;
每组数据的第一行是整数 n , L n,L n,L W W W
接下来的 n n n 行,每行包含两个整数,给出一个喷头的位置和浇灌半径(上面的示意图是样例输入第一组数据所描述的情况)。

输出
对每组测试数据输出一个数字,表示要浇灌整块草坪所需喷头数目的最小值。如果所有喷头都打开也不能浇灌整块草坪,则输出 -1 。

样例输入
3
8 20 2
5 3
4 1
1 2
7 2
10 2
13 3
16 2
19 4
3 10 1
3 5
9 3
6 1
3 10 1
5 3
1 1
9 1

样例输出
6
2
-1

数据范围对于 100 % 100\% 100%的数据, n < = 15000 n<=15000 n<=15000

这个题就是一个线段覆盖,和上道题很像,但主要的问题是这是一个二维的东西,怎么变成一维的东西,其实我们可以只看矩形的最上面那条边(或者下面),只要这条边覆盖了,那么整个矩形必定覆盖了.然后就又转换成了线段覆盖问题.

#include<cstdio>
#include<algorithm>
#include<cstring>
#include<cmath>
#define maxn 15010
using namespace std;
int T,n,l,w,cnt,ans;
struct node{
	double l,r;
}a,water[maxn];
inline bool cmp(node a,node b) {return a.l<b.l;}
int main(){
	scanf("%d",&T);
	while(T--){
		cnt=ans=0;
		scanf("%d %d %d",&n,&l,&w);
		for(int i=1,temp,r;i<=n;i++){
			scanf("%d %d",&temp,&r);
			if(r*2<=w) continue;
			a.l=temp-sqrt(r*r-w*w/4.0);//只要把交于长的点覆盖 
			a.r=temp+sqrt(r*r-w*w/4.0);//那么一定中间也能覆盖 
			water[++cnt]=a;
		}
		sort(water+1,water+1+cnt,cmp);
		double end=0,right=0;
		for(int i=1;i<=cnt && end<l;i++){
			if(water[i].l>end) end=right,ans++;
			if(water[i].l<=end) right=max(right,water[i].r);
		}
		if(end<l) end=right,ans++;
		if(end>=l) printf("%d\n",ans);
		else printf("-1\n");
	}
	return 0;
}

例题6:Circles

赛后总结:我以为我丢精了,结果不完全是,没开 l o n g   l o n g long\ long long long,所以爆了居然,所以在 d i s = ( x [ i ] − x [ j ] ) ∗ ( x [ i ] − x [ j ] ) + ( y [ i ] − y [ j ] ) ∗ ( y [ i ] − y [ j ] ) dis=(x[i]-x[j])*(x[i]-x[j])+(y[i]-y[j])*(y[i]-y[j]) dis=(x[i]x[j])(x[i]x[j])+(y[i]y[j])(y[i]y[j])计算的时候爆了,所以在 s q r t ( d i s ) sqrt(dis) sqrt(dis)的时候 d i s dis dis还可能出现负数,再然后就是 π \pi π可以最后来乘上去,可能对精度有改善吧,而且乘 a c o s ( − 1 ) acos(-1) acos(1)更好,最后再来看题本身,首先没问题的是距离短的先考虑,但是又有问题了,两个点直接距离虽然短,但是也有不行的时候,因为有可能其中有一个点不能再扩散了,所以我们现在不看最短距离,我看当前的扩散速度,把他们放进优先队列里面去弄就行了.

总结:这个题和上面的两个覆盖题非常相似,但是这个题使用的方法截然不同,值得放在一起总结,关于类似并查集和覆盖的问题,在网络流和图论里面也有所涉及,先挖个坑,后面补.

#include<cstdio>
#include<algorithm>
#include<cstring>
#include<queue>
#include<cmath>
#define int long long
using namespace std;
int n,x[5000],y[5000];
double ans,r[5000];
struct node{
	int i,j;
	double dis;
	friend bool operator < (node a,node b) {return a.dis>b.dis;}
}temp;
priority_queue<node> q; 
signed main(){
	scanf("%lld",&n);
	for(int i=1;i<=n;i++) scanf("%lld %lld",&x[i],&y[i]);
	for(int i=1;i<=n;i++){
		for(int j=i+1;j<=n;j++){
			temp.dis=hypot((x[i]-x[j]),(y[i]-y[j]))/2.0; temp.i=i; temp.j=j;
			q.push(temp);
		}
	}
	while(!q.empty()){
		temp=q.top(); q.pop();
		if(r[temp.i] && r[temp.j]) continue;
		else if(!r[temp.i] && !r[temp.j]){
			r[temp.i]=r[temp.j]=temp.dis;
			ans+=r[temp.i]*r[temp.i]*2;
		}else{
			double dis=(x[temp.i]-x[temp.j])*(x[temp.i]-x[temp.j])+(y[temp.i]-y[temp.j])*(y[temp.i]-y[temp.j]);
			dis=sqrt(dis);
			if(r[temp.i]){
				if(abs(r[temp.i]+temp.dis-dis)<=0.0001){
					r[temp.j]=temp.dis;
					ans+=temp.dis*temp.dis;
				}else{
					temp.dis=dis-r[temp.i];
					q.push(temp);
				}
			}
			else if(r[temp.j]){
				if(abs(r[temp.j]+temp.dis-dis)<=0.0001){
					r[temp.i]=temp.dis;
					ans+=temp.dis*temp.dis;
				}else{
					temp.dis=dis-r[temp.j];
					q.push(temp);
				}
			}
		}
	}
	printf("%.9lf\n",acos(-1)*ans);
	return 0;
}

例题7:货物收集

根据题意,可以二分一个健身的时间,然后 O n On On的遍历树,寻找是否可行,这样是 n l o g nlog nlog的时间复杂度,跑得快就能过.还有一种方法,还是得先排序,也是个 n l o g nlog nlog,然后依次把边加进去,直到 1 1 1的连通块达到所需货物的时候,就输出就行了

#include<cstdio>
#include<algorithm>
#include<cstring>
#define maxn 1000100
using namespace std;
int n,w,data[maxn],fa[maxn];
struct node{
    int u,v,val;
}edge[maxn];
inline bool cmp(node a,node b) {return a.val<b.val;}
int find(int x){
    while(x!=fa[x]) x=fa[x]=fa[fa[x]];
    return x;
}
int main(){
    scanf("%d %d",&n,&w);
    for(int i=2;i<=n;i++) scanf("%d",&data[i]),fa[i]=i;
    for(int i=1;i<n;i++) scanf("%d %d %d",&edge[i].u,&edge[i].v,&edge[i].val);
    sort(edge+1,edge+n,cmp);
    for(int i=1;i<n;i++){
        int fx=find(edge[i].u);
        int fy=find(edge[i].v);
        if(fx==fy) continue;
        if(fx>fy) swap(fx,fy);
        fa[fy]=fx; data[fx]+=data[fy];
        if(data[find(1)]>=w) printf("%d\n",edge[i].val),exit(0);
    }
    return 0;
}

例题8:逐个击破

这个题目和上一个是差不多的,我们可以先假设所有的边已经连接上去了的,然后我们就来删除边,当然是删除的越多就越好,所以能删的就删,我们最开始的时候让不同的点在不同的并查集里面,当删除边之后他们两个分开之后,就弄到一个并查集里就行了.

#include<cstdio>
#include<algorithm>
#include<cstring>
#define maxn 101000
using namespace std;
int n,m,fa[maxn];
long long sum;
bool book[maxn];
struct node{
	int u,v,w;
}edge[maxn];
int find(int x){
	while(x!=fa[x]) x=fa[x]=fa[fa[x]];
	return x;
}
bool cmp(node a,node b) {return a.w>b.w;}
int main(){
	scanf("%d %d",&n,&m);
	for(int i=1;i<=n;i++) fa[i]=i;
	for(int i=1,x;i<=m;i++) scanf("%d",&x),book[x]=true;
	for(int i=1;i<n;i++) scanf("%d %d %d",&edge[i].u,&edge[i].v,&edge[i].w),sum+=edge[i].w;
	sort(edge+1,edge+n,cmp);
	for(int i=1;i<n;i++){
		int fx=find(edge[i].u);
		int fy=find(edge[i].v);
		if(fx==fy || (book[fx] && book[fy])) continue;
		sum-=edge[i].w;
		fa[fx]=fy;
		if(book[fx]||book[fy]) book[fy]=true;
	}
	printf("%lld\n",sum);
	return 0;
}

例题9:信息传递

这个有一点不一样,但是其实就是找一个最小的环,可以用 d f s dfs dfs,但是也可以用带权并查集,也就是在其他的基础上加一个深度.

#include<cstdio>
#include<algorithm>
#include<cstring>
#define maxn 200010
using namespace std;
int fa[maxn],deep[maxn],ans=maxn,last,n;
int find(int x) {
	if(fa[x]!=x){
		int father=fa[x];
		fa[x]=find(fa[x]);
		deep[x]+=deep[father];
	}
	return fa[x];
}
void check(int x,int y){
	int fx=find(x),fy=find(y);
	if(fx==fy) ans=min(ans,deep[x]+deep[y]+1);
	else deep[x]=deep[y]+1,fa[fx]=fy;
}
signed main(){
	scanf("%d",&n);
	for(int i=1;i<=n;i++) fa[i]=i;
	for(int i=1,temp;i<=n;i++) scanf("%d",&temp),check(i,temp);
	printf("%d\n",ans);
	return 0;
}

例题10:食物链

我们可以将并查集扩大, 1 → n 1 \to n 1n表示群系 A A A, n + 1 → 2 n n+1 \to 2n n+12n表示群系 B B B, 2 n + 1 → 3 n 2n+1\to 3n 2n+13n表示群系 C C C, x x x y y y,我们就连边 x → y + n x \to y+n xy+n, x + n → y + 2 ∗ n x+n\to y+2*n x+ny+2n, x + 2 ∗ n → y x+2*n\to y x+2ny

  • 对于一个群系,他的食物是什么就看这个群系和下一个群系有没有连边
  • 对于一个群系,他的天敌是什么就看上一个群系和这个群系有没有连边.
  • 对于一个群系,他和谁是同类就看 x y xy xy是否在一个并查集内,或者他是不是被另外一个吃,或者是不是吃另外一个

加上一个图可以自己画画理解理解.

7)

#include<cstdio>
#include<algorithm>
#include<cstring>
#define maxn 200010
using namespace std;
int n,m,fa[maxn*3],opt,x,y,ans;
int find(int x) {return fa[x]==x?fa[x]:fa[x]=find(fa[x]);}
void merge(int x,int y) {fa[find(x)]=find(y);}
signed main(){
	scanf("%d %d",&n,&m);
	for(int i=1;i<=3*n;i++) fa[i]=i;
	for(int i=1;i<=m;i++){
		scanf("%d %d %d",&opt,&x,&y);
		if(x>n||y>n) ans++;
		else if(opt==1){
			if(find(x)==find(y+n) || find(x)==find(y+2*n)) ans++;
			else merge(x,y),merge(x+n,y+n),merge(x+2*n,y+2*n);
		}else {
			if(find(x)==find(y) || find(x)==find(y+2*n)) ans++;
			else merge(x,y+n),merge(x+n,y+2*n),merge(x+2*n,y);
		}
	}
	printf("%d\n",ans);
	return 0;
}

例题11:关押罪犯
  • 这个题做法和上一题类似,也是种类并查集,将 f a fa fa复制一次,再将每一组 a i , b i , c i a_i,b_i,c_i ai,bi,ci按照 c i c_i ci从大到小排序,因为只有两个牢,所以要么丢去这个要么丢去那个,所以到了第一个不能调开的地方,答案就是这一个 c i c_i ci,也就是 1 & 2 1\&2 1&2, 2 & 3 2\&3 2&3, 3 & 1 3\&1 3&1,这种情况就只能 1 , 3 1,3 1,3一组, 2 2 2单独一组
  • x x x的敌对域是 [ 1 , n ] [1,n] [1,n] ,友好域是 [ n + 1 , 2 n ] [n+1,2n] [n+1,2n],所以后面的就好理解了.
#include<cstdio>
#include<algorithm>
#include<cstring>
#define maxn 100100
using namespace std;
int n,m,fa[maxn];
struct node{
	int x,y,val;
}data[maxn];
bool cmp(node a,node b) {return a.val>b.val;}
int find(int x) {return x==fa[x]?x:fa[x]=find(fa[x]);}
void merge(int x,int y) {fa[find(x)]=find(y);}
signed main(){
	scanf("%d %d",&n,&m);
	for(int i=1;i<=2*n;i++) fa[i]=i;
	for(int i=1;i<=m;i++) scanf("%d %d %d",&data[i].x,&data[i].y,&data[i].val);
	sort(data+1,data+1+m,cmp);
	for(int i=1;i<=m;i++){
		if(find(data[i].x)==find(data[i].y)) printf("%d\n",data[i].val),exit(0);
		merge(data[i].x,data[i].y+n); merge(data[i].x+n,data[i].y);
	}
	printf("0\n");
	return 0;
}

例题12:银河英雄传说

这个题和例题 9 9 9很像,都是带权并查集.

#include<cstdio>
#include<algorithm>
#include<cstring>
#define maxn 30010
using namespace std;
int n,fa[maxn],size[maxn],deep[maxn]; char opt;
int find(int x){
	if(x==fa[x]) return x;
	int temp=find(fa[x]);
	deep[x]+=deep[fa[x]];
	return fa[x]=temp;
}
void merge(int x,int y){
	int tempx=find(x),tempy=find(y);
	deep[tempy]=size[tempx];
	size[tempx]+=size[tempy];
	fa[tempy]=tempx;
}
signed main(){
	scanf("%d",&n);
	for(int i=1;i<=30000;i++) fa[i]=i,size[i]=1;
	for(int i=1,x,y;i<=n;i++){
		scanf("\n%c %d %d",&opt,&x,&y);
		if(opt=='M') merge(x,y);
		else printf("%d\n",find(x)==find(y)?abs(deep[x]-deep[y])-1:-1);
	}
	return 0;
}

例题13:DZY Loves Planting

a

搬运机房大佬题解

a]

#include<cstdio>
#include<algorithm>
#include<cstring>
#define maxn 200000
using namespace std;
int n,data[maxn],sum,fa[maxn],size[maxn];
struct node{
	int u,to,w;
}edge[maxn];
bool cmp(node a,node b) {return a.w<b.w;}
int find(int x) {return x==fa[x]?x:fa[x]=find(fa[x]);}
signed main(){
	scanf("%d",&n);
	for(int i=1;i<n;i++) scanf("%d %d %d",&edge[i].u,&edge[i].to,&edge[i].w);
	for(int i=1;i<=n;i++) scanf("%d",&data[i]),sum+=data[i],fa[i]=i,size[i]=1;
	sort(edge+1,edge+n,cmp);
	for(int i=1;i<n;i++){
		int tempx=find(edge[i].u),tempy=find(edge[i].to);
		size[tempx]+=size[tempy]; fa[tempy]=tempx;  data[tempx]+=data[tempy];
		if(size[tempx]>sum-data[tempx]) printf("%d\n",edge[i].w),exit(0);
	}
	printf("%d\n",edge[n-1].w);
	return 0;
}

例题14:毒瘤题 [无链接]

题目描述
Tom热爱出毒瘤题,但是这次他似乎除了一个并不毒瘤的题。给定一张N个点M条无向边的图,每条边有边权w。定义一个联通块的大小为该连通块内所有边权的和。然后进行K组询问。每次询问在由所有边权不超过Xi的边构成的新的生成子图中联通块的大小的最大值。

输入
第一行三个数N,M,K,表示图有N个点,M条边,有K组询问;
后面m行每行三个正整数u,v,w,表示点u与点v之间有一条边权为w的边。
后面k行每行一个整数x,表示在这次询问中,只能使用边权不超过x的边,数据可能存在重边,但不存在自环。

输出
K行,对于每组询问输出一次答案

样例输入
5 5 3
1 2 5
2 3 6
3 4 4
3 5 2
4 5 3
7 5 3

样例输出
20
9
5
对于100%的数据,N,M,K<=100000,w<=1000000,x<=int。

一眼就能看出是并查集,我们把询问离线,把询问排序,然后依次加入边.

#pragma GCC optimize("Ofast")
#pragma GCC optimize(3)
#include<cstdio>
#include<algorithm>
#include<cstring>
#define maxn 1000000
using namespace std;
int n,m,q,fa[maxn];
long long ans[maxn],val[maxn],Max;
int read(){
    register int x=0,f=1; register char ch=getchar();
    while(ch<'0' || ch>'9') {if(ch=='-') f=-1; ch=getchar();}
    while(ch>='0' && ch<='9') x=x*10+ch-'0',ch=getchar();
    return x*f;
}
struct node{
    int u,v,w;
}edge[maxn];
struct ASK{
    int val,id;
}ask[maxn];
bool cmp1(ASK a,ASK b) {return a.val<b.val;}
bool cmp2(node a,node b) {return a.w<b.w;}
int find(int x){
    while(x!=fa[x]) x=fa[x]=fa[fa[x]];
    return x;
}
int main(){
    n=read(); m=read(); q=read();
    for(int i=1;i<=n;i++) fa[i]=i;
    for(int i=1;i<=m;i++) edge[i].u=read(),edge[i].v=read(),edge[i].w=read();
    for(int i=1;i<=q;i++) ask[i].val=read(),ask[i].id=i;
    sort(ask+1,ask+1+q,cmp1);
    sort(edge+1,edge+1+m,cmp2);
    for(int pos=1,now=1;pos<=q;pos++){
        for(int i=now;i<=m;i++){
            if(edge[i].w>ask[pos].val) break;
            int fu=find(edge[i].u);
            int fv=find(edge[i].v);
            if(fu==fv){
                val[fu]+=(long long) edge[i].w;
                Max=max(Max,val[fu]);
                now++;
                continue;
            }
            val[fu]=val[fu]+val[fv]+(long long)edge[i].w;
            fa[fv]=fu;
            Max=max(Max,val[fu]);
            now++;
        }
        ans[ask[pos].id]=Max;
    }
    for(int i=1;i<=q;i++) printf("%lld\n",ans[i]);
    return 0;
}

例题15:Moo Tube 双倍经验

这道题应该能算上三倍经验了,和上道题差不多,也是离线加边,看到这个问题我想起了另外一道题,将删边操作离线倒着操作就是加边了,并查集就能用了.

#include<cstdio>
#include<algorithm>
#include<cstring>
#define maxn 200010
using namespace std;
int n,m,size[maxn],k=1,fa[maxn],ans[maxn];
struct node{
	int u,v,w;
}edge[maxn];
struct asknode{
	int k,v,id;
}ask[maxn];
inline bool cmp1(node a,node b) {return a.w>b.w;}
inline bool cmp2(asknode a,asknode b) {return a.k>b.k;}
int find(int x){
	while(x!=fa[x]) x=fa[x]=fa[fa[x]];
	return x;
}
void merge(int x,int y){
	int fx=find(x);
	int fy=find(y);
	if(fx==fy) return ;
	size[fy]+=size[fx]; fa[fx]=fy;
}
int main(){
	scanf("%d %d",&n,&m);
	for(int i=1;i<=n;i++) fa[i]=i,size[i]=1;
	for(int i=1;i<n;i++) scanf("%d %d %d",&edge[i].u,&edge[i].v,&edge[i].w);
	for(int i=1;i<=m;i++) scanf("%d %d",&ask[i].k,&ask[i].v),ask[i].id=i;
	sort(edge+1,edge+1+n,cmp1); sort(ask+1,ask+1+m,cmp2);
	for(int i=1;i<=m;i++){
		while(edge[k].w>=ask[i].k) merge(edge[k].u,edge[k].v),k++;
		ans[ask[i].id]=size[find(ask[i].v)];
	}
	for(int i=1;i<=m;i++) printf("%d\n",ans[i]-1);
	return 0;
}

例题16:刺客信条 [无链接]

刺客信条

【问题描述】

故事发生在1486 年的意大利,Ezio原本只是一个文艺复兴时期的贵族,后来因为家族成员受到圣殿骑士的杀害,决心成为一名刺客。最终,凭借着他的努力和出众的天赋,成为了杰出的刺客大师。刺客组织在他的带领下,为被剥削的平民声张正义,赶跑了原本统治意大利的圣殿骑士首领-教皇亚历山大六世。在他的一生中,经历了无数次惊心动魄、扣人心弦的探险和刺杀。

这次的故事就是他暗杀一位作恶多端的红衣主教。红衣主教可以吸取他周围人的生命力量(以他为圆心的一个圆),而他的红衣教徒也拥有这个力量。红衣主教的家是一个 x ∗ y x*y xy的长方形房间,也就是说,他的家的四个角坐标分别为 ( 0 , 0 ) , ( x , 0 ) , ( 0 , y ) , ( x , y ) (0,0),(x,0),(0,y),(x,y) (0,0),(x,0),(0,y),(x,y)。教堂的门在 ( 0 , 0 ) (0,0) (0,0),而红衣主教就在 ( x , y ) (x,y) (x,y)的卧室休息。他的家中还有 n n n个守护着他的红衣教徒,站在 ( a i , b i ) (ai,bi) (ai,bi)。Ezi想要趁主教休息时,从门进入潜入到他的卧室刺杀他,因为主教休息时会脱下红衣,这样吸取生命的力量就消失了。可是守卫他的红衣教徒依然很危险,离红衣教徒太近就会被吸取生命。因此,Ezi想知道,当红衣教徒的影响范围最大为多少时,Ezi可以不被吸取生命并且成功的刺杀掉红衣主教(当两个红衣教徒的影响范围相切时,可以视为Ezio恰好可以穿过)。注意:教徒都在房间里。

【输入格式】

第一行三个整数 x , y , n x,y,n x,y,n。之后 n n n行,每行两个整数 ,意义见题目描述。

【输出描述】

一行一个数 D D D,表示红衣教徒的最大影响范围,保留两位小数。

【样例输入】

10 20 2

3 3

6 14

【样例输出】

3.00

【样例解释】

贴着墙走

【数据范围】

对于 100 % 100\% 100%的数据, n < = 2000 n<=2000 n<=2000, 0 < = x 0<=x 0<=x, y < = 1 0 6 y<=10^6 y<=106

这个题和例题4非常的类似,也是二分,找扩散的范围,可以 n 2 n^2 n2的枚举两个点,也可以先排一个序,到了“一定程度”就可以 b r e a k break break了,详见代码.然后这里最关键的操作就是把长方体的四条边弄成四个点,这样可以方便的判断是否可以走过去,当上面与下面成为一个连通块(或者上右,左下,左右)就不能走过去了.

#include<cstdio>
#include<algorithm>
#include<cstring>
#define maxn 2005
#define eps 1e-5
#define int long long
using namespace std;
int n,m,k,fa[maxn];
struct posnode{
	int x,y;
}a[maxn];
bool cmp(posnode a,posnode b) {return a.x==b.x?a.y<b.y:a.x<b.x;}
int find(int x){
	while(x!=fa[x]) x=fa[x]=fa[fa[x]];
	return x;
}
void merge(int x,int y){fa[find(x)]=find(y);}
bool check(double x){
	for(int i=1;i<=k+4;i++) fa[i]=i;
	merge(k+1,k+2); merge(k+3,k+4);//1->4:右上左下 
	for(int i=1;i<=k;i++){
		if(a[i].y<x) merge(i,k+1);
		if(m-a[i].y<x) merge(i,k+3);
		if(a[i].x<x) merge(i,k+4);
		if(n-a[i].x<x) merge(i,k+2);
	}
	double basedis=x*x*4.0;//二分出来的距离 
	for(int i=1;i<=k;i++){
		for(int j=i+1;j<=k;j++){
			int num=(a[j].x-a[i].x)*(a[j].x-a[i].x);
			if(num>=basedis) break;
			num+=(a[j].y-a[i].y)*(a[j].y-a[i].y);
			if(num<basedis) merge(i,j);
			if(find(k+2)==find(k+3)) return false;
		}
	}
	return true;
}
signed main(){
	scanf("%lld %lld %lld",&n,&m,&k);
	for(int i=1;i<=k;i++) scanf("%lld %lld",&a[i].x,&a[i].y);
	sort(a+1,a+1+k,cmp);//这样排就是从左下角排到右上角 
	double l=0,r=max(n,m);
	while(r-l>=eps){
		double mid=(l+r)/2.0;
		if(check(mid)) l=mid;
		else r=mid;
	}
	printf("%.2lf\n",l);
	return 0;
}

例题17:星球大战

这道题就是我前面说的删操作离线并倒过来做,然后就变成了加操作,对于没有被摧毁过的点,我们先把他们 m e r g e merge merge,然后倒着来枚举,他攻下一个星球,也就变成了我们恢复一个星球, c n t cnt cnt就是当前连通块的个数. m e r g e merge merge的时候来更新.

#include<cstdio>
#include<algorithm>
#include<cstring>
#define maxn 1000000
using namespace std;
int n,m,k,tot,head[maxn],a[maxn],fa[maxn],size[maxn],ans[maxn],cnt; bool book[maxn];
struct node{
	int to,next;
}edge[maxn];
void add(int u,int v){
	edge[++tot]=(node) {v,head[u]}; head[u]=tot;
	edge[++tot]=(node) {u,head[v]}; head[v]=tot;
}
int find(int x) {return fa[x]==x?x:fa[x]=find(fa[x]);}
void merge(int x,int y){
	if(find(x)==find(y)) return ;
	fa[find(y)]=find(x); cnt--;
}
signed main(){
	scanf("%d %d",&n,&m);
	for(int i=1;i<=n;i++) fa[i]=i;
	for(int i=1,u,v;i<=m;i++) scanf("%d %d",&u,&v),u++,v++,add(u,v);
	scanf("%d",&k);
	for(int i=1;i<=k;i++) scanf("%d",&a[i]),a[i]++,book[a[i]]=true;
	cnt=n-k;
	for(int u=1;u<=n;u++){
		if(book[u]) continue;
		for(int i=head[u];i;i=edge[i].next) 
			if(!book[edge[i].to]) merge(edge[i].to,u);
	} 
	ans[k+1]=cnt;
	for(int u=k;u;u--){
		cnt++; book[a[u]]=false;
		for(int i=head[a[u]];i;i=edge[i].next) 
			if(!book[edge[i].to]) merge(edge[i].to,a[u]);
		ans[u]=cnt;
	}
	for(int i=1;i<=k+1;i++) printf("%d\n",ans[i]);
	return 0;
}

例题18:奶酪

和那个刺客信条也有点像(已经不知道是几倍经验了),正常的弄,如果两个点之间距离够就 m e r g e merge merge,然后如果有可以到奶酪上面的就记录到 f 1 f1 f1中,如果可以从下面穿出去就记录在 f 2 f2 f2中,最后来看有没有一组点 ( f 1 , f 2 ) (f1,f2) (f1,f2),他们是同一个并查集里面的,说明可以从下面走到上面.

#include<cstdio>
#include<algorithm>
#include<cstring>
#define maxn 1010
#define int long long
using namespace std;
int T,n,h,r,x[maxn],y[maxn],z[maxn],fa[maxn],f1[maxn],f2[maxn];
int dis(int x,int y,int z,int xx,int yy,int zz){
	return (x-xx)*(x-xx)+(y-yy)*(y-yy)+(z-zz)*(z-zz);
}
int find(int x){
	while(x!=fa[x]) x=fa[x]=fa[fa[x]];
	return fa[x];
}
signed main(){
	scanf("%lld",&T);
	while(T--){
		int tot1=0,tot2=0;
		scanf("%lld %lld %lld",&n,&h,&r);
		for(int i=1;i<=n;i++) fa[i]=i;
		for(int i=1;i<=n;i++){
			scanf("%lld %lld %lld",&x[i],&y[i],&z[i]);
			if(z[i]+r>=h) f1[++tot1]=i;
			if(z[i]-r<=0) f2[++tot2]=i;
			for(int j=1;j<=i;j++){
				if(dis(x[i],y[i],z[i],x[j],y[j],z[j])>4*r*r) continue;
				fa[find(j)]=find(i);
			}
		}
		bool judge=false;
		for(int i=1;i<=tot1&&(!judge);i++)
			for(int j=1;j<=tot2;j++)
				if(find(f1[i])==find(f2[j])) {judge=true; break;}
		printf("%s\n",judge?"Yes":"No");
	}
	return 0;
}

模板: 并查集

三个核心函数

int getf(int x){return fa[x]==x?x:getf(fa[x]);}
void merge(int v,int u){
	int temp1,temp2;
	temp1=getf(v); temp2=getf(u);
	if(temp2<temp1) swap(temp2,temp1);
	if(temp1!=temp2) fa[temp2]=temp1;
}
bool find(int u,int v){
	int temp1,temp2;
	temp1=getf(v); temp2=getf(u);
	return temp1==temp2;
}
  • 按秩合并:这里把 s i z e size size小的合并到的 s i z e size size大的上面去(我没写)

  • 路径压缩:其中 g e t f getf getf函数还可以进行路径压缩

int getf(int x){return fa[x]==x?x:fa[x]=getf(fa[x]);}

还能这么写

int getf(int x){
	while(x!=fa[x]) x=fa[x]=fa[fa[x]];
	return x;
}

注意要初始化哦

for(int i=1;i<=n;i++) fa[i]=i;

例题1: Mocha and Diana

这个题有一个简单版,就是 n 2 n^2 n2​​的枚举 i i i​​, j j j​​转移,看他们行不行.但是当 n < = 1 0 5 n<=10^5 n<=105的时候就不行了

首先的思路就是不能每次枚举 i i i的时候都枚举 j j j,然后我们又想,只要 j j j​有一次被合并了之后,或者他的森林被合并了之后,就最好不要去枚举了,节省时间.所以我们就找一种方式,让枚举有顺序,让枚举 j j j的时候稍微不那么暴力.

  • 我们先让所有能够合并在 1 1 1号节点的全部先合并到 1 1 1号节点上去
  • 所以现在我们再来枚举 i i i j j j的时候:
    • i i i​有三种情况: 1. 1. 1. i i i​是在并查集 1 1 1​里面的 2. 2. 2. i i i​​是在以前被合并过的并查集里,但不是并查集 1 1 1里面的 3. 3. 3. i i i没有被合并过,而且不能合并进集合 1 1 1里面
    • j j j也有和上面三种一模一样的情况
  • 现在考虑筛选完之后的点对 ( i , j ) (i,j) (i,j)的特点, i i i在Mocha和Diana的并查集中,一个连接着 1 1 1,一个没有连接着 1 1 1, j j j也是这样的,所以我们加了 ( i , j ) (i,j) (i,j)之后, i , j i,j i,j所在的森林全部都连接到了并查集 1 1 1上.举个例子,也就是样例三跑完第一次之后: 5 5 5在Mocha没连接 1 1 1,在Diana连接了 1 1 1,那为了满足 ( i , j ) (i,j) (i,j)的性质, 7 7 7在Mocha连接了1,在Diana没连接1,所以每次加入边 ( i , j ) (i,j) (i,j)之后 i , j i,j i,j所在的森林全部都连接到了并查集 1 1 1上,所以我们就把这个连通块(森林)标记,以后也没有枚举的必要了,因为连通块里面都不满足 ( i , j ) (i,j) (i,j)的性质,只能找新的 ( i , j ) (i,j) (i,j)
#include<cstdio>
#include<algorithm>
#include<cstring>
#define maxn 100100
using namespace std;
int n,m1,m2,k,mark1[maxn],mark2[maxn];
struct node{
	int fa[maxn];
	int getfa(int x){return fa[x]==x?x:fa[x]=getfa(fa[x]);}
	void merge(int u,int v){
		int temp1=getfa(u),temp2=getfa(v);
		if(temp1>temp2) swap(temp1,temp2);
		fa[temp2]=temp1; 
	}
}a,b;
struct Node{
	int x,y;
}ans[maxn];
int main(){
	scanf("%d %d %d",&n,&m1,&m2);
	for(int i=1;i<=n;i++) a.fa[i]=b.fa[i]=i;
	for(int i=1,u,v;i<=m1;i++) scanf("%d %d",&u,&v),a.merge(u,v);
	for(int i=1,u,v;i<=m2;i++) scanf("%d %d",&u,&v),b.merge(u,v);
	for(int i=1;i<=n;i++){
		if(a.getfa(i)==1 || b.getfa(i)==1) continue;
		a.merge(1,i); b.merge(1,i);
		ans[++k].x=1; ans[k].y=i;
	}
	for(int i=1,j=1;i<=n;i++){
		if(a.getfa(i)==1 || mark1[a.getfa(i)]) continue;
		while(j<=n && (b.getfa(j)==1 || mark2[b.getfa(j)])) j++;
		if(j>n) break;
		mark1[a.getfa(i)]=mark2[b.getfa(j)]=1;
		ans[++k].x=i; ans[k].y=j;
	}
	printf("%d\n",k);
	for(int i=1;i<=k;i++) printf("%d %d\n",ans[i].x,ans[i].y);
	return 0;
}

例题2:Cow and Snacks

题意:每个牛都有两种喜欢的食物,它必须吃到一种,不然就不高兴,问一共有几个牛牛不能被满足(安排好顺序)

  • 牛牛的顺序是肯定会影响答案的,例如 1   2 , 2   3 , 3   4 1\ 2,2\ 3,3\ 4 1 2,2 3,3 4 1   2 , 3   4 , 2   3 1\ 2,3 \ 4,2 \ 3 1 2,3 4,2 3这样安排顺序答案是不一样的
  • 贪心的来说,让一个牛牛只吃一种就是最好的,我们先来建边,看看会是什么样子(样例)

图一.png

  • 我们要是把顺序问题给解决就好办了

  • 可以发现,只要我们能够连接一条边,两条边都属于一个不同的并查集,那么我们就可以通过一些操作使这个牛牛被满足

    • 左右是一个并查集,那无论如何这个牛牛也不得行了
    • 左右不是一个并查集,左右为空,牛牛吃两个,左右不为空,左边牛牛两个,中间和右边牛牛吃一个,就行了,其他情况都同理.
#include<cstdio>
#include<algorithm>
#include<cstring>
#define maxn 100010
using namespace std;
int n,k,ans,fa[maxn];
int find(int x){
	while(x!=fa[x]) x=fa[x]=fa[fa[x]];
	return x;
}
int main(){
	scanf("%d %d",&n,&k);
	for(int i=1;i<=n;i++) fa[i]=i;
	for(int i=1,x,y;i<=k;i++) {
		scanf("%d %d",&x,&y);
		int fx=find(x);
		int fy=find(y);
		if(fx!=fy) fa[fx]=fy;
		else ans++;
	}
	printf("%d\n",ans);
	return 0;
}

例题3:Unstable String Sort
  • 首先,我们要知道 a i a_i ai b i b_i bi是可以整成一个环的,也就是说 a i → j a_{i\to j} aij b i → j b_{i\to j} bij两个合起来看是矛盾的,这个时候环内就是一个字母就行了.
  • 那问题就变成了怎么缩点了,缩点我们就用并查集来缩就行了,用两个指针,没有矛盾的时候就是 f a [ a [ h 1 ] ] = f a [ b [ h 2 ] ] fa[a[h1]]=fa[b[h2]] fa[a[h1]]=fa[b[h2]]的时候,但是我们要在 h 1 = h 2 h1=h2 h1=h2的时候标记,而且要标记 h 1 + 1 h1+1 h1+1这个位置,表示 h 1 h1 h1之后的位置是一个新的点了,这个时候就可以选择字母变一下,或者不变也行
#include<cstdio>
#include<algorithm>
#include<cstring>
#define maxn 500000
using namespace std;
int n,k,a[maxn],b[maxn],fa[maxn],pos[maxn],cnt;
char ans[maxn];
int find(int x) {return fa[x]==x?x:fa[x]=find(fa[x]);}
void merge(int x,int y){fa[find(x)]=find(y);}
signed main(){
	scanf("%d %d",&n,&k);
	for(int i=1;i<=n;i++) scanf("%d",&a[i]);
	for(int i=1;i<=n;i++) scanf("%d",&b[i]);
	for(int i=1;i<=n;i++) fa[i]=i;
	for(int h1=1,h2=1;h1<=n && h2<=n;h2++){
		while(find(a[h1])!=find(b[h2]) && h1<=n) merge(a[h1],a[h1+1]),h1++;
		if(h1==h2) cnt++,pos[h1+1]=1;
	}
	if(cnt<k) printf("NO\n"),exit(0);
	printf("YES\n");
	for(int i=1,now=0;i<=n;i++) now=min(now+pos[i],k-1),ans[a[i]]='a'+now;
	for(int i=1;i<=n;i++) printf("%c",ans[i]);
	return 0;
}

例题4:扩散(无链接)

题目描述
一个点每过一个单位时间就会向 4 4 4个方向扩散一个距离,两个点 a , b a,b a,b连通,记作 e ( a , b ) e(a,b) e(a,b),当且仅当 a , b a,b a,b的扩散区域有公共部分。连通块的定义是块内的任意两个点 u , v u,v u,v都必定存在路径 e ( u , a 0 ) , e ( a 0 , a 1 ) , … e ( a k , v ) e(u,a0),e(a0,a1),…e(ak,v) e(u,a0),e(a0,a1),e(ak,v).给定平面上的 n n n个点,问最早什么时候它们形成一个连通块。

输入
第一行一个数 n n n ,以下 n n n 行,每行一个点坐标。

输出
输出仅一个数,表示最早的时刻所有点形成连通块。

样例输入
2
0 0
5 5
样例输出
5

对于 100 % 100\% 100% 的数据,满足 1 ≤ n ≤ 50 , 1 ≤ X i , Y i ≤ 1 0 9 1≤n≤50,1≤Xi,Yi≤10^9 1n50,1Xi,Yi109.

这道题就是二分一个时刻,然后再 c h e c k check check一下是不是所有的点都连到了一起,举出这个题只是想说明并查集能够优化做题的代码难度和时间复杂度,以及引出下一题.

#include<cstdio>
#include<algorithm>
#include<cstring>
#define maxn 1010
using namespace std;
int n,fa[maxn];
struct posnode{
	int x,y;
}pos[maxn];
int find(int x){
	while(x!=fa[x]) x=fa[x]=fa[fa[x]];
	return x;
}
bool dis(int x,int y,int val) {return (abs(pos[x].x-pos[y].x)+abs(pos[x].y-pos[y].y))<=val;}
bool check(int x){
	int cnt=0;
	for(int i=1;i<=n;i++) fa[i]=i;
	for(int i=1;i<=n;i++) for(int j=i+1;j<=n;j++) if(dis(i,j,x*2)) fa[find(j)]=find(i);
	for(int i=1;i<=n;i++) if(fa[i]==i) cnt++;
	return cnt==1;
}
int main(){
	scanf("%d",&n);
	for(int i=1;i<=n;i++) scanf("%d %d",&pos[i].x,&pos[i].y);
	int ans=0;
	int l=0,r=2e8;
	while(l<=r){
		int mid=l+r>>1;
		if(check(mid)) ans=mid,r=mid-1;
		else l=mid+1;
	}
	printf("%d\n",ans);
	return 0;
}

以下两个题都不是用并查集做的,但是也是很有价值,很类似的题
例题5:喷水装置(无链接)

题目描述
L L L 米,宽 W W W米的草坪里装有 n n n个浇灌喷头。每个喷头都装在草坪中心线上(离两边各 W / 2 W/2 W/2 米)。我们知道每个喷头的位置(离草坪中心线左端的距离),以及它能覆盖到的浇灌范围。
请问:如果要同时浇灌整块草坪,最少需要打开多少个喷头?

输入
第一行一个整数 T T T表示数据组数;
每组数据的第一行是整数 n , L n,L n,L W W W
接下来的 n n n 行,每行包含两个整数,给出一个喷头的位置和浇灌半径(上面的示意图是样例输入第一组数据所描述的情况)。

输出
对每组测试数据输出一个数字,表示要浇灌整块草坪所需喷头数目的最小值。如果所有喷头都打开也不能浇灌整块草坪,则输出 -1 。

样例输入
3
8 20 2
5 3
4 1
1 2
7 2
10 2
13 3
16 2
19 4
3 10 1
3 5
9 3
6 1
3 10 1
5 3
1 1
9 1

样例输出
6
2
-1

数据范围对于 100 % 100\% 100%的数据, n < = 15000 n<=15000 n<=15000

这个题就是一个线段覆盖,和上道题很像,但主要的问题是这是一个二维的东西,怎么变成一维的东西,其实我们可以只看矩形的最上面那条边(或者下面),只要这条边覆盖了,那么整个矩形必定覆盖了.然后就又转换成了线段覆盖问题.

#include<cstdio>
#include<algorithm>
#include<cstring>
#include<cmath>
#define maxn 15010
using namespace std;
int T,n,l,w,cnt,ans;
struct node{
	double l,r;
}a,water[maxn];
inline bool cmp(node a,node b) {return a.l<b.l;}
int main(){
	scanf("%d",&T);
	while(T--){
		cnt=ans=0;
		scanf("%d %d %d",&n,&l,&w);
		for(int i=1,temp,r;i<=n;i++){
			scanf("%d %d",&temp,&r);
			if(r*2<=w) continue;
			a.l=temp-sqrt(r*r-w*w/4.0);//只要把交于长的点覆盖 
			a.r=temp+sqrt(r*r-w*w/4.0);//那么一定中间也能覆盖 
			water[++cnt]=a;
		}
		sort(water+1,water+1+cnt,cmp);
		double end=0,right=0;
		for(int i=1;i<=cnt && end<l;i++){
			if(water[i].l>end) end=right,ans++;
			if(water[i].l<=end) right=max(right,water[i].r);
		}
		if(end<l) end=right,ans++;
		if(end>=l) printf("%d\n",ans);
		else printf("-1\n");
	}
	return 0;
}

例题6:Circles

赛后总结:我以为我丢精了,结果不完全是,没开 l o n g   l o n g long\ long long long,所以爆了居然,所以在 d i s = ( x [ i ] − x [ j ] ) ∗ ( x [ i ] − x [ j ] ) + ( y [ i ] − y [ j ] ) ∗ ( y [ i ] − y [ j ] ) dis=(x[i]-x[j])*(x[i]-x[j])+(y[i]-y[j])*(y[i]-y[j]) dis=(x[i]x[j])(x[i]x[j])+(y[i]y[j])(y[i]y[j])计算的时候爆了,所以在 s q r t ( d i s ) sqrt(dis) sqrt(dis)的时候 d i s dis dis还可能出现负数,再然后就是 π \pi π可以最后来乘上去,可能对精度有改善吧,而且乘 a c o s ( − 1 ) acos(-1) acos(1)更好,最后再来看题本身,首先没问题的是距离短的先考虑,但是又有问题了,两个点直接距离虽然短,但是也有不行的时候,因为有可能其中有一个点不能再扩散了,所以我们现在不看最短距离,我看当前的扩散速度,把他们放进优先队列里面去弄就行了.

总结:这个题和上面的两个覆盖题非常相似,但是这个题使用的方法截然不同,值得放在一起总结,关于类似并查集和覆盖的问题,在网络流和图论里面也有所涉及,先挖个坑,后面补.

#include<cstdio>
#include<algorithm>
#include<cstring>
#include<queue>
#include<cmath>
#define int long long
using namespace std;
int n,x[5000],y[5000];
double ans,r[5000];
struct node{
	int i,j;
	double dis;
	friend bool operator < (node a,node b) {return a.dis>b.dis;}
}temp;
priority_queue<node> q; 
signed main(){
	scanf("%lld",&n);
	for(int i=1;i<=n;i++) scanf("%lld %lld",&x[i],&y[i]);
	for(int i=1;i<=n;i++){
		for(int j=i+1;j<=n;j++){
			temp.dis=hypot((x[i]-x[j]),(y[i]-y[j]))/2.0; temp.i=i; temp.j=j;
			q.push(temp);
		}
	}
	while(!q.empty()){
		temp=q.top(); q.pop();
		if(r[temp.i] && r[temp.j]) continue;
		else if(!r[temp.i] && !r[temp.j]){
			r[temp.i]=r[temp.j]=temp.dis;
			ans+=r[temp.i]*r[temp.i]*2;
		}else{
			double dis=(x[temp.i]-x[temp.j])*(x[temp.i]-x[temp.j])+(y[temp.i]-y[temp.j])*(y[temp.i]-y[temp.j]);
			dis=sqrt(dis);
			if(r[temp.i]){
				if(abs(r[temp.i]+temp.dis-dis)<=0.0001){
					r[temp.j]=temp.dis;
					ans+=temp.dis*temp.dis;
				}else{
					temp.dis=dis-r[temp.i];
					q.push(temp);
				}
			}
			else if(r[temp.j]){
				if(abs(r[temp.j]+temp.dis-dis)<=0.0001){
					r[temp.i]=temp.dis;
					ans+=temp.dis*temp.dis;
				}else{
					temp.dis=dis-r[temp.j];
					q.push(temp);
				}
			}
		}
	}
	printf("%.9lf\n",acos(-1)*ans);
	return 0;
}

例题7:货物收集

根据题意,可以二分一个健身的时间,然后 O n On On的遍历树,寻找是否可行,这样是 n l o g nlog nlog的时间复杂度,跑得快就能过.还有一种方法,还是得先排序,也是个 n l o g nlog nlog,然后依次把边加进去,直到 1 1 1的连通块达到所需货物的时候,就输出就行了

#include<cstdio>
#include<algorithm>
#include<cstring>
#define maxn 1000100
using namespace std;
int n,w,data[maxn],fa[maxn];
struct node{
    int u,v,val;
}edge[maxn];
inline bool cmp(node a,node b) {return a.val<b.val;}
int find(int x){
    while(x!=fa[x]) x=fa[x]=fa[fa[x]];
    return x;
}
int main(){
    scanf("%d %d",&n,&w);
    for(int i=2;i<=n;i++) scanf("%d",&data[i]),fa[i]=i;
    for(int i=1;i<n;i++) scanf("%d %d %d",&edge[i].u,&edge[i].v,&edge[i].val);
    sort(edge+1,edge+n,cmp);
    for(int i=1;i<n;i++){
        int fx=find(edge[i].u);
        int fy=find(edge[i].v);
        if(fx==fy) continue;
        if(fx>fy) swap(fx,fy);
        fa[fy]=fx; data[fx]+=data[fy];
        if(data[find(1)]>=w) printf("%d\n",edge[i].val),exit(0);
    }
    return 0;
}

例题8:逐个击破

这个题目和上一个是差不多的,我们可以先假设所有的边已经连接上去了的,然后我们就来删除边,当然是删除的越多就越好,所以能删的就删,我们最开始的时候让不同的点在不同的并查集里面,当删除边之后他们两个分开之后,就弄到一个并查集里就行了.

#include<cstdio>
#include<algorithm>
#include<cstring>
#define maxn 101000
using namespace std;
int n,m,fa[maxn];
long long sum;
bool book[maxn];
struct node{
	int u,v,w;
}edge[maxn];
int find(int x){
	while(x!=fa[x]) x=fa[x]=fa[fa[x]];
	return x;
}
bool cmp(node a,node b) {return a.w>b.w;}
int main(){
	scanf("%d %d",&n,&m);
	for(int i=1;i<=n;i++) fa[i]=i;
	for(int i=1,x;i<=m;i++) scanf("%d",&x),book[x]=true;
	for(int i=1;i<n;i++) scanf("%d %d %d",&edge[i].u,&edge[i].v,&edge[i].w),sum+=edge[i].w;
	sort(edge+1,edge+n,cmp);
	for(int i=1;i<n;i++){
		int fx=find(edge[i].u);
		int fy=find(edge[i].v);
		if(fx==fy || (book[fx] && book[fy])) continue;
		sum-=edge[i].w;
		fa[fx]=fy;
		if(book[fx]||book[fy]) book[fy]=true;
	}
	printf("%lld\n",sum);
	return 0;
}

例题9:信息传递

这个有一点不一样,但是其实就是找一个最小的环,可以用 d f s dfs dfs,但是也可以用带权并查集,也就是在其他的基础上加一个深度.

#include<cstdio>
#include<algorithm>
#include<cstring>
#define maxn 200010
using namespace std;
int fa[maxn],deep[maxn],ans=maxn,last,n;
int find(int x) {
	if(fa[x]!=x){
		int father=fa[x];
		fa[x]=find(fa[x]);
		deep[x]+=deep[father];
	}
	return fa[x];
}
void check(int x,int y){
	int fx=find(x),fy=find(y);
	if(fx==fy) ans=min(ans,deep[x]+deep[y]+1);
	else deep[x]=deep[y]+1,fa[fx]=fy;
}
signed main(){
	scanf("%d",&n);
	for(int i=1;i<=n;i++) fa[i]=i;
	for(int i=1,temp;i<=n;i++) scanf("%d",&temp),check(i,temp);
	printf("%d\n",ans);
	return 0;
}

例题10:食物链

我们可以将并查集扩大, 1 → n 1 \to n 1n表示群系 A A A, n + 1 → 2 n n+1 \to 2n n+12n表示群系 B B B, 2 n + 1 → 3 n 2n+1\to 3n 2n+13n表示群系 C C C, x x x y y y,我们就连边 x → y + n x \to y+n xy+n, x + n → y + 2 ∗ n x+n\to y+2*n x+ny+2n, x + 2 ∗ n → y x+2*n\to y x+2ny

  • 对于一个群系,他的食物是什么就看这个群系和下一个群系有没有连边
  • 对于一个群系,他的天敌是什么就看上一个群系和这个群系有没有连边.
  • 对于一个群系,他和谁是同类就看 x y xy xy是否在一个并查集内,或者他是不是被另外一个吃,或者是不是吃另外一个

加上一个图可以自己画画理解理解.

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-gACsqF9x-1630334970241)(https://i.loli.net/2021/08/24/S4BLn9lJbe3ag2Z.png)]

#include<cstdio>
#include<algorithm>
#include<cstring>
#define maxn 200010
using namespace std;
int n,m,fa[maxn*3],opt,x,y,ans;
int find(int x) {return fa[x]==x?fa[x]:fa[x]=find(fa[x]);}
void merge(int x,int y) {fa[find(x)]=find(y);}
signed main(){
	scanf("%d %d",&n,&m);
	for(int i=1;i<=3*n;i++) fa[i]=i;
	for(int i=1;i<=m;i++){
		scanf("%d %d %d",&opt,&x,&y);
		if(x>n||y>n) ans++;
		else if(opt==1){
			if(find(x)==find(y+n) || find(x)==find(y+2*n)) ans++;
			else merge(x,y),merge(x+n,y+n),merge(x+2*n,y+2*n);
		}else {
			if(find(x)==find(y) || find(x)==find(y+2*n)) ans++;
			else merge(x,y+n),merge(x+n,y+2*n),merge(x+2*n,y);
		}
	}
	printf("%d\n",ans);
	return 0;
}

例题11:关押罪犯
  • 这个题做法和上一题类似,也是种类并查集,将 f a fa fa复制一次,再将每一组 a i , b i , c i a_i,b_i,c_i ai,bi,ci按照 c i c_i ci从大到小排序,因为只有两个牢,所以要么丢去这个要么丢去那个,所以到了第一个不能调开的地方,答案就是这一个 c i c_i ci,也就是 1 & 2 1\&2 1&2, 2 & 3 2\&3 2&3, 3 & 1 3\&1 3&1,这种情况就只能 1 , 3 1,3 1,3一组, 2 2 2单独一组
  • x x x的敌对域是 [ 1 , n ] [1,n] [1,n] ,友好域是 [ n + 1 , 2 n ] [n+1,2n] [n+1,2n],所以后面的就好理解了.
#include<cstdio>
#include<algorithm>
#include<cstring>
#define maxn 100100
using namespace std;
int n,m,fa[maxn];
struct node{
	int x,y,val;
}data[maxn];
bool cmp(node a,node b) {return a.val>b.val;}
int find(int x) {return x==fa[x]?x:fa[x]=find(fa[x]);}
void merge(int x,int y) {fa[find(x)]=find(y);}
signed main(){
	scanf("%d %d",&n,&m);
	for(int i=1;i<=2*n;i++) fa[i]=i;
	for(int i=1;i<=m;i++) scanf("%d %d %d",&data[i].x,&data[i].y,&data[i].val);
	sort(data+1,data+1+m,cmp);
	for(int i=1;i<=m;i++){
		if(find(data[i].x)==find(data[i].y)) printf("%d\n",data[i].val),exit(0);
		merge(data[i].x,data[i].y+n); merge(data[i].x+n,data[i].y);
	}
	printf("0\n");
	return 0;
}

例题12:银河英雄传说

这个题和例题 9 9 9很像,都是带权并查集.

#include<cstdio>
#include<algorithm>
#include<cstring>
#define maxn 30010
using namespace std;
int n,fa[maxn],size[maxn],deep[maxn]; char opt;
int find(int x){
	if(x==fa[x]) return x;
	int temp=find(fa[x]);
	deep[x]+=deep[fa[x]];
	return fa[x]=temp;
}
void merge(int x,int y){
	int tempx=find(x),tempy=find(y);
	deep[tempy]=size[tempx];
	size[tempx]+=size[tempy];
	fa[tempy]=tempx;
}
signed main(){
	scanf("%d",&n);
	for(int i=1;i<=30000;i++) fa[i]=i,size[i]=1;
	for(int i=1,x,y;i<=n;i++){
		scanf("\n%c %d %d",&opt,&x,&y);
		if(opt=='M') merge(x,y);
		else printf("%d\n",find(x)==find(y)?abs(deep[x]-deep[y])-1:-1);
	}
	return 0;
}

例题13:DZY Loves Planting

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Tur48Tks-1630334970242)(https://i.loli.net/2021/08/26/SB6E9qlDi4JyO51.png)]

搬运机房大佬题解

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-4Mbc7pCT-1630334970243)(https://i.loli.net/2021/08/26/WHTZMpSxhJE5wCn.png)]

#include<cstdio>
#include<algorithm>
#include<cstring>
#define maxn 200000
using namespace std;
int n,data[maxn],sum,fa[maxn],size[maxn];
struct node{
	int u,to,w;
}edge[maxn];
bool cmp(node a,node b) {return a.w<b.w;}
int find(int x) {return x==fa[x]?x:fa[x]=find(fa[x]);}
signed main(){
	scanf("%d",&n);
	for(int i=1;i<n;i++) scanf("%d %d %d",&edge[i].u,&edge[i].to,&edge[i].w);
	for(int i=1;i<=n;i++) scanf("%d",&data[i]),sum+=data[i],fa[i]=i,size[i]=1;
	sort(edge+1,edge+n,cmp);
	for(int i=1;i<n;i++){
		int tempx=find(edge[i].u),tempy=find(edge[i].to);
		size[tempx]+=size[tempy]; fa[tempy]=tempx;  data[tempx]+=data[tempy];
		if(size[tempx]>sum-data[tempx]) printf("%d\n",edge[i].w),exit(0);
	}
	printf("%d\n",edge[n-1].w);
	return 0;
}

例题14:毒瘤题 [无链接]

题目描述
Tom热爱出毒瘤题,但是这次他似乎除了一个并不毒瘤的题。给定一张N个点M条无向边的图,每条边有边权w。定义一个联通块的大小为该连通块内所有边权的和。然后进行K组询问。每次询问在由所有边权不超过Xi的边构成的新的生成子图中联通块的大小的最大值。

输入
第一行三个数N,M,K,表示图有N个点,M条边,有K组询问;
后面m行每行三个正整数u,v,w,表示点u与点v之间有一条边权为w的边。
后面k行每行一个整数x,表示在这次询问中,只能使用边权不超过x的边,数据可能存在重边,但不存在自环。

输出
K行,对于每组询问输出一次答案

样例输入
5 5 3
1 2 5
2 3 6
3 4 4
3 5 2
4 5 3
7 5 3

样例输出
20
9
5
对于100%的数据,N,M,K<=100000,w<=1000000,x<=int。

一眼就能看出是并查集,我们把询问离线,把询问排序,然后依次加入边.

#pragma GCC optimize("Ofast")
#pragma GCC optimize(3)
#include<cstdio>
#include<algorithm>
#include<cstring>
#define maxn 1000000
using namespace std;
int n,m,q,fa[maxn];
long long ans[maxn],val[maxn],Max;
int read(){
    register int x=0,f=1; register char ch=getchar();
    while(ch<'0' || ch>'9') {if(ch=='-') f=-1; ch=getchar();}
    while(ch>='0' && ch<='9') x=x*10+ch-'0',ch=getchar();
    return x*f;
}
struct node{
    int u,v,w;
}edge[maxn];
struct ASK{
    int val,id;
}ask[maxn];
bool cmp1(ASK a,ASK b) {return a.val<b.val;}
bool cmp2(node a,node b) {return a.w<b.w;}
int find(int x){
    while(x!=fa[x]) x=fa[x]=fa[fa[x]];
    return x;
}
int main(){
    n=read(); m=read(); q=read();
    for(int i=1;i<=n;i++) fa[i]=i;
    for(int i=1;i<=m;i++) edge[i].u=read(),edge[i].v=read(),edge[i].w=read();
    for(int i=1;i<=q;i++) ask[i].val=read(),ask[i].id=i;
    sort(ask+1,ask+1+q,cmp1);
    sort(edge+1,edge+1+m,cmp2);
    for(int pos=1,now=1;pos<=q;pos++){
        for(int i=now;i<=m;i++){
            if(edge[i].w>ask[pos].val) break;
            int fu=find(edge[i].u);
            int fv=find(edge[i].v);
            if(fu==fv){
                val[fu]+=(long long) edge[i].w;
                Max=max(Max,val[fu]);
                now++;
                continue;
            }
            val[fu]=val[fu]+val[fv]+(long long)edge[i].w;
            fa[fv]=fu;
            Max=max(Max,val[fu]);
            now++;
        }
        ans[ask[pos].id]=Max;
    }
    for(int i=1;i<=q;i++) printf("%lld\n",ans[i]);
    return 0;
}

例题15:Moo Tube 双倍经验

这道题应该能算上三倍经验了,和上道题差不多,也是离线加边,看到这个问题我想起了另外一道题,将删边操作离线倒着操作就是加边了,并查集就能用了.

#include<cstdio>
#include<algorithm>
#include<cstring>
#define maxn 200010
using namespace std;
int n,m,size[maxn],k=1,fa[maxn],ans[maxn];
struct node{
	int u,v,w;
}edge[maxn];
struct asknode{
	int k,v,id;
}ask[maxn];
inline bool cmp1(node a,node b) {return a.w>b.w;}
inline bool cmp2(asknode a,asknode b) {return a.k>b.k;}
int find(int x){
	while(x!=fa[x]) x=fa[x]=fa[fa[x]];
	return x;
}
void merge(int x,int y){
	int fx=find(x);
	int fy=find(y);
	if(fx==fy) return ;
	size[fy]+=size[fx]; fa[fx]=fy;
}
int main(){
	scanf("%d %d",&n,&m);
	for(int i=1;i<=n;i++) fa[i]=i,size[i]=1;
	for(int i=1;i<n;i++) scanf("%d %d %d",&edge[i].u,&edge[i].v,&edge[i].w);
	for(int i=1;i<=m;i++) scanf("%d %d",&ask[i].k,&ask[i].v),ask[i].id=i;
	sort(edge+1,edge+1+n,cmp1); sort(ask+1,ask+1+m,cmp2);
	for(int i=1;i<=m;i++){
		while(edge[k].w>=ask[i].k) merge(edge[k].u,edge[k].v),k++;
		ans[ask[i].id]=size[find(ask[i].v)];
	}
	for(int i=1;i<=m;i++) printf("%d\n",ans[i]-1);
	return 0;
}

例题16:刺客信条 [无链接]

刺客信条

【问题描述】

故事发生在1486 年的意大利,Ezio原本只是一个文艺复兴时期的贵族,后来因为家族成员受到圣殿骑士的杀害,决心成为一名刺客。最终,凭借着他的努力和出众的天赋,成为了杰出的刺客大师。刺客组织在他的带领下,为被剥削的平民声张正义,赶跑了原本统治意大利的圣殿骑士首领-教皇亚历山大六世。在他的一生中,经历了无数次惊心动魄、扣人心弦的探险和刺杀。

这次的故事就是他暗杀一位作恶多端的红衣主教。红衣主教可以吸取他周围人的生命力量(以他为圆心的一个圆),而他的红衣教徒也拥有这个力量。红衣主教的家是一个 x ∗ y x*y xy的长方形房间,也就是说,他的家的四个角坐标分别为 ( 0 , 0 ) , ( x , 0 ) , ( 0 , y ) , ( x , y ) (0,0),(x,0),(0,y),(x,y) (0,0),(x,0),(0,y),(x,y)。教堂的门在 ( 0 , 0 ) (0,0) (0,0),而红衣主教就在 ( x , y ) (x,y) (x,y)的卧室休息。他的家中还有 n n n个守护着他的红衣教徒,站在 ( a i , b i ) (ai,bi) (ai,bi)。Ezi想要趁主教休息时,从门进入潜入到他的卧室刺杀他,因为主教休息时会脱下红衣,这样吸取生命的力量就消失了。可是守卫他的红衣教徒依然很危险,离红衣教徒太近就会被吸取生命。因此,Ezi想知道,当红衣教徒的影响范围最大为多少时,Ezi可以不被吸取生命并且成功的刺杀掉红衣主教(当两个红衣教徒的影响范围相切时,可以视为Ezio恰好可以穿过)。注意:教徒都在房间里。

【输入格式】

第一行三个整数 x , y , n x,y,n x,y,n。之后 n n n行,每行两个整数 ,意义见题目描述。

【输出描述】

一行一个数 D D D,表示红衣教徒的最大影响范围,保留两位小数。

【样例输入】

10 20 2

3 3

6 14

【样例输出】

3.00

【样例解释】

贴着墙走

【数据范围】

对于 100 % 100\% 100%的数据, n < = 2000 n<=2000 n<=2000, 0 < = x 0<=x 0<=x, y < = 1 0 6 y<=10^6 y<=106

这个题和例题4非常的类似,也是二分,找扩散的范围,可以 n 2 n^2 n2的枚举两个点,也可以先排一个序,到了“一定程度”就可以 b r e a k break break了,详见代码.然后这里最关键的操作就是把长方体的四条边弄成四个点,这样可以方便的判断是否可以走过去,当上面与下面成为一个连通块(或者上右,左下,左右)就不能走过去了.

#include<cstdio>
#include<algorithm>
#include<cstring>
#define maxn 2005
#define eps 1e-5
#define int long long
using namespace std;
int n,m,k,fa[maxn];
struct posnode{
	int x,y;
}a[maxn];
bool cmp(posnode a,posnode b) {return a.x==b.x?a.y<b.y:a.x<b.x;}
int find(int x){
	while(x!=fa[x]) x=fa[x]=fa[fa[x]];
	return x;
}
void merge(int x,int y){fa[find(x)]=find(y);}
bool check(double x){
	for(int i=1;i<=k+4;i++) fa[i]=i;
	merge(k+1,k+2); merge(k+3,k+4);//1->4:右上左下 
	for(int i=1;i<=k;i++){
		if(a[i].y<x) merge(i,k+1);
		if(m-a[i].y<x) merge(i,k+3);
		if(a[i].x<x) merge(i,k+4);
		if(n-a[i].x<x) merge(i,k+2);
	}
	double basedis=x*x*4.0;//二分出来的距离 
	for(int i=1;i<=k;i++){
		for(int j=i+1;j<=k;j++){
			int num=(a[j].x-a[i].x)*(a[j].x-a[i].x);
			if(num>=basedis) break;
			num+=(a[j].y-a[i].y)*(a[j].y-a[i].y);
			if(num<basedis) merge(i,j);
			if(find(k+2)==find(k+3)) return false;
		}
	}
	return true;
}
signed main(){
	scanf("%lld %lld %lld",&n,&m,&k);
	for(int i=1;i<=k;i++) scanf("%lld %lld",&a[i].x,&a[i].y);
	sort(a+1,a+1+k,cmp);//这样排就是从左下角排到右上角 
	double l=0,r=max(n,m);
	while(r-l>=eps){
		double mid=(l+r)/2.0;
		if(check(mid)) l=mid;
		else r=mid;
	}
	printf("%.2lf\n",l);
	return 0;
}

例题17:星球大战

这道题就是我前面说的删操作离线并倒过来做,然后就变成了加操作,对于没有被摧毁过的点,我们先把他们 m e r g e merge merge,然后倒着来枚举,他攻下一个星球,也就变成了我们恢复一个星球, c n t cnt cnt就是当前连通块的个数. m e r g e merge merge的时候来更新.

#include<cstdio>
#include<algorithm>
#include<cstring>
#define maxn 1000000
using namespace std;
int n,m,k,tot,head[maxn],a[maxn],fa[maxn],size[maxn],ans[maxn],cnt; bool book[maxn];
struct node{
	int to,next;
}edge[maxn];
void add(int u,int v){
	edge[++tot]=(node) {v,head[u]}; head[u]=tot;
	edge[++tot]=(node) {u,head[v]}; head[v]=tot;
}
int find(int x) {return fa[x]==x?x:fa[x]=find(fa[x]);}
void merge(int x,int y){
	if(find(x)==find(y)) return ;
	fa[find(y)]=find(x); cnt--;
}
signed main(){
	scanf("%d %d",&n,&m);
	for(int i=1;i<=n;i++) fa[i]=i;
	for(int i=1,u,v;i<=m;i++) scanf("%d %d",&u,&v),u++,v++,add(u,v);
	scanf("%d",&k);
	for(int i=1;i<=k;i++) scanf("%d",&a[i]),a[i]++,book[a[i]]=true;
	cnt=n-k;
	for(int u=1;u<=n;u++){
		if(book[u]) continue;
		for(int i=head[u];i;i=edge[i].next) 
			if(!book[edge[i].to]) merge(edge[i].to,u);
	} 
	ans[k+1]=cnt;
	for(int u=k;u;u--){
		cnt++; book[a[u]]=false;
		for(int i=head[a[u]];i;i=edge[i].next) 
			if(!book[edge[i].to]) merge(edge[i].to,a[u]);
		ans[u]=cnt;
	}
	for(int i=1;i<=k+1;i++) printf("%d\n",ans[i]);
	return 0;
}

例题18:奶酪

和那个刺客信条也有点像(已经不知道是几倍经验了),正常的弄,如果两个点之间距离够就 m e r g e merge merge,然后如果有可以到奶酪上面的就记录到 f 1 f1 f1中,如果可以从下面穿出去就记录在 f 2 f2 f2中,最后来看有没有一组点 ( f 1 , f 2 ) (f1,f2) (f1,f2),他们是同一个并查集里面的,说明可以从下面走到上面.

#include<cstdio>
#include<algorithm>
#include<cstring>
#define maxn 1010
#define int long long
using namespace std;
int T,n,h,r,x[maxn],y[maxn],z[maxn],fa[maxn],f1[maxn],f2[maxn];
int dis(int x,int y,int z,int xx,int yy,int zz){
	return (x-xx)*(x-xx)+(y-yy)*(y-yy)+(z-zz)*(z-zz);
}
int find(int x){
	while(x!=fa[x]) x=fa[x]=fa[fa[x]];
	return fa[x];
}
signed main(){
	scanf("%lld",&T);
	while(T--){
		int tot1=0,tot2=0;
		scanf("%lld %lld %lld",&n,&h,&r);
		for(int i=1;i<=n;i++) fa[i]=i;
		for(int i=1;i<=n;i++){
			scanf("%lld %lld %lld",&x[i],&y[i],&z[i]);
			if(z[i]+r>=h) f1[++tot1]=i;
			if(z[i]-r<=0) f2[++tot2]=i;
			for(int j=1;j<=i;j++){
				if(dis(x[i],y[i],z[i],x[j],y[j],z[j])>4*r*r) continue;
				fa[find(j)]=find(i);
			}
		}
		bool judge=false;
		for(int i=1;i<=tot1&&(!judge);i++)
			for(int j=1;j<=tot2;j++)
				if(find(f1[i])==find(f2[j])) {judge=true; break;}
		printf("%s\n",judge?"Yes":"No");
	}
	return 0;
}

因为是学习笔记,有什么问题还请指正,如果有哪里写漏了也欢迎指出.
(我所说的无链接的题网上也能找到,不过可能是一些需要付费的网站)
点赞+关注😍

  • 4
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

d3ac

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值