LCT 的运用
维护连通性
对一个森林,进行连边,删边操作(保证任何时候整个图是一个森林)和查询两个点是否连通。
做法
连边/删边是基础操作。
LCT 可以查找某个点 \(u\) 所在的树的树根,具体方法是,对 \(u\) access 再 splay,此时 \(u\) 和根在同一个 Splay 内,那么这个 Splay 中序遍历下的第一个节点就是树根。
具体方法是从这个 Splay 的根出发不断跳左儿子,直到没有左儿子,将这个点返回即可。(注意在返回前要把这个点 splay 上去以保证复杂度)
对于两个点 \(u, v\),如果它们找到的原树的根相同,则它们连通。
需要注意的是题目必须保证任何时候整个图是一个森林,否则不能这么做。
例题
点击查看代码
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
#define rep(i, a, b) for(ll i=a;i<=b;i++)
#define rrep(i, a, b) for(ll i=a;i>=b;i--)
const ll N=10009;
ll n, m;
struct LCT{
#define ls(u) tr[u].l
#define rs(u) tr[u].r
#define fa(u) tr[u].fa
#define isleft(u) (ls(tr[u].fa)==u?1:0)
#define isroot(u) (ls(fa(u))!=u&&rs(fa(u))!=u?1:0)
#define son(u, ty) (ty==1?ls(u):rs(u))
struct point{
ll l, r, fa, tag;
}tr[N];
void rotate(ll u){
ll y=fa(u), z=fa(y);
if(isleft(u)){
ll p2=rs(u), ty=isleft(y);
fa(u)=z;rs(u)=y;
fa(y)=u;ls(y)=p2;
if(p2)fa(p2)=y;
if(son(z, ty)==y)son(z, ty)=u;
}else{
ll p2=ls(u), ty=isleft(y);
fa(u)=z;ls(u)=y;
fa(y)=u;rs(y)=p2;
if(p2)fa(p2)=y;
if(son(z, ty)==y)son(z, ty)=u;
}
}
void push_down(ll u){
if(tr[u].tag){
if(ls(u)){swap(ls(ls(u)), rs(ls(u)));tr[ls(u)].tag^=1;}
if(rs(u)){swap(ls(rs(u)), rs(rs(u)));tr[rs(u)].tag^=1;}
tr[u].tag=0;
}
}
void update(ll u){
if(fa(u))update(fa(u));
push_down(u);
}
void splay(ll u){
update(u);
while(!isroot(u)){
if(isroot(fa(u))){//zig
rotate(u);return;
}
if(isleft(u)==isleft(fa(u))){
rotate(fa(u));rotate(u);
}else{
rotate(u);rotate(u);
}
}
}
ll access(ll u){
ll p=0;
while(u){
splay(u);rs(u)=p;
p=u;u=fa(u);
}
return p;
}
void makeroot(ll u){
ll y=access(u);
swap(ls(y), rs(y));
tr[y].tag^=1;
}
void link(ll u, ll v){//保证u和不连通
makeroot(u);
splay(u);fa(u)=v;
}
void cut(ll u, ll v){//保证存在u-v
makeroot(u);access(v);splay(v);
ls(v)=0;fa(u)=0;
}
ll find(ll u){//找到u所在树的原树根
ll p=access(u);
push_down(p);
while(ls(p)){p=ls(p);push_down(p);}
splay(p);
return p;
}
}lct;
int main(){
ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
cin >> n >> m;
rep(i, 1, m){
string s;cin >> s;
ll u, v;cin >>u >> v;
if(s=="Connect"){
lct.link(u, v);
}else if(s=="Destroy"){
lct.cut(u, v);
}else{
if(lct.find(u)==lct.find(v)){
cout << "Yes" << endl;
}else{
cout << "No" << endl;
}
}
}
return 0;
}
查询边(点)权和/边(点)权修改
对一个森林,进行连边,删边操作(保证任何时候整个图是一个森林),修改某个边(点)的权值和查询两个点之间路径的边(点)权和。
解法
先考虑点权怎么维护。
-
点权修改:将这个点 splay 到树根,再修改点权即可。
-
路径点权和查询:将一个点 \(u\) 设为原树的根,然后
access(v),此时 \(u\) 和 \(v\) 之间的路径恰好在同一 Splay 中,直接输出这个 Splay 的点权和。
维护边权的方法是对于每一条边 \((u, v)\),建立一个虚点 \(z\),虚点的点权是原来的边权,让 \(z\) 连接 \(u\) 和 \(v\)。这样问题就变为了点权问题。
例题
点击查看代码
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
#define rep(i, a, b) for(ll i=a;i<=b;i++)
#define rrep(i, a, b) for(ll i=a;i>=b;i--)
const ll N=1e5+9;
ll n, m;
struct LCT{//动态树
#define ls(u) tr[u].l
#define rs(u) tr[u].r
#define fa(u) tr[u].fa
#define pos(u) (ls(fa(u))==u?0:(rs(fa(u))==u?1:-1))
//0为左儿子,1为右儿子,-1表示u是单元splay的根
#define son(u, ty) (ty==0?ls(u):rs(u))
#define isroot(u) (ls(fa(u))!=u&&rs(fa(u))!=u)//判断u是否是单元Splay的根
#define pushup(u) tr[u].xorV=tr[u].val^(ls(u)?tr[ls(u)].xorV:0)^(rs(u)?tr[rs(u)].xorV:0);
void pushdown(ll u){
if(tr[u].p){
if(ls(u)){tr[ls(u)].p^=1;swap(ls(ls(u)), rs(ls(u)));}
if(rs(u)){tr[rs(u)].p^=1;swap(ls(rs(u)), rs(rs(u)));}
tr[u].p=0;
}
}
struct point{
ll l, r, val, p, xorV, fa;//xorV为它领导的单元Splay上的子树异或和
}tr[N];
void rotate(ll u){
ll y=fa(u), z=fa(y);
pushdown(y);pushdown(u);
if(pos(u)==0){
ll p2=rs(u), ty=pos(y);
fa(u)=z;rs(u)=y;
fa(y)=u;ls(y)=p2;
if(p2)fa(p2)=y;
if(ty!=-1)son(z, ty)=u;
}else{
ll p2=ls(u), ty=pos(y);
fa(u)=z;ls(u)=y;
fa(y)=u;rs(y)=p2;
if(p2)fa(p2)=y;
if(ty!=-1)son(z, ty)=u;
}
pushup(y);
pushup(u);
}
void update(ll u){
if(!isroot(u))update(fa(u));
pushdown(u);
}
void splay(ll u){//将u旋转到u所在的单元Splay的根
update(u);//!必须update的原因,如果不updata,原本要zig-zag的变成了zig-zig,原本要zig-zig的变成了zig-zag,复杂度错误
while(!isroot(u)){
if(isroot(fa(u))){
rotate(u);return;
}
if(pos(u)==pos(fa(u))){
rotate(fa(u));rotate(u);
}else{
rotate(u);rotate(u);
}
}
}
ll access(ll u){
ll p=0;
while(u){
splay(u);pushdown(u);rs(u)=p;pushup(u);//!忘记pushup
p=u;u=fa(u);
}
return p;
}
ll find(ll u){//找到u所在的联通块的原根
ll p=access(u);
pushdown(p);
while(ls(p)){p=ls(p);pushdown(p);}
return p;
}
void makeroot(ll u){
ll p=access(u);
swap(ls(p), rs(p));
tr[p].p^=1;
}
bool link(ll u, ll v){
if(find(u)==find(v))return 0;
makeroot(u);
splay(u);
fa(u)=v;
return 1;
}
void cut(ll u, ll v){
makeroot(u);
access(v);
splay(v);
pushdown(v);
ls(v)=fa(u)=0;
pushup(v);//每次树形态的修改都要pushup和pushdown
}
void change(ll u, ll x){
splay(u);
tr[u].val=x;
pushup(u);
}
void printtree(){
rep(u, 1, n){
cout << ls(u) << ' ' << rs(u) << ' ' << tr[u].val << ' ' << fa(u) << ' ' << tr[u].xorV << ' ' << tr[u].p << endl;
}
cout << endl;
}
ll query(ll u, ll v){
makeroot(u);
ll p=access(v);
return tr[p].xorV;
}
}lct;
set <pair<ll, ll> > st;
int main(){
ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
cin >> n >> m;
rep(i, 1, n){
ll a;cin >> a;
lct.change(i, a);
}
rep(i, 1, m){
ll ty, x, y;cin >> ty >> x >> y;
if(ty==0){
cout << lct.query(x, y) << endl;
}else if(ty==1){
if(x>y)swap(x, y);
if(lct.link(x, y))st.insert(make_pair(x, y));
}else if(ty==2){
if(x>y)swap(x, y);
if(st.count(make_pair(x, y))){
st.erase(make_pair(x, y));
lct.cut(x, y);
}
}else{
lct.change(x, y);
}
}
return 0;
}
动态维护最小生成树
对一个无向图,进行加边操作(不保证图是森林)以及查询整张图是否连通,如果连通,则输出整张图的最小生成树边权和。
解法
在加入边 \(w(u, v)\) 时,如果 \(u\), \(v\) 不连通,直接 link,否则,找到路径上边权最大的边,设其为 \(w'\)。
如果 \(w'\) 的边权小于 \(w\) 的边权,不做任何操作,否则,删去 \(w'\),连接 \(w\)。
例题
点击查看代码
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
#define rep(i, a, b) for(ll i=a;i<=b;i++)
const ll N= 400009;
#define ls(u) tr[u].l
#define rs(u) tr[u].r
#define fa(u) tr[u].fa
#define pos(u) (ls(fa(u))==u?0:rs(fa(u))==u?1:-1)
#define son(u, ty) (ty==0?ls(u):rs(u))
struct LCT{//支持断边/连边/修改点权值/查询路径点权和
struct point{
ll l, r, fa, p, val, mx, mxk;
}tr[N];
#define mx(u) (tr[u].mx)
#define val(u) (tr[u].val)
#define isroot(u) (pos(u)==-1?1:0)
void pushdown(ll u){//!什么时候要pushup/down?1.rotate里面,2.如果一个点的儿子要修改/访问,修改前pushdown,修改后pushup 3.修改这个点的点权要pushup
if(tr[u].p){
tr[ls(u)].p^=1;
tr[rs(u)].p^=1;
swap(ls(ls(u)), rs(ls(u)));
swap(ls(rs(u)), rs(rs(u)));
tr[u].p=0;
}
}
void pushup(ll u){
mx(u)=max(val(u), max(mx(ls(u)), mx(rs(u))));
if(val(u)==mx(u))tr[u].mxk=u;
else if(ls(u)&&mx(ls(u))==mx(u))tr[u].mxk=tr[ls(u)].mxk;
else if(rs(u)&&mx(rs(u))==mx(u))tr[u].mxk=tr[rs(u)].mxk;
}
void rotate(ll u){
ll y=fa(u), z=fa(y);
pushdown(y);pushdown(u);
if(pos(u)==0){
ll p2=rs(u), t=pos(y);
rs(u)=y;
fa(u)=z;
fa(y)=u;
ls(y)=p2;
if(p2)fa(p2)=y;
if(t!=-1)son(z, t)=u;
}else{
ll p2=ls(u), t=pos(y);
ls(u)=y;
fa(u)=z;
fa(y)=u;
rs(y)=p2;
if(p2)fa(p2)=y;
if(t!=-1)son(z, t)=u;
}
pushup(y);pushup(u);
}
void pushdownAll(ll u){
if(isroot(u)){pushdown(u);return;}
pushdownAll(fa(u));pushdown(u);
}
void splay(ll u){
pushdownAll(u);//!这句话漏掉了
while(!isroot(u)){
if(isroot(fa(u))){
rotate(u);return;
}
if(pos(u)==pos(fa(u))){
rotate(fa(u));
rotate(u);
}else{
rotate(u);rotate(u);
}
}
}
ll access(ll u){
ll k=0;
while(u){
splay(u);
pushdown(u);
rs(u)=k;
pushup(u);
k=u;
u=fa(u);
}
return k;
}
void makeroot(ll u){
ll p=access(u);
tr[p].p^=1;
swap(ls(p), rs(p));
}
void cut(ll u, ll v){//要求必须存在(u, v)这条边
makeroot(u);
access(v);
splay(v);
pushdown(v);
ls(v)=fa(u)=0;
pushup(v);
}
void link(ll u, ll v){//要求u和v必须不连通
makeroot(u);
splay(u);
fa(u)=v;
}
void update(ll u, ll val){//修改u的点权
splay(u);
val(u)=val;
pushup(u);
}
pair<ll, ll> findmx(ll u, ll v){//要求u和v必须连通
makeroot(u);
access(v);
splay(v);
return make_pair(mx(v), tr[v].mxk);
}
ll findroot(ll u){
access(u);
splay(u);
pushdown(u);
while(ls(u)){u=ls(u);pushdown(u);}
splay(u);
return u;
}
}lct;
ll u[N], v[N];
int main(){
ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
ll n, m;
cin >> n >> m;
ll ans=0;
ll cnt=0;
rep(i, 1, m){
ll x, y, z;cin >> x >> y >> z;
u[i+n]=x;v[i+n]=y;
if(lct.findroot(x)!=lct.findroot(y)){
cnt++;
ans+=z;
lct.update(i+n, z);
lct.link(x, i+n);
lct.link(y, i+n);
}else{
auto mx=lct.findmx(x, y);
if(mx.first<z)continue;
ans+=z-mx.first;
lct.cut(mx.second, u[mx.second]);
lct.cut(mx.second, v[mx.second]);
lct.update(i+n, z);
lct.link(x, i+n);
lct.link(y, i+n);
}
}
if(cnt!=n-1){
cout << "orz" << endl;
}else{
cout << ans << endl;
}
return 0;
}
考虑到最大边最小生成树就是最小生成树,因此按照边权从大到小的顺序加入边,设加入边权为 \(w_i\) 的边后,最小生成树的最大边权为 \(w'_i\),那么答案是 \(\min (w'_i-w_i)\)
点击查看代码
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
#define rep(i, a, b) for(ll i=a;i<=b;i++)
#define rrep(i, a, b) for(ll i=a;i>=b;i--)
const ll N= 400009, INF=1e16;
#define ls(u) tr[u].l
#define rs(u) tr[u].r
#define fa(u) tr[u].fa
#define pos(u) (ls(fa(u))==u?0:rs(fa(u))==u?1:-1)
#define son(u, ty) (ty==0?ls(u):rs(u))
struct LCT{//支持断边/连边/修改点权值/查询路径点权和
struct point{
ll l, r, fa, p, val, mx, mxk;
}tr[N];
#define mx(u) (tr[u].mx)
#define val(u) (tr[u].val)
#define isroot(u) (pos(u)==-1?1:0)
void pushdown(ll u){//!什么时候要pushup/down?1.rotate里面,2.如果一个点的儿子要修改/访问,修改前pushdown,修改后pushup 3.修改这个点的点权要pushup
if(tr[u].p){
tr[ls(u)].p^=1;
tr[rs(u)].p^=1;
swap(ls(ls(u)), rs(ls(u)));
swap(ls(rs(u)), rs(rs(u)));
tr[u].p=0;
}
}
void pushup(ll u){
mx(u)=max(val(u), max(mx(ls(u)), mx(rs(u))));
if(val(u)==mx(u))tr[u].mxk=u;
else if(ls(u)&&mx(ls(u))==mx(u))tr[u].mxk=tr[ls(u)].mxk;
else if(rs(u)&&mx(rs(u))==mx(u))tr[u].mxk=tr[rs(u)].mxk;
}
void rotate(ll u){
ll y=fa(u), z=fa(y);
pushdown(y);pushdown(u);
if(pos(u)==0){
ll p2=rs(u), t=pos(y);
rs(u)=y;
fa(u)=z;
fa(y)=u;
ls(y)=p2;
if(p2)fa(p2)=y;
if(t!=-1)son(z, t)=u;
}else{
ll p2=ls(u), t=pos(y);
ls(u)=y;
fa(u)=z;
fa(y)=u;
rs(y)=p2;
if(p2)fa(p2)=y;
if(t!=-1)son(z, t)=u;
}
pushup(y);pushup(u);
}
void pushdownAll(ll u){
if(isroot(u)){pushdown(u);return;}
pushdownAll(fa(u));pushdown(u);
}
void splay(ll u){
pushdownAll(u);//!这句话漏掉了
while(!isroot(u)){
if(isroot(fa(u))){
rotate(u);return;
}
if(pos(u)==pos(fa(u))){
rotate(fa(u));
rotate(u);
}else{
rotate(u);rotate(u);
}
}
}
ll access(ll u){
ll k=0;
while(u){
splay(u);
pushdown(u);
rs(u)=k;
pushup(u);
k=u;
u=fa(u);
}
return k;
}
void makeroot(ll u){
ll p=access(u);
tr[p].p^=1;
swap(ls(p), rs(p));
}
void cut(ll u, ll v){//要求必须存在(u, v)这条边
makeroot(u);
access(v);
splay(v);
pushdown(v);
ls(v)=fa(u)=0;
pushup(v);
}
void link(ll u, ll v){//要求u和v必须不连通
makeroot(u);
splay(u);
fa(u)=v;
}
void update(ll u, ll val){//修改u的点权
splay(u);
val(u)=val;
pushup(u);
}
pair<ll, ll> findmx(ll u, ll v){//要求u和v必须连通
makeroot(u);
access(v);
splay(v);
return make_pair(mx(v), tr[v].mxk);
}
ll findroot(ll u){
access(u);
splay(u);
pushdown(u);
while(ls(u)){u=ls(u);pushdown(u);}
splay(u);
return u;
}
}lct;
struct edge{
ll u, v, w;
bool operator < (const edge& rhs) const{
return w<rhs.w;
}
}e[N];
multiset <ll> ans;
ll u[N], v[N];
int main(){
ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
ll n, m;
cin >> n >> m;
rep(i, 1, m){
cin >> e[i].u >> e[i].v >> e[i].w;
if(e[i].u==e[i].v){e[i].w=INF;}
}
sort(e+1, e+m+1);
while(e[m].u==e[m].v)m--;
ll cnt=0;
ll res=INF;
rrep(i, m, 1){
ll x=e[i].u, y=e[i].v, z=e[i].w;
u[i+n]=x;v[i+n]=y;
if(lct.findroot(x)!=lct.findroot(y)){
cnt++;
ans.insert(z);
lct.update(i+n, z);
lct.link(x, i+n);
lct.link(y, i+n);
}else{
auto mx=lct.findmx(x, y);
if(mx.first<z)continue;
ans.insert(z);
ans.erase(ans.find(mx.first));
lct.cut(mx.second, u[mx.second]);
lct.cut(mx.second, v[mx.second]);
lct.update(i+n, z);
lct.link(x, i+n);
lct.link(y, i+n);
}
if(cnt==n-1){
res=min(res, *(ans.rbegin())-*(ans.begin()));
}
}
cout << res << endl;
return 0;
}
动态维护点双
有一个无向图。要求支持加边(不保证图是森林)以及查询某两个点是否在同一个边双内/某个点所在的边双中包含的点的数量。
解法
动态维护缩点的过程,不断将处于同一个边双内的点合并在一起。使用并查集维护这一过程。一个点在并差集上的祖先代表这个点出现在 LCT中。(所以 LCT 上的节点数量就是边双的数量)
具体来说,在每次加边 \((u, v)\) 时,考虑:
-
\(u\) 和 \(v\) 不连通,则
link(u, v)。 -
\(u\) 和 \(v\) 已经处于同一边双中(并查集中已经连通),则不做任何操作。
-
\(u\) 和 \(v\) 连通但不属于同一边双中,则先
makeroot(u)再access(v),此时 \(u\) 到 \(v\) 路径上的所有点都处于同一 Splay 中。
然后让这一颗 Splay 上的所有节点都用并查集连接到这个 Splay 的根,这样就起到了合并整颗 Splay 的效果。
Q:这么做仅仅让 Splay 上的节点在并查集上连在了一起,在 LCT 上还是分开的节点,怎么办?
A:将 Splay 的根 \(r\) 的左右儿子都设为空。
Q:但是其他 Splay 的根的父亲依然可能指向错误的节点而非 \(r\)?
A:只需要将所有查询 \(u\) 的父亲的代码从 tr[u].fa 改为 find(tr[u].fa) 即可,这样就能找到了 \(r\)。(find(x) 表示并查集中 \(x\) 的根)
例题
点击查看代码
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
#define rep(i, a, b) for(ll i=a;i<=b;i++)
#define rrep(i, a, b) for(ll i=a;i>=b;i--)
const ll N= 400009, INF=1e16;
#define ls(u) tr[u].l
#define rs(u) tr[u].r
#define fa(u) (tr[u].fa)
#define pos(u) (ls(findl(fa(u)))==u?0:rs(findl(fa(u)))==u?1:-1)
#define son(u, ty) (ty==0?ls(u):rs(u))
ll bcj[N];
ll sz[N];
ll findl(ll u){
if(bcj[u]==u)return u;
return bcj[u]=findl(bcj[u]);
}
struct LCT{
struct point{
ll l, r, fa, p;
}tr[N];
#define mx(u) (tr[u].mx)
#define val(u) (tr[u].val)
#define isroot(u) (pos(u)==-1?1:0)
void pushdown(ll u){//!什么时候要pushup/down?1.rotate里面,2.如果一个点的儿子要修改/访问,修改前pushdown,修改后pushup 3.修改这个点的点权要pushup
if(tr[u].p){
tr[ls(u)].p^=1;
tr[rs(u)].p^=1;
swap(ls(ls(u)), rs(ls(u)));
swap(ls(rs(u)), rs(rs(u)));
tr[u].p=0;
}
}
void rotate(ll u){
ll y=findl(fa(u)), z=findl(fa(y));
pushdown(y);pushdown(u);
if(pos(u)==0){
ll p2=rs(u), t=pos(y);
rs(u)=y;
fa(u)=z;
fa(y)=u;
ls(y)=p2;
if(p2)fa(p2)=y;
if(t!=-1)son(z, t)=u;
}else{
ll p2=ls(u), t=pos(y);
ls(u)=y;
fa(u)=z;
fa(y)=u;
rs(y)=p2;
if(p2)fa(p2)=y;
if(t!=-1)son(z, t)=u;
}
}
void pushdownAll(ll u){
if(isroot(u)){pushdown(u);return;}
pushdownAll(findl(fa(u)));pushdown(u);
}
void splay(ll u){
pushdownAll(u);
while(!isroot(u)){
if(isroot(findl(fa(u)))){
rotate(u);return;
}
if(pos(u)==pos(findl(fa(u)))){
rotate(findl(fa(u)));
rotate(u);
}else{
rotate(u);rotate(u);
}
}
}
ll access(ll u){
ll k=0;
while(u){
splay(u);
pushdown(u);
rs(u)=k;
k=u;
u=findl(fa(u));
}
return k;
}
void makeroot(ll u){
ll p=access(u);
tr[p].p^=1;
swap(ls(p), rs(p));
}
void cut(ll u, ll v){//要求必须存在(u, v)这条边
makeroot(u);
access(v);
splay(v);
pushdown(v);
ls(v)=fa(u)=0;
}
void link(ll u, ll v){//要求u和v必须不连通
makeroot(u);
splay(u);
fa(u)=v;
}
ll findroot(ll u){
access(u);
splay(u);
pushdown(u);
while(ls(u)){u=ls(u);pushdown(u);}
splay(u);
return u;
}
void mergeSplay(ll u, ll b){
if(u!=b){
sz[b]+=sz[u];
bcj[u]=b;
}
if(ls(u))mergeSplay(ls(u), b);
if(rs(u))mergeSplay(rs(u), b);
}
void merge(ll u, ll v){
makeroot(u);
ll p=access(v);//p是单位splay的根
mergeSplay(p, p);
ls(p)=rs(p)=0;
}
}lct;
ll n, m, q;
int main(){
ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
cin >> n >> m >> q;
rep(i, 1, n){
bcj[i]=i;sz[i]=1;
}
rep(i, 1, m+q){
ll u, v;cin >> u >> v;
u=findl(u);v=findl(v);
if(lct.findroot(u)!=lct.findroot(v)){
lct.link(u, v);
if(i>m){
cout << "No\n";
}
}else{
if(u==v){
if(i>m){
cout << sz[u] << endl;
}
continue;
}
lct.merge(u, v);
if(i>m){
cout << sz[findl(u)] << endl;
}
}
}
return 0;
}

浙公网安备 33010602011771号