长链剖分
能 \(O(n)\) 时空解决部分 dsu on tree / 线段树合并 / 倍增 \(O(n\log n)\) 时空的问题。
长链剖分的机制是选子树内深度最大的儿子作为长儿子(类比重链剖分的重儿子)。
然后有几个性质:
-
一条链向上跳一条链,则链上最大深度不减。
-
由 1 可知任何一个点向上跳链次数不会超过 \(\sqrt n\) 次。(但是这个性质不是很有用)。
-
任意一个点 \(x\) 的 \(k\) 次祖先 \(y\) 所在的长链的长度大于等于 \(k\),因为 \(x\to y\) 长度为 \(k\),长儿子必然更长。
P5903 【模板】树上 k 级祖先
对树进行长链剖分,记录每个点所在链的顶点和深度。
树上倍增求出每个点的 \(2^c\) 级祖先。
对于每条链,如果其长度为 \(len\),那么在顶点处记录顶点向上的 \(len\) 个祖先和向下的 \(len\) 个链上的儿子。
预处理出每个数的二进制最高位。
对于每次询问 \(x\) 的 \(k\) 级祖先:
利用倍增数组先将 \(x\) 跳到最大的 \(2^c\) 级祖先。
设剩下还有 \(k'\)。
由刚刚的性质,此时 \(x\) 所在长链长度 \(\ge 2^c>k'\)。
因此可以先将 \(x\) 跳到 \(x\) 所在链的顶点,若之后剩下的级数为正,则利用向上的数组求出答案,否则利用向下的数组求出答案。
复杂度为 \(O(n\log n)−O(1)\)。
点击查看代码
/*
* Author: ShaoJia
* Create Time: 2022-09-01 21:12:55
* Last Modified time: 2022-09-01 21:53:13
* Motto: We'll be counting stars.
*/
//#pragma GCC optimize("Ofast")
#include<bits/stdc++.h>
using namespace std;
#define fir first
#define sec second
#define mkp make_pair
#define pb emplace_back
#define For(i,j,k) for(int i=(j),i##_=(k);i<=i##_;i++)
#define Rof(i,j,k) for(int i=(j),i##_=(k);i>=i##_;i--)
#define ckmx(a,b) a=max(a,b)
#define ckmn(a,b) a=min(a,b)
#define debug(...) cerr<<"#"<<__LINE__<<": "<<__VA_ARGS__<<endl
#define ui unsigned int
ui S;
inline ui get(ui x) {
x ^= x << 13;
x ^= x >> 17;
x ^= x << 5;
return S = x;
}
//-------------------------
#define C 18
#define N 500010
int n,q,root,dep[N],md[N],wson[N],top[N],f[N][C+1],lg[N];
vector<int> e[N],up[N],dn[N];
void dfs(int rt,int fa){
f[rt][0]=fa;
For(i,1,C) f[rt][i]=f[f[rt][i-1]][i-1];
md[rt]=dep[rt]=dep[fa]+1;
for(int i:e[rt]){
dfs(i,rt);
ckmx(md[rt],md[i]);
if(md[wson[rt]]<md[i]) wson[rt]=i;
}
}
void dfs2(int rt,int tp){
top[rt]=tp;
if(rt==tp){
int x=rt;
For(i,0,md[rt]-dep[rt]){
up[rt].pb(x);
x=f[x][0];
}
x=rt;
For(i,0,md[rt]-dep[rt]){
dn[rt].pb(x);
x=wson[x];
}
}
if(wson[rt]) dfs2(wson[rt],tp);
for(int i:e[rt]) if(i!=wson[rt]) dfs2(i,i);
}
int que(int x,int y){
if(!y) return x;//0 dont have lg
x=f[x][lg[y]];
y-=1<<lg[y];
y-=dep[x]-dep[top[x]];
x=top[x];
return y>0?up[x][y]:dn[x][-y];
}
signed main(){ios::sync_with_stdio(false),cin.tie(nullptr);
cin>>n>>q>>S;
For(i,2,n) lg[i]=lg[i>>1]+1;
int x,k,ans=0;
long long out=0;
For(i,1,n){
cin>>x;
if(x) e[x].pb(i);
else root=i;
}
dfs(root,0);
dfs2(root,root);
For(i,1,q){
x=(get(S)^ans)%n+1;
k=(get(S)^ans)%dep[x];
out^=(long long)i*(ans=que(x,k));
}
cout<<out<<endl;
return 0;}
P3899 [湖南集训]更为厉害
长链剖分优化 DP。
时空 \(O(n)\)。
相当于按 \(mxdep\) 启发式合并(?)
点击查看代码
/*
* Author: ShaoJia
* Last Modified time: 2022-09-02 10:54:28
* Motto: We'll be counting stars.
*/
#include<bits/stdc++.h>
using namespace std;
#define pb emplace_back
#define For(i,j,k) for(int i=(j),i##_=(k);i<=i##_;i++)
#define Rof(i,j,k) for(int i=(j),i##_=(k);i>=i##_;i--)
#define ll long long
#define pi pair<int,int>
const int N=300010;
int n,q,dep[N],wson[N],md[N],sz[N];
ll ans[N],*f[N];//f: dp's sufsum
vector<int> e[N];
vector<pi> g[N];
void dfs(int rt,int fa){
dep[rt]=dep[fa]+1;
sz[rt]=1;
for(int i:e[rt]) if(i!=fa){
dfs(i,rt);
sz[rt]+=sz[i];
if(md[i]>md[wson[rt]]) wson[rt]=i;
}
if(wson[rt]) md[rt]=md[wson[rt]];
else md[rt]=dep[rt];
}
void work(int rt,int fa,int tp){
if(rt==tp) f[rt]=new ll[md[rt]-dep[rt]+1];//top
f[rt][0]=sz[rt]-1;
if(wson[rt]){
f[wson[rt]]=f[rt]+1;
work(wson[rt],rt,tp);
f[rt][0]+=f[wson[rt]][0];
}
for(int i:e[rt]) if(i!=fa && i!=wson[rt]){
work(i,rt,i);
For(j,0,md[i]-dep[i]) f[rt][j+1]+=f[i][j];
f[rt][0]+=f[i][0];
}
for(auto [k,x]:g[rt]){
ans[x]=(ll)(sz[rt]-1)*min(dep[rt]-1,k);//b is anc of a
ans[x]+=f[rt][0]-(sz[rt]-1);
if(k<md[rt]-dep[rt]) ans[x]-=f[rt][k+1];//sufsum get range sum
}
}
signed main(){ios::sync_with_stdio(false),cin.tie(nullptr);
cin>>n>>q;
int x,y;
For(i,1,n-1){
cin>>x>>y;
e[x].pb(y);
e[y].pb(x);
}
dfs(1,0);
For(i,1,q){
cin>>x>>y;
g[x].pb(y,i);
}
work(1,0,1);
For(i,1,q) cout<<ans[i]<<endl;
return 0;}
CF570D Tree Requests
跳坑:传 wson 时忘记 f[wson[rt]]=f[rt]+1;
,求答案时忘记判 k>md[rt]-dep[rt]
。
CF526G Spiders Evil Plan
复合题。
题意简述
给定一棵 \(n\) 个节点的无根树,每条边有边权。
有 \(q\) 次询问,每次询问给出 \(x,y\),你需要选择 \(y\) 条树上的路径,使这些路径形成一个包含 \(x\) 的连通块,且连通块中包含的边 权和最大。
强制在线。
- \(1 \le n, q \le 10^5\)。
题解
可以证明:使用 \(k\) 条路径就可以覆盖一棵有 \(2 k\) 的叶子的树。
先以任意方式匹配叶子。如果有两条路径不相交,可以调整成相交的情况。
不断调整就可以让任意两条路径都相交,于是显然覆盖了整棵树。
(证明不严谨,因为没有说明调整能在有限步内结束,不过这不重要)
所以当询问 \(y\) 的时候,就是要在原树中选取不超过 \(2 y\) 个叶子,让这些叶子组成的极小连通块的边权和尽量大。
再考虑:每次询问中,一定存在一种方案使得直径的两端中至少有一端被选取。
那么我们以两个直径端点为根,每次询问在两棵树中分别查询即可。
那么,现在根是一个叶子(直径端点必然是叶子),且根必选。
也就是说,需要选其它至多 \(2 y - 1\) 个叶子,打通他们到根的链,并且最大化边权和。
考虑带边权的长链剖分,发现这和选取过程是等价的,也就是贪心地选取前 \(2 y - 1\) 个最长链即可。
但是选完之后不一定经过 \(x\),所以需要做一下调整。
首先打通 \(x\) 子树中最深的点到根的路径,然后需要去掉另一个叶子,使得减小量尽量小。
可以发现,要不然是删掉第 \(2 y - 1\) 个最长链,也就是仅选取前 \(2 y - 2\) 个最长链,然后把 \(x\) 接上。
要不然就是在选取前 \(2 y - 1\) 个最长链的基础上,先把 \(x\) 接上,然后删去第一个碰到的点的其它子树。
最优解,一定符合这两种情况之一,且不会统计到错误的情况(叶子数都不超过 \(2 y - 1\)),所以是正确的。
向上跳的过程可以倍增,但是我喜欢省空间,写了一个重剖。
时间复杂度为 \(O ((n + q) \log n)\),空间倍增 \(O(n\log n)\),树剖 \(O(n)\)。
CF1009F Dominant Indices
本文来自博客园,作者:ShaoJia,版权归作者所有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。