(题源未知)连通分量最值 题解
(题源未知)连通分量最值 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 :
-
最小值很好求,我们用所有的边去连不同的连通分量,故答案为 \(max \{ 1,m-k \}\) 。
-
再求最大值时,我们肯定要先把每个连通分量中没连起来的边连上,如果不够连,那么直接输出原本的连通分量个数,否则我们尽量将包含点个数多的连通分量连起来,因为这样在连通分量数不进一步减少的情况下,能够增加的边数更多。
在这题里还有一条公式:\(总边数=(点数)*(点数-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;
}

浙公网安备 33010602011771号