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;
}
启示:
-
一条链上的操作可以转化为两条根链上的操作。
-
在树链剖分后,根链上的操作大部分都是要 \(O(\log^2 n)\) 的,(当然也有例外,比如在这道题中查询连通块的顶端),因此很多时候就要对偶成子树上的操作,这样就可以做到 \(O(\log n)\)。
-
很经典的对偶:
- 子树修改、单点查 \(\iff\) 单点修改、根链查。
- 根链修改、单点查 \(\iff\) 单点修改、子树查。
- 附加一个差分、前缀和之后:单点修改、根链查 \(\Rightarrow\) 根链修改、单点查。
-
这道题非常考验思维的连贯性,需要在各种转化间来回跳动。
-
原题是要求我们奇偶性相同的加,奇偶性不同的减,这样我们非常的难受。我们直接钦定奇数层为正,偶数层为负,每次看看当前 \(u\) 是第几层,决定是 \(+x\) 还是 \(-x\),最后查询单点的时候我们再乘上层数的正负即可

浙公网安备 33010602011771号