(题源未知)连通分量最值 题解

(题源未知)连通分量最值 By Plus_Cat


题目内容

题目描述

给出\(n\)个初始孤立点,给出\(q\)个下列操作之一:

1 a b:增加边 (\(a\), \(b\))。

2 k:询问:如果加入\(k\)条边,能得到的连通分量个数的最小值和最大值。

输入格式

第一行输入两个数\(n\)\(q\)

接下来\(q\)行,每行一个操作:

1 a b:增加边 (\(a\), \(b\))。

2 k:询问:如果加入\(k\)条边,能得到的连通分量个数的最小值和最大值。

输出格式

输出多行,每行一个询问的结果。

样例

输入

10 10
2 7
1 7 8
1 7 3
1 1 10
2 4
1 3 4
2 11
1 10 9
1 1 5
1 1 7

输出

3 6
3 6
1 5

数据范围和提示

注意:两点之间只能连一条边

40%的数据,\(n,q\)\(1*10^2\)

70%的数据,\(n,q\)\(1*10^3\)

100%的数据,\(n,q\)\(1*10^5\)


分析

首先,操作中影响到之后的操作只有操作 1 ,我们可以用并查集维护。

对于操作 2

  1. 最小值很好求,我们用所有的边去连不同的连通分量,故答案为 \(max \{ 1,m-k \}\)

  2. 再求最大值时,我们肯定要先把每个连通分量中没连起来的边连上,如果不够连,那么直接输出原本的连通分量个数,否则我们尽量将包含点个数多的连通分量连起来,因为这样在连通分量数不进一步减少的情况下,能够增加的边数更多。

在这题里还有一条公式:\(总边数=(点数)*(点数-1)/2\)


实现

\(40pts\)

\(40pts\)的数据太水,一般直接暴力模拟。

\(70pts\)

\(70pts\)的数据,我们使用并查集+ \(vector<int>\)\(vector<int>\) 用于维护单个连通分量大小,在更改时直接二分查找,然后暴力 \(erase\)\(insert\) ,保证升序,在查找最大值时,倒序 \(for\) 循环,找到最小值(或者使用 \(set<long long>\) ,会更快一点)。

在进入查找之前,我们先将 \(k\) 减去每个连通分量中没连起来的边数,如果 \(k\leq0\) ,直接输出现连通分量个数。

倒序 \(for\) 循环时,我们记下此时点总数,将他乘上新连通分量的点数,就得到了在连通分量数不进一步减少的情况下,能够增加的边数最大值,将 \(k\) 直接减去该值,如果 \(k\leq0\) ,直接输出现连通分量个数,否则继续循环下去。

\(100pts\)

满分做法是基于 \(70pts\) 的做法的,本题需要优化的部分只有更改与求最大值,而在70%的做法中我们使用了 \(O(n)\) 的删除与插入以及查询最大值,那我们可以尝试优化到 \(O(\log_{2}^{n})\) 。我们很容易可以发现查找最大值倒序 \(for\) 循环时,满足一个单调性,故可以二分,但直接二分的话,复杂度会直接升到 \(O(n*\log_{2}^{n})\) ,明显需要使用数据结构优化,这个时候可以联想到与二分性质十分契合的线段树,那么怎么设置这个状态呢?

还是看到 \(70pts\) 的做法,倒序 \(for\) 循环 \(vector<int>\) 的部分,我们维护了维护单个连通分量大小,那么这个就作为线段树的值域,其中维护边的个数,但是考虑到连通分量大小可能有相同,我们还要加入连通分量数量与总点数(其实可以只记连通分量数量,只不过为了方便,记了另外两个值)。


线段树设置

struct Segment_Tree{
#define ls (p<<1)
#define rs (p<<1|1)
#define mid (tr[p].l+tr[p].r>>1)
	/*变量定义*/
	int n;
	struct node{
		int l,r,len;
		ll cnt,sum,tot;//连通分量数,点数,边数(后两个可不计)
	}tr[N<<2];
	/*初始化*/
	void init(int _n){
		n=_n,build(1,1,n);
	}
	/*建树*/
	void build(int p,int l,int r){
		if(l==r)return tr[p]={l,l,1,(l==1)*n,(l==1)*n,0},void();
		tr[p]={l,r,r-l+1,0,0,0};
		build(ls,l,mid),build(rs,mid+1,r),push_up(p);
	}
	/*传递*/
	void push_up(int p){
		tr[p].cnt=tr[ls].cnt+tr[rs].cnt;
		tr[p].sum=tr[ls].sum+tr[rs].sum;
		tr[p].tot=tr[ls].tot+tr[rs].tot;
	}
	/*修改*/
	void update(int p,int x,int d){
		if(tr[p].len==1)
			return tr[p].cnt+=1ll*d,tr[p].sum+=1ll*d*x,tr[p].tot+=1ll*d*cal(x),void();
		if(x<=mid)update(ls,x,d);
		else update(rs,x,d);
		push_up(p);
	}
	/*查询*/
	int query(int p,ll needs,int sum){//sum是已经有的点数 
		if(tr[p].len==1){
			int L=1,R=tr[p].cnt,ans=tr[p].cnt;//初始值要注意设置,不然可能有情况二分不出来 
			while(L<=R){
				int Mid=L+R>>1;
				ll s=1ll*cal(Mid*tr[p].l+sum)-1ll*Mid*cal(tr[p].l);
				if(s>=needs)ans=Mid,R=Mid-1;
				else L=Mid+1;
			}return ans;
		}
		ll s=cal(tr[rs].sum+sum)-tr[rs].tot;
		if(s>=needs)return query(rs,needs,sum);
		return query(ls,needs+tr[rs].tot,sum+tr[rs].sum)+tr[rs].cnt;
	}
#undef ls
#undef rs
#undef mid
}seg;

并查集设置

struct DSU{
	int n,m,fa[N],siz[N];//n是总点数,m是连通分量个数 
	void init(int _n){
		n=m=_n;
		FOR(i,0,n)fa[i]=i,siz[i]=1;
	}
	int get(int x){return fa[x]==x?x:fa[x]=get(fa[x]);}
	void uni(int u,int v){
		int x=get(u),y=get(v);
		if(x!=y)fa[y]=x,siz[x]+=siz[y],--m;
	}
}dsu;

CODE

#include<bits/stdc++.h>
#define ll long long
#define max(a,b) ((a)<(b)?(b):(a))
#define FOR(i,a,b) for(register int i=(a);i<=(b);++i)
#define DOR(i,a,b) for(register int i=(a);i>=(b);--i)
#define main Main();signed main(){ios::sync_with_stdio(0);cin.tie(0);return Main();}signed Main
using namespace std;
const int N=1e5+10;
int n,Q;
inline ll cal(ll num){return (num*(num-1)>>1);}//公式.
ll res;
struct Segment_Tree{
#define ls (p<<1)
#define rs (p<<1|1)
#define mid (tr[p].l+tr[p].r>>1)
	/*变量定义*/
	int n;
	struct node{
		int l,r,len;
		ll cnt,sum,tot;//连通分量数,点数,边数.
	}tr[N<<2];
	/*初始化*/
	void init(int _n){
		n=_n,build(1,1,n);
	}
	/*建树*/
	void build(int p,int l,int r){
		if(l==r)return tr[p]={l,l,1,(l==1)*n,(l==1)*n,0},void();//最初每个连通分量大小都是1.
		tr[p]={l,r,r-l+1,0,0,0};
		build(ls,l,mid),build(rs,mid+1,r),push_up(p);
	}
	/*传递*/
	void push_up(int p){
		tr[p].cnt=tr[ls].cnt+tr[rs].cnt;
		tr[p].sum=tr[ls].sum+tr[rs].sum;
		tr[p].tot=tr[ls].tot+tr[rs].tot;
	}
	/*修改*/
	void update(int p,int x,int d){
		if(tr[p].len==1)
			return tr[p].cnt+=1ll*d,tr[p].sum+=1ll*d*x,tr[p].tot+=1ll*d*cal(x),void();
		if(x<=mid)update(ls,x,d);
		else update(rs,x,d);
		push_up(p);
	}
	/*查询*/
	int query(int p,ll needs,int sum){//sum是已经有的点数.
		if(tr[p].len==1){
			int L=1,R=tr[p].cnt,ans=tr[p].cnt;//初始值要注意设置,不然可能有情况二分不出来.
			while(L<=R){
				int Mid=L+R>>1;
				ll s=1ll*cal(Mid*tr[p].l+sum)-1ll*Mid*cal(tr[p].l);
				if(s>=needs)ans=Mid,R=Mid-1;
				else L=Mid+1;
			}return ans;
		}
		ll s=cal(tr[rs].sum+sum)-tr[rs].tot;
		if(s>=needs)return query(rs,needs,sum);
		return query(ls,needs+tr[rs].tot,sum+tr[rs].sum)+tr[rs].cnt;
	}
#undef ls
#undef rs
#undef mid
}seg;
struct DSU{
	int n,m,fa[N],siz[N];//n是总点数,m是连通分量个数.
	void init(int _n){
		n=m=_n;
		FOR(i,0,n)fa[i]=i,siz[i]=1;
	}
	int get(int x){return fa[x]==x?x:fa[x]=get(fa[x]);}
	void uni(int u,int v){
		int x=get(u),y=get(v);
		if(x!=y)fa[y]=x,siz[x]+=siz[y],--m;
	}
}dsu;
void init(){
	seg.init(n),dsu.init(n);
}
signed main(){
	cin>>n>>Q;
	init();
	while(Q--){
		int opt;cin>>opt;
		if(opt==1){
			int u,v;cin>>u>>v;--res;
			int x=dsu.get(u),y=dsu.get(v);
			if(x!=y){
				res-=cal(dsu.siz[x])+cal(dsu.siz[y]);
				seg.update(1,dsu.siz[x],-1),seg.update(1,dsu.siz[y],-1);
				dsu.uni(x,y);
				res+=cal(dsu.siz[x]);
				seg.update(1,dsu.siz[x],1);
			}
		}else {
			ll k;cin>>k;
			cout<<max(1,dsu.m-k)<<" "<<dsu.m-seg.query(1,k-res,0)+1<<endl;
            //query返回的是最少合并几个,所以要加1.
		}
	}
	return 0;
}
posted @ 2024-05-02 20:43  Add_Catalyst  阅读(17)  评论(0)    收藏  举报