P15952(P15952 [ICPC 2018 Jakarta R] Rotating Gears) 题解

P1004. 旋转齿轮(gears)

洛谷链接

题目大意

有 \(N\) 个齿轮构成一棵树,接触的齿轮旋转方向相反(相邻反向)。初始所有齿轮箭头朝上(角度 \(0\))。
支持三种操作:

  • 1 u:从板上取下齿轮 \(u\)(角度不变);
  • 2 u:将齿轮 \(u\) 放回原位置(角度保持取下时的值);
  • 3 u α:将齿轮 \(u\) 顺时针旋转 \(α\) 度(\(u\) 当前在板上),此时所有在板上的齿轮会联动旋转(相邻反向),本次操作消耗能量 = 参与旋转的齿轮数 × \(α\)。

每次操作 3 输出本次能量,最后输出所有 \(N\) 个齿轮最终角度之和(包括不在板上的齿轮)。

\(N,Q \le 10^5,\ 0 \le α \le 359\),保证齿轮图是一棵树。

解题思路

这是单 \(\log\) 解法,非常妙的一道树上操作题目,一定要理清头绪来写。

首先抽象一下问题,操作 1 和操作 2 相当于在树上删去或者加回一个点;操作 3 相当于给 \(u\) 子树内与 \(u\) 深度奇偶性相同的点加上 \(\alpha\),奇偶性不同的点减去 \(\alpha\),并查询 \(u\) 所在的连通块大小(第一问);最后第二问查询每一个点的权值。

先考虑询问怎么做?再根据询问回去导修改。

很经典的对偶:

  • 子树修改、单点查 \(\iff\) 单点修改、根链查。
  • 根链修改、单点查 \(\iff\) 单点修改、子树查。

树链剖分后,用线段树维护重链。

很显然我们希望能把 \(u\) 改为当前连通块的顶端,这样我们就从考虑连通块变成只用考虑子树了。如何查询连通块的顶端呢?优秀一点的方法是对于每条重链维护最小断点深度(维护这个东西用 set),然后往上跳 \(O(\log n)\) 次重链,每次 \(O(1)\) 判断,如果满足就要进入这条重链然后查找这条重链内更下方的点,这样就能查到连通块的顶端。

现在考虑加断点对连通块大小的影响,每次维护 \(siz\) 表示以 \(u\) 为顶端的实际连通块大小,每次改就是找到 \(u\) 所在的连通块的顶端 \(p\),修改 \(u\to p\) 这条链上的点。我们将其差分成两个根链上的操作,就是 \(rt\to u\) 以及 \(rt\to fa_p\),但是我们在根链上的修改是 \(O(\log^2n)\) 的,于是我们将其转化成单点修改,子树查,这样就可以做到 \(O(\log n)\) 了。

现在我们来到第二问。首先做一个非常有必要的简化,原题是要求我们奇偶性相同的加,奇偶性不同的减,这样我们非常的难受。我们直接钦定奇数层为正,偶数层为负,每次看看当前 \(u\) 是第几层,决定是 \(+x\) 还是 \(-x\),最后查询单点的时候我们再乘上层数的正负即可。

每次操作相当于连通块修改、单点查,但是连通块修改是不好做的,于是我们要将其转化为树上差分,变成单点修改、链查,链查其实可以转化为根链查,中间有个问题,为什么一定要用链查,不能直接查单点(顶部),因为有加删操作直接炸了。

现在考虑加删点,删掉点 \(u\) 时将 \(fa_u\) 的答案建议到点 \(u\),这里统计 \(fa_u\) 的答案同样需要链查。每次查询一个点的最终答案其实就是算出 \(u\to p\) 这条链上的答案,\(p\) 是连通块的顶端,但是在具体实现上 \(p\) 是连通块顶端的父亲,也就是最上面的断点,这样就可以保证在断掉一个点时只用修改这个点本身的值,而不是把它的所有儿子都改掉,这样就保证了复杂度。

与第一问一样,我们将单点修改,根链查转化为子树修改,单点查,最后做完了,注意这里的单点查不是直接查单点,而是类似于查找 \(u\to p\) 的链变成了查 \(u\) 再减去 \(fa_p\)。

综上,我们需要实现树上差分、子树修改、单点修改、子树查询、单点查询等等操作,用两个树状数组维护即可。

代码里的 \(link\) 本来是要查 \(u\to v\) 这条链上的,也就是查 \(u\to rt\) 减去 \(fa_v\to rt\),但是经过转化后,\(u\to rt\) 变成了单点查,所以这里的 link 查也就是两个单点查了。

代码如下,注意 fnd 中,要考虑如果传进来的 \(u\) 是被删掉的点的情况,所以要给 \(v\) 赋初值,初值就是 \(u\) 随便一个孩子,因为回来后要取的是 \(fa_{fnd}\),所以 \(fa_{son_u}=u\),给 \(v\) 赋值 \(son_u\) 即可。

code
#define IOS ios::sync_with_stdio(false),cin.tie(0),cout.tie(0)
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
typedef unsigned long long ull;
#define out() (cout << "sb\n")

const int N=1e5+5;

int n,q,tot,dfn[N],rnk[N],tp[N],fa[N],son[N],siz[N],dep[N];
vector<int> G[N];
set<int> s[N];
bool on[N];

struct BIT{
	int bt[N];
	void add(int pos,int x){
		if(!pos) return;
		for(int i=pos;i<=n;i+=(i&(-i))) bt[i]+=x;
	}
	int qry(int pos){
		if(pos==0) return 0;
		int ans=0;
		for(int i=pos;i>=1;i-=(i&(-i))) ans+=bt[i];
		return ans;
	}
	void node_add(int u,int x){add(dfn[u],x);}
	void tree_add(int u,int x){add(dfn[u],x),add(dfn[u]+siz[u],-x);}
	void link_add(int u,int v,int x){add(dfn[u],x),add(dfn[fa[v]],-x);}
	
	int tree_qry(int u){return qry(dfn[u]+siz[u]-1)-qry(dfn[u]-1);}
	int link_qry(int u,int v){return qry(dfn[u])-qry(dfn[fa[v]]);}
}t1,t2; //连通块大小、树上差分的答案 

void dfs1(int u,int p){
	fa[u]=p,siz[u]=1,dep[u]=dep[p]+1;
	for(int v:G[u]){
		if(v==p) continue;
		dfs1(v,u);
		siz[u]+=siz[v];		if(siz[v]>siz[son[u]]) son[u]=v;

	}
}

void dfs2(int u,int tpk){
	dfn[u]=++tot,rnk[tot]=u,tp[u]=tpk;
	if(son[u]) dfs2(son[u],tpk);
	for(int v:G[u]){
		if(v==fa[u]||v==son[u]) continue;
		dfs2(v,v);
	}	
}

#define Op(u) (dep[u]&1 ? 1:-1)

int fnd(int u){ //找顶端
	int v=son[u]; //要记得赋初值。 
	while(u){
		int p=tp[u];
		if(!s[p].empty() && *s[p].begin()<=dfn[u]){
			int now=*(--s[p].upper_bound(dfn[u]));
			if(now==dfn[u]) return v; //返回之前的重链顶端
			return son[rnk[now]];
		}
		v=p,u=fa[v];
	}
	return 1;
}

void del(int u){
	int p=fa[fnd(u)]; //第一个黑点
	t1.link_add(fa[u],p,-t1.tree_qry(u));
	on[u]=0;
	t2.tree_add(u,t2.link_qry(fa[u],p));
	s[tp[u]].insert(dfn[u]); 
}

void rec(int u){ //recover 
	int p=fa[fnd(fa[u])];
	t1.link_add(fa[u],p,t1.tree_qry(u));
	on[u]=1;
	t2.tree_add(u,-t2.link_qry(fa[u],p));
	s[tp[u]].erase(dfn[u]);
}

int main(){
	//system("fc .out .out");
	freopen("gears.in", "r", stdin);
	freopen("gears.out", "w", stdout);
	IOS;
	cin>>n;
	for(int i=1;i<=n;i++) on[i]=1;
	for(int i=1;i<n;i++){
		int u,v;
		cin>>u>>v;
		G[u].push_back(v);
		G[v].push_back(u);
	}
	dfs1(1,0); // 要保证 fa[1]=0 
	dfs2(1,1); 
	for(int i=1;i<=n;i++) t1.node_add(i,1); //每个点的大小 
	cin>>q;
	while(q--){
		int op,u,x;
		cin>>op>>u;
		if(op==1) del(u);
		else if(op==2) rec(u);
		else if(op==3){
			cin>>x;
			int p=fnd(u);
//			cout<<p<<"\n";x
			t2.tree_add(p,Op(u)*x);
//			if(t1.tree_qry(p)*x==-90) cout<<t1.tree_qry(p)<<"\n";
			cout<<t1.tree_qry(p)*x<<"\n";
		}
//		for(int i=1;i<=n;i++) cout<<t2.link_qry(i,1)<<" \n"[i==n];
	} 
	for(int i=1;i<=n;i++) if(!on[i]) rec(i);
	int ans=0;
	for(int i=1;i<=n;i++) ans+=(t2.link_qry(i,1)*Op(i)%360+360)%360;
	cout<<ans<<"\n";
	return 0;
}

启示:

  1. 一条链上的操作可以转化为两条根链上的操作。

  2. 在树链剖分后,根链上的操作大部分都是要 \(O(\log^2 n)\) 的,(当然也有例外,比如在这道题中查询连通块的顶端),因此很多时候就要对偶成子树上的操作,这样就可以做到 \(O(\log n)\)。

  3. 很经典的对偶:

    • 子树修改、单点查 \(\iff\) 单点修改、根链查。
    • 根链修改、单点查 \(\iff\) 单点修改、子树查。
    • 附加一个差分、前缀和之后:单点修改、根链查 \(\Rightarrow\) 根链修改、单点查。
  4. 这道题非常考验思维的连贯性,需要在各种转化间来回跳动。

  5. 原题是要求我们奇偶性相同的加,奇偶性不同的减,这样我们非常的难受。我们直接钦定奇数层为正,偶数层为负,每次看看当前 \(u\) 是第几层,决定是 \(+x\) 还是 \(-x\),最后查询单点的时候我们再乘上层数的正负即可

posted @ 2026-09-09 16:21  lcx_OIer  阅读(22)  评论(0)    收藏  举报