长链剖分

O(n) 时空解决部分 dsu on tree / 线段树合并 / 倍增 O(nlogn) 时空的问题。

长链剖分的机制是选子树内深度最大的儿子作为长儿子(类比重链剖分的重儿子)。

然后有几个性质:

  1. 一条链向上跳一条链,则链上最大深度不减。

  2. 由 1 可知任何一个点向上跳链次数不会超过 n 次。(但是这个性质不是很有用)。

  3. 任意一个点 xk 次祖先 y 所在的长链的长度大于等于 k,因为 xy 长度为 k,长儿子必然更长。

P5903 【模板】树上 k 级祖先

对树进行长链剖分,记录每个点所在链的顶点和深度。

树上倍增求出每个点的 2c 级祖先。

对于每条链,如果其长度为 len,那么在顶点处记录顶点向上的 len 个祖先和向下的 len 个链上的儿子。

预处理出每个数的二进制最高位。

对于每次询问 xk 级祖先:

利用倍增数组先将 x 跳到最大的 2c 级祖先。

设剩下还有 k

由刚刚的性质,此时 x 所在长链长度 2c>k

因此可以先将 x 跳到 x 所在链的顶点,若之后剩下的级数为正,则利用向上的数组求出答案,否则利用向下的数组求出答案。

复杂度为 O(nlogn)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 的连通块,且连通块中包含的边 权和最大。

强制在线。

  • 1n,q105

题解

可以证明:使用 k 条路径就可以覆盖一棵有 2k 的叶子的树。

先以任意方式匹配叶子。如果有两条路径不相交,可以调整成相交的情况。

不断调整就可以让任意两条路径都相交,于是显然覆盖了整棵树。

(证明不严谨,因为没有说明调整能在有限步内结束,不过这不重要)

所以当询问 y 的时候,就是要在原树中选取不超过 2y 个叶子,让这些叶子组成的极小连通块的边权和尽量大。

再考虑:每次询问中,一定存在一种方案使得直径的两端中至少有一端被选取。

那么我们以两个直径端点为根,每次询问在两棵树中分别查询即可。

那么,现在根是一个叶子(直径端点必然是叶子),且根必选。

也就是说,需要选其它至多 2y1 个叶子,打通他们到根的链,并且最大化边权和。

考虑带边权的长链剖分,发现这和选取过程是等价的,也就是贪心地选取前 2y1 个最长链即可。

但是选完之后不一定经过 x,所以需要做一下调整。

首先打通 x 子树中最深的点到根的路径,然后需要去掉另一个叶子,使得减小量尽量小。

可以发现,要不然是删掉第 2y1 个最长链,也就是仅选取前 2y2 个最长链,然后把 x 接上。

要不然就是在选取前 2y1 个最长链的基础上,先把 x 接上,然后删去第一个碰到的点的其它子树。

最优解,一定符合这两种情况之一,且不会统计到错误的情况(叶子数都不超过 2y1),所以是正确的。

向上跳的过程可以倍增,但是我喜欢省空间,写了一个重剖。

时间复杂度为 O((n+q)logn),空间倍增 O(nlogn),树剖 O(n)

倍增

树剖

再卡空间的树剖

CF1009F Dominant Indices

posted @   ShaoJia  阅读(84)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· 阿里巴巴 QwQ-32B真的超越了 DeepSeek R-1吗?
· 【译】Visual Studio 中新的强大生产力特性
· 【设计模式】告别冗长if-else语句:使用策略模式优化代码结构
· 10年+ .NET Coder 心语 ── 封装的思维:从隐藏、稳定开始理解其本质意义
点击右上角即可分享
微信分享提示