Loading

学习笔记——点分治&点分树

前言

又是每个人都会的算法,我不会只能爬去学~

点分治

点分治通常用来解决树上路径统计问题。

其实点分治的想法很简单就是:我们考虑一棵有根的树,显然可以将其路径分成两类,一类是经过根的,一类是不经过根的。据此,可以得到一个牛逼想法就是:对于当前的根节点,直接求出它到子树内各个点的信息并统计,然后合并各个子树的信息。最后直接递归各个子树解决子问题,从而减少一些不必要的运算。

但是这个想法很快就假了。关键点就在于如何确定作为根的节点。我们联系到树的各种性质,发现重心是最优的。

所以我们的做法就是——找到重心——处理经过重心的路径——分治重心的各个子树——求答案。

求重心

int rt,S,rtmx; bool used[MAXN];
int find(int x,int fa){
	int mx=0,siz=1,son;
	for(Edge s:e[x]){
		if(s.to==fa||used[s.to]) continue;
		siz+=(son=find(s.to,x));
		mx=max(mx,son);
	}mx=max(mx,S-siz);
	if(rtmx>mx) rt=x,rtmx=mx;
	return siz;
}

解释一下,rt 存的是重心,S 存的是当前找重心的树的大小(因为你每次递归到子树处理的时候都需要处理子树的重心来继续分治),used[] 表示这个点有没有作为重心被删除(显然你不能重心分治后还算上其他子树中的点)。

其他都挺好理解的,复杂度 \(O(S)\)

分治

void Divid(int x){
	used[x]=1;
	for(auto s:e[x]){
		if(used[s.to]) continue;
		//处理当前跟相关的东西
	}init();
	for(auto s:e[x]){
		if(used[s.to]) continue;
		S=Siz(s.to,s.to);rtmx=inf;find(s.to,s.to);
		Divid(rt)//递归处理子树
	}
}

个人易错

  • siz 的时候注意函数初始返回值是 siz=1
  • 不要忘记 cnt[0]=1 之类的。

例题

感觉核心就是思想,和 dsu on tree 一样,所以还是要多写几题。


P3806 【模板】点分治1
模板但是比较恶心~考虑用桶统计当前到根距离为 \(i\) 的节点有没有出现。

My Code
const int MAXN=1e5+10;
struct Edge{int to,w;};
vector<Edge> e[MAXN];
int rt,S,rtmx; bool used[MAXN];
int find(int x,int fa){
	int mx=0,siz=1,son;
	for(Edge s:e[x]){
		if(s.to==fa||used[s.to]) continue;
		siz+=(son=find(s.to,x));
		mx=max(mx,son);
	}mx=max(mx,S-siz);
	if(rtmx>mx) rt=x,rtmx=mx;
	return siz;
}
int Siz(int x,int fa){
	int ret=1;
	for(Edge s:e[x]){
		if(s.to==fa||used[s.to]) continue;
		ret+=Siz(s.to,x);
	}return ret;
}
const int MAXM=1e7+10;
bool ans[MAXM];
int K;
vector<int> val,Ap;
void getdis(int x,int fa,int len){
	if(len>K) return;
	val.pb(len);
	for(auto s:e[x]){
		if(s.to==fa||used[s.to]) continue;
		getdis(s.to,x,len+s.w);
	}
}
void init(){while(!Ap.empty()) ans[Ap.back()]=0,Ap.pop_back();}
bool Divid(int x){
	used[x]=1;
	for(auto s:e[x]){
		if(used[s.to]) continue;
		getdis(s.to,x,s.w);
		for(auto W:val) if(ans[K-W]){init();val.clear();used[x]=0;return 1;}
		while(!val.empty()){
			if(!ans[val.back()]) ans[val.back()]=1,Ap.pb(val.back());
			val.pop_back();
		}
	}init();
	for(auto s:e[x]){
		if(used[s.to]) continue;
		S=Siz(s.to,s.to);rtmx=inf;find(s.to,s.to);
		if(Divid(rt)){used[x]=0;return 1;}
	}used[x]=0;return 0;
}
int main()
{
//	freopen("P3806_1.in","r",stdin);
//	freopen("my.out","w",stdout);
	int n,m;
	scanf("%d%d",&n,&m);
	for(int i=2,u,v,w;i<=n;i++){
		scanf("%d%d%d",&u,&v,&w); 
		e[u].pb((Edge){v,w});
		e[v].pb((Edge){u,w});
	}ans[0]=1;
	S=n;rtmx=inf;find(1,0);
	int Rt=rt;
	while(m--){
		scanf("%d",&K);
		if(Divid(Rt)) puts("AYE");
		else puts("NAY");
	}
}

CF161D Distance in Tree
和上面那题差不多,只不过换成了计数,本质相同。

My Code
const int MAXN=5e4+10;
vector<int> e[MAXN];
int rt,S,mxrt;bool used[MAXN];
int find(int x,int fa){
	int mx=0,siz=1,son;
	for(int s:e[x]){
		if(s==fa||used[s]) continue;
		siz+=(son=find(s,x));
		mx=max(mx,son);
	}mx=max(mx,S-siz);
	if(mx<mxrt) rt=x,mxrt=mx;
	return siz;
}
int Siz(int x,int fa){
	int ret=1;
	for(int s:e[x]){
		if(s==fa||used[s]) continue;
		ret+=Siz(s,x);
	}return ret;
}
int cnt[MAXN],K,Ans;
vector<int> val,Ap;
void init(){
	while(!Ap.empty()){
		cnt[Ap.back()]--;
		Ap.pop_back();
	}
}
void getdis(int x,int fa,int len){
	if(len>K) return;val.pb(len);
	for(int s:e[x]){
		if(s==fa||used[s]) continue;
		getdis(s,x,len+1);
	}
}
void Divid(int x){
	used[x]=1;
	for(int s:e[x]){
		if(used[s]) continue;
		getdis(s,x,1);
		for(int q:val) Ans+=cnt[K-q],Ap.pb(q);
		for(int q:val) cnt[q]++;val.clear();
	}init();
	for(int s:e[x]){
		if(used[s]) continue;
		S=Siz(s,s);mxrt=inf;find(s,s);
		Divid(rt);
	}used[x]=0;
}
int main()
{
	int n;scanf("%d%d",&n,&K);
	for(int i=2,u,v;i<=n;i++){
		scanf("%d%d",&u,&v);
		e[u].pb(v);e[v].pb(u);
	}
	cnt[0]=1;
	mxrt=inf;S=n;find(1,1);
	Divid(rt);
	printf("%d\n",Ans);
}

P4178 Tree
本质相同,和上面一样,不过把桶换成树状数组,然后每次查询小于等于 \(K-W\) 的路径数。

My Code
const int MAXN=4e4+10;
const int N=2e4;
int tr[MAXN];
int lbt(int x){return x&(-x);}
void add(int x,int v){for(int i=x;i<=N;i+=lbt(i))tr[i]+=v;}
int ask(int x){int ret=0;for(int i=x;i;i-=lbt(i))ret+=tr[i];return ret+1;}
struct Edge{int to,w;};
vector<Edge> e[MAXN];
int rt,S,mxrt;bool used[MAXN];
int find(int x,int fa){
	int mx=0,siz=1,son;
	for(auto s:e[x]){
		if(s.to==fa||used[s.to]) continue;
		son=find(s.to,x);siz+=son;
		mx=max(mx,son);
	}mx=max(mx,S-siz);
	if(mx<mxrt) mxrt=mx,rt=x;
	return siz;
}
int Siz(int x,int fa){
	int siz=1;
	for(auto s:e[x]){
		if(s.to==fa||used[s.to]) continue;
		siz+=Siz(s.to,x);
	}return siz;
}
int K,Ans;
vector<int> val,Ap;
void getdis(int x,int fa,int len){
	if(len>K) return;val.pb(len);
	for(auto s:e[x]){
		if(s.to==fa||used[s.to]) continue;
		getdis(s.to,x,len+s.w);
	}
}
void Divid(int x){
	used[x]=1;
	for(auto s:e[x]){
		if(used[s.to]) continue;
		getdis(s.to,s.to,s.w);
		for(int q:val) Ans+=ask(K-q),Ap.pb(q);
		for(int q:val) add(q,1);val.clear();
	}while(!Ap.empty()) add(Ap.back(),-1),Ap.pop_back();
	for(auto s:e[x]){
		if(used[s.to]) continue;
		mxrt=inf;S=Siz(s.to,s.to);find(s.to,s.to);
		Divid(rt);
	}
}
int main()
{
	int n;
	scanf("%d",&n);
	for(int i=1,u,v,w;i<n;i++){
		scanf("%d%d%d",&u,&v,&w);
		e[u].pb(Edge{v,w});
		e[v].pb(Edge{u,w});
	}
	scanf("%d",&K);
	mxrt=inf;S=n;find(1,1);
	Divid(rt);
	printf("%d\n",Ans);
}

P4149 [IOI2011]Race
感觉也是差不多的,直接用一个桶记录长度为 \(i\) 的最短路径长度是多少,答案取小的就行了。

My Code
const int MAXN=1e6+10;
struct Edge{int to,w;};
vector<Edge> e[MAXN];
int mxrt,rt,S;bool used[MAXN];
int find(int x,int fa){
	int mx=0,siz=1,son;
	for(auto s:e[x]){
		if(s.to==fa||used[s.to]) continue;
		siz+=(son=find(s.to,x));
		mx=max(mx,son);
	}mx=max(mx,S-siz);
	if(mx<mxrt) mxrt=mx,rt=x;
	return siz;
}
int Siz(int x,int fa){
	int siz=1;
	for(auto s:e[x]){
		if(s.to==fa||used[s.to]) continue;
		siz+=Siz(s.to,x);
	}return siz;
}
int K,pat[MAXN],Ans=inf;
vector<pii> val;
vector<int> Ap;
void getdis(int x,int fa,int len,int dep){
	if(len>K) return;val.pb(mkp(len,dep));
	for(auto s:e[x]){
		if(s.to==fa||used[s.to]) continue;
		getdis(s.to,x,len+s.w,dep+1);
	}
}
void Divid(int x){
	used[x]=1;
	for(auto s:e[x]){
		if(used[s.to]) continue;
		getdis(s.to,s.to,s.w,1);
		for(auto q:val) Ans=min(Ans,pat[K-q.fi]+q.se);
		for(auto q:val) pat[q.fi]=min(pat[q.fi],q.se),Ap.pb(q.fi);
		val.clear();
	}while(!Ap.empty()) pat[Ap.back()]=inf,Ap.pop_back();
	for(auto s:e[x]){
		if(used[s.to]) continue;
		S=Siz(s.to,s.to);mxrt=inf;find(s.to,s.to);
		Divid(rt);
	}
}
int main()
{
	int n;scanf("%d%d",&n,&K);
	for(int i=2,u,v,w;i<=n;i++){
		scanf("%d%d%d",&u,&v,&w);
		u++,v++;e[u].pb(Edge{v,w});e[v].pb(Edge{u,w});
	}memset(pat,0x3f,sizeof(pat));pat[0]=0;
	mxrt=inf;S=n;find(1,1);
	Divid(rt);printf("%d\n",Ans>n?-1:Ans);
}

SP1825 FTOUR2 - Free tour II
这题太难了,套路是一样一样的,但是我们发现随手写的线段树 T 了,于是开始咒骂出题人。
然后发现只需要前缀 max,换了个树状数组就过了。

My Code
const int MAXN=2e5+10;
struct Edge{int to,w;};
vector<Edge> e[MAXN];
int mxrt,rt,S;bool used[MAXN];
int find(int x,int fa){
	int siz=1,mx=0,son;
	for(auto s:e[x]){
		if(s.to==fa||used[s.to]) continue;
		siz+=(son=find(s.to,x));
		mx=max(mx,son);
	}mx=max(mx,S-siz);
	if(mx<mxrt) mxrt=mx,rt=x;
	return siz;
}
int Siz(int x,int fa){
	int siz=1;
	for(auto s:e[x]){
		if(s.to==fa||used[s.to]) continue;
		siz+=Siz(s.to,x);
	}return siz;
}

int tr[MAXN],m;
int lbt(int x){return x&(-x);}
void upd(int x,int v){if(x<0) return;
	if(x==0){tr[x]=max(tr[x],v);return;}
	for(int i=x;i<=m;i+=lbt(i)) tr[i]=max(tr[i],v);
}int ask(int x){
	int ret=-inf;if(x<0) return -inf;
	for(int i=x;i>0;i-=lbt(i)) ret=max(ret,tr[i]);
	return max(ret,tr[0]);
}void init(int x){if(x<0) return;
	if(x==0){tr[x]=0;return;}
	for(int i=x;i<=m;i+=lbt(i)) tr[i]=-inf;
}

int Ans=0,col[MAXN],K;
vector<int> Ap;
vector<pii> val;
void getdis(int x,int fa,int cnt,int len){
	if(cnt>K) return;val.pb(mkp(cnt,len));
	for(auto s:e[x]){
		if(s.to==fa||used[s.to]) continue;
		getdis(s.to,x,cnt+col[s.to],len+s.w);
	}
}
void Divid(int x){
	used[x]=1;
	for(auto s:e[x]){
		if(used[s.to]) continue;
		getdis(s.to,x,col[s.to],s.w);
		//注意没有算 x 的颜色
		for(auto q:val) Ans=max(Ans,ask(K-q.fi-col[x])+q.se);
		for(auto q:val) upd(q.fi,q.se),Ap.pb(q.fi);val.clear();
	}while(!Ap.empty()){init(Ap.back());Ap.pop_back();}
	for(auto s:e[x]){
		if(used[s.to]) continue;
		mxrt=inf;S=Siz(s.to,s.to);find(s.to,s.to);
		Divid(rt);
	}
}
int main()
{
	int n;
	scanf("%d%d%d",&n,&K,&m);
	for(int i=1,t;i<=m;i++){scanf("%d",&t);col[t]=1;}
	for(int i=2,u,v,w;i<=n;i++){
		scanf("%d%d%d",&u,&v,&w);
		e[u].pb(Edge{v,w});
		e[v].pb(Edge{u,w});
	}memset(tr,-0x3f,sizeof(tr));upd(0,0);
	mxrt=inf;S=n;find(1,1);Divid(rt);
	printf("%d\n",Ans);
}

P6326 Shopping
这题主要是套了一个树上的 \(dp\)。对于一个固定的根,我们怎么计算其子树中经过根的路径对答案的贡献呢。
不妨设 \(dp[i][j]\) 表示在点分治的时候根 \(i\) 在联通块中,花费了 \(j\) 后所能获得的最大价值。
那只要搞出这个 \(dp[i][j]\)。我们考虑在向下跑的时候,利用大法师的遍历顺序,直接进行状态转移即可。为了解决依赖的问题,我们先强制进行一次转移,使之至少选一个,然后再跑多重背包即可。

My Code
#define clr(a) memset(a,0,sizeof(a))
const int MAXN=4010;
vector<int> e[MAXN];
int mxrt,rt,S;bool used[MAXN];
int find(int x,int fa){
	int siz=1,mx=0,son;
	for(auto s:e[x]){
		if(s==fa||used[s]) continue;
		siz+=(son=find(s,x));
		mx=max(mx,son);
	}mx=max(mx,S-siz);
	if(mx<mxrt) mxrt=mx,rt=x;
	return siz;
}
int Siz(int x,int fa){
	int siz=1;
	for(auto s:e[x]){
		if(s==fa||used[s]) continue;
		siz+=Siz(s,x);
	}return siz;
}

int w[MAXN],c[MAXN],d[MAXN],m;
int dp[MAXN][MAXN],f[MAXN],Ans=0;
void DP(int ww,int cc,int dd){
	for(int i=1;i<=dd;dd-=i,i<<=1)
		for(int j=m;j>=i*cc;j--)
			f[j]=max(f[j],f[j-i*cc]+i*ww);
	if(!dd) return;
	for(int j=m;j>=dd*cc;j--)
		f[j]=max(f[j],f[j-dd*cc]+dd*ww);
}
void dfs(int x,int fa,int tot){
	if(tot>m) return;
	for(int i=0;i<=m;i++) dp[x][i]=f[i];
	for(int i=m;i>=0;i--)
		if(i>=tot) f[i]=f[i-c[x]]+w[x];
		else f[i]=-inf;DP(w[x],c[x],d[x]-1);
	for(int s:e[x]){
		if(s==fa||used[s]) continue;
		dfs(s,x,tot+c[s]);
	}for(int i=0;i<=m;i++) dp[x][i]=f[i]=max(dp[x][i],f[i]);
}
void Divid(int x){
	for(int i=0;i<=m;i++) f[i]=-inf;f[0]=0;
	dfs(x,x,c[x]);for(int i=0;i<=m;i++) Ans=max(Ans,dp[x][i]);
	used[x]=1;
	for(int s:e[x]){
		if(used[s]) continue;
		mxrt=inf;S=Siz(s,s);find(s,s);
		Divid(rt);
	}
}

void solve(){
	int n;
	scanf("%d%d",&n,&m);
	for(int i=1;i<=n;i++) scanf("%d",&w[i]);
	for(int i=1;i<=n;i++) scanf("%d",&c[i]);
	for(int i=1;i<=n;i++) scanf("%d",&d[i]);
	for(int i=1,u,v;i<n;i++){
		scanf("%d%d",&u,&v);
		e[u].pb(v);e[v].pb(u);
	}
	mxrt=inf;S=n;find(1,1);Divid(rt);
	printf("%d\n",Ans);
	Ans=0;clr(used);clr(dp);clr(f);
	for(int i=1;i<=n;i++) e[i].clear();
}
int main()
{
	int T;
	for(scanf("%d",&T);T--;)
		solve();
	return 0;
}

点分树

(氡态淀粉质?)

考虑把淀粉质每次求重心的结果来构造父子关系,成为一棵高为 \(\log n\) 的树,使得原来暴力的做法复杂度是正确的。

一般而言,点分树用来解决无关于树的形态的树上修改与路径计数问题。

其套路是:

  1. 对于每个节点建立两个数据结构 \(S_1,S_2\) 分别表示当前节点 \(x\) 的贡献和其父亲 \(fa\) 的贡献。
  2. 修改操作,对于一个节点 \(u\) 的修改,从点分树上向上逐个跳。如果当前节点是 \(x\),那么在 \(x\)\(S_1,S_2\) 中更新贡献。
  3. 查询操作,对于一个节点 \(u\) 的查询,从点分树上向上逐个跳。如果当前节点是 \(x\),上一个节点是 \(l\),那么对答案加上 \(x\)\(S_1\),减去 \(l\)\(S_2\)

有一个方法就是初始化可以看成 \(n\) 次修改。

刚学点分治和点分树的同♂学可能会不是很理解为什么要维护第二个 \(S_2\)。这里给出简单的解释。

其实我们在数据结构中存储的是当前节点 \(x\) 到它子树中所有节点的贡献。在我们向上跳的时候,如果仅仅是累加 \(S_1\),做的时候,我们是在数据结构中查询有没有 \(K-val(x,u)\) 的路径是从 \(x\) 开始的。那么显然会把下面这种路径计算进去:

为了把这样的路径减掉,我们需要从 \(x\) 的上一个节点中得到 \(S_2\) 的信息——也就是被算重的 \(x\) 中的 \(S_1\) 的信息。通俗地说,就是询问一个节点 \(u\) 时,要去掉从 \(u\) 出发再回到 \(u\) 子树内的信息,做法就是减去独属于 \(u\) 子树内的信息,也就是 \(S_2\) 记录的 \(u\)\(u\) 父亲产生的贡献。

好啦这就是点分树了,来写几道题吧。

例题

题都好♂,希望能写van。


P6329 【模板】点分树 | 震波
模板题。

My Code
const int MAXN=1e5+10;
vector<int> e[MAXN];

int son[MAXN],top[MAXN],dep[MAXN],siz[MAXN],f[MAXN];
void Fdfs(int x,int fa){
	son[x]=-1;siz[x]=1;f[x]=fa;
	for(int s:e[x]){
		if(s==fa) continue;
		dep[s]=dep[x]+1;Fdfs(s,x);siz[x]+=siz[s];
		if(son[x]==-1||siz[s]>siz[son[x]]) son[x]=s;
	}
}
void Rdfs(int x,int t){
	top[x]=t;
	if(~son[x]) Rdfs(son[x],t);
	for(int s:e[x])
		if(s!=son[x]&&s!=f[x])
			Rdfs(s,s);
}
int LCA(int u,int v){
	while(top[u]^top[v]){
		if(dep[top[u]]<dep[top[v]]) swap(u,v);
		u=f[top[u]];
	}return dep[u]>dep[v]?v:u;
}
int Dis(int u,int v){return dep[u]+dep[v]-2*dep[LCA(u,v)];}

struct Tree{
	vector<int> tr;
	int lbt(int x){return x&(-x);}
	void upd(int x,int v){
		if(x==0){tr[0]+=v;return;}
		for(int i=x;i<tr.size();i+=lbt(i))
			tr[i]+=v;
	}int ask(int x){
		if(x<0) return 0;
		x=min(x,(int)tr.size()-1);
		int ret=0;
		for(int i=x;i;i-=lbt(i))
			ret+=tr[i];
		return ret+tr[0];
	}
}S1[MAXN],S2[MAXN];

int mxrt,rt,S,dff[MAXN];bool used[MAXN];
int find(int x,int fa){
	int mx=0,siz=1,son;
	for(int s:e[x]){
		if(s==fa||used[s]) continue;
		siz+=(son=find(s,x));
		mx=max(mx,son);
	}mx=max(mx,S-siz);
	if(mx<mxrt) rt=x,mxrt=mx;
	return siz;
}
int Siz(int x,int fa){
	int siz=1;
	for(int s:e[x]){
		if(s==fa||used[s]) continue;
		siz+=Siz(s,x);
	}return siz;
}
void Divid(int x,int tot){
	used[x]=1;
	S1[x].tr.resize(tot+1);
	S2[x].tr.resize(tot+1);
	for(int s:e[x]){
		if(used[s]) continue;
		mxrt=inf;S=Siz(s,s);
		find(s,s);dff[rt]=x;Divid(rt,S);
	}
}

void Updat(int u,int v){
	int cur=u;
	while(cur){
		S1[cur].upd(Dis(cur,u),v);
		if(dff[cur]) S2[cur].upd(Dis(dff[cur],u),v);
		cur=dff[cur];
	}
}
int Query(int u,int k){
	int cur=u,lst=-1,ret=0;
	while(cur){
		int dist=Dis(cur,u);
		ret+=S1[cur].ask(k-dist);
		if(~lst) ret-=S2[lst].ask(k-dist);
		lst=cur;cur=dff[cur];
	}return ret;
}

int a[MAXN];
void prework(int n){
	Fdfs(1,1);Rdfs(1,1);
	mxrt=inf;S=n;find(1,1);
	Divid(rt,n);
	for(int i=1;i<=n;i++)
		Updat(i,a[i]);
}
int main()
{
	int n,m;
	scanf("%d%d",&n,&m);
	for(int i=1;i<=n;i++) scanf("%d",&a[i]);
	for(int i=1,u,v;i<n;i++){
		scanf("%d%d",&u,&v);
		e[u].pb(v);e[v].pb(u);
	}
	prework(n);
	int opt,x,y,ans=0;
	while(m--){
		scanf("%d%d%d",&opt,&x,&y);
		x^=ans;y^=ans;
		if(opt==0) printf("%d\n",ans=Query(x,y));
		else Updat(x,y-a[x]),a[x]=y;
	}
}

P2056 [ZJOI2007]捉迷藏
SP2666 QTREE4 - Query on a tree IV双倍经验

这题需要维护最大值,维护最值这一类信息的方法略有不同。但其实是一样的。我们还是记录 \(S_1,S_2\),然后每次搞出 \(S_2\) 的最值,来更新 \(S_1\)。这个数据结构可以用平衡树或者堆(这个写法记在 Trick 里了)。

傻逼色批哦截,我改了改水双倍经验又 T 又 WA 的/ll。

My Code
const int MAXN=1e5+10;
vector<int> e[MAXN];
namespace TD{
	int son[MAXN],siz[MAXN],f[MAXN],dep[MAXN];
	void dfs1(int x,int fa){
		son[x]=-1;siz[x]=1;
		for(int s:e[x]){
			if(s==fa) continue;
			dep[s]=dep[x]+1;f[s]=x;
			dfs1(s,x);siz[x]+=siz[s];
			if(son[x]==-1||siz[son[x]]<siz[s])
				son[x]=s;
		}
	}
	int top[MAXN];
	void dfs2(int x,int t){
		top[x]=t;
		if(~son[x]) dfs2(son[x],t);
		for(int s:e[x])
			if(s!=son[x]&&s!=f[x])
				dfs2(s,s);
	}
	int LCA(int x,int y){
		while(top[x]^top[y]){
			if(dep[top[x]]<dep[top[y]]) swap(x,y);
			x=f[top[x]];
		}return dep[x]>dep[y]?y:x;
	}
	int dis(int x,int y){return dep[x]+dep[y]-2*dep[LCA(x,y)];}
}
struct Heap{
    priority_queue<int> val,del;
    void push(int x){val.push(x);}
    void erase(int x){del.push(x);}
    int top(){
    	while(!del.empty()&&val.top()==del.top())
    	val.pop(),del.pop();return val.top();
    }
    void pop(){
    	while(!del.empty()&&val.top()==del.top())
    	val.pop(),del.pop();val.pop();
    }
    int rop(){
    	int t=top();pop();int ret=top();push(t);return ret;
    }
    int size(){return val.size()-del.size();}
}S1[MAXN],S2[MAXN],Ans;
int rt,S,mxrt,vis[MAXN];
int find(int x,int fa){
	int ret=1,son,mx=0;
	for(int s:e[x]){
		if(s==fa||vis[s]) continue;
		ret+=(son=find(s,x));
		mx=max(mx,son);
	}mx=max(mx,S-ret);
	if(mxrt>mx) mxrt=mx,rt=x;
	return ret;
}
int Siz(int x,int fa){
	int ret=1;
	for(int s:e[x]){
		if(s==fa||vis[s]) continue;
		ret+=Siz(s,x);
	}return ret;
}
int F[MAXN];
void Divid(int x){
	vis[x]=1;
	for(int s:e[x]){
		if(vis[s]) continue;
		mxrt=inf;S=Siz(s,s);find(s,s);
		F[rt]=x;Divid(rt);
	}
}
int sta[MAXN];
void Del(Heap &a){if(a.size()>1) Ans.erase(a.top()+a.rop());}
void Ins(Heap &a){if(a.size()>1) Ans.push(a.top()+a.rop());}
void On(int u){
	Del(S2[u]);S2[u].erase(0);Ins(S2[u]);
	for(int i=u;F[i];i=F[i]){
		Del(S2[F[i]]);if(S1[i].size()) S2[F[i]].erase(S1[i].top());
		S1[i].erase(TD::dis(u,F[i]));
		if(S1[i].size()) S2[F[i]].push(S1[i].top());Ins(S2[F[i]]);
	}
}
void Off(int u){
	Del(S2[u]);S2[u].push(0);Ins(S2[u]);
	for(int i=u;F[i];i=F[i]){
		Del(S2[F[i]]);if(S1[i].size()) S2[F[i]].erase(S1[i].top());
		S1[i].push(TD::dis(u,F[i]));
		if(S1[i].size()) S2[F[i]].push(S1[i].top());Ins(S2[F[i]]);
	}
}
int main()
{
	ios::sync_with_stdio(0);
	cin.tie(0);cout.tie(0);
	int n;cin>>n;
	rep(i,2,n){
		int u,v;cin>>u>>v;
		e[u].pb(v);e[v].pb(u);
	}
	mxrt=inf;S=n;find(1,1);
	Divid(rt);
	TD::dfs1(1,0);TD::dfs2(1,1);
	rep(i,1,n) Off(i);
	int Q;cin>>Q;
	char op;int u,sum=n;
	while(Q--){
		cin>>op;
		if(op=='C'){
			cin>>u;
			if(!sta[u]) On(u),sum--;
			else Off(u),sum++;sta[u]^=1;
		}else{
			if(sum<=1) cout<<sum-1<<'\n';
			else cout<<Ans.top()<<'\n';
		}
	}
	return 0;
}

P3241 [HNOI2015]开店


P3345 [ZJOI2015]幻想乡战略游戏


P3920 [WC2014]紫荆花之恋


还有两题太恶心了,不打算现在写。
P4220 [WC2018]通道
P4565 [CTSC2018]暴力写挂

posted @ 2022-01-20 21:02  ZCETHAN  阅读(89)  评论(0编辑  收藏  举报