长链剖分记
对于一颗
长儿子:一个结点的所有儿子中,子树深度最大的儿子。
长链:连续的长儿子组成的链。
使用两次 dfs,第一次处理结点深度、长儿子等信息,第二次处理长链信息、dfn 序等。注意第二次搜索先走长儿子。
预处理可以简单做到
- 所有长链长度之和为
\mathcal{O}(n) 。长链之间互不相交。长链的末端是叶子结点。 - 对于一个结点
x 的k 级祖先y ,y 所在长链的长度一定\ge k 。此时y 一定存在长度=k 的链,如果这条链不作为长链,则长链长度>k ,否则=k 。 - 我们也可以知道,对于同一条祖先链上的长链链头
e_1,e_2 ,\ldots e_k 及其对应的长链长度L_1,L_2 ,\ldots ,L_k ,若\mathrm{dep}(e_i)<\mathrm{dep}(e_j) ,则有L_i>L_j 。有边权时同样成立。 - 从任意一个结点
x 开始,不停向上跳长链,最多跳\mathcal{O}(\sqrt{n}) 次。因为x 每跳一次长链,x 所在长链的长度最少+1 (这不代表每次跳长链的距离一定会越来越大),又因为长链之间互不相交,所以假如我们跳了c 次长链,一定有n \ge \sum _{i=1}^{c}i=\mathcal{O}(c^2) 。故c 的上界是\mathcal{O}(\sqrt{n}) 。更加一般的,假如长链剖分的树上有边权,对树进行带权的长链剖分,记边权分别为w_1,w_2,\ldots ,w_{n-1} ,我们可以得到c 的上界为\mathcal{O}\left (\sqrt{\sum_{i=1}^{n-1}\limits w_i}\right ) ,这个和上面的推导方式相同。 - 与重链剖分相同,在一条长链上的点 dfn 序连续。
- 在边有边权时(意思是深度也带权),长链剖分也可以维护一类贪心。具体而言,如果我们要选出
k 个点,最大化这k 个点到根路径的并的边权和时,答案为前k 长的长链之和,此时选择的k 个点为前k 长的长链的末端。这个比较容易感性理解。
例题
给定一颗
n 个结点的树,有q 次查询。每次给定x,k ,求x 的k 级祖先。
使用树上倍增或重链剖分可以分别做到
利用长链剖分,我们可以做到
先进行树上倍增的预处理,处理出每个
对于一次询问而言,对于
此时我们还有
对于每条长链,若其长度为
因为所有长链长度之和为
因为
这样单次查询的时间复杂度就是严格
#include<bits/stdc++.h>
#define rd read()
#define gc getchar()
using namespace std;
inline int read(){
register int x=0,ss=1,s=gc;
while(!isdigit(s)&&s!='-')s=gc;if(s=='-')ss=-1,s=gc;
while(isdigit(s))x=(x<<1)+(x<<3)+(s^48),s=gc;
return ss*x;
}
const int N=500005;
unsigned s;long long out;
inline unsigned get(unsigned x) {
x^=x<<13,x^=x>>17,x^=x<<5;
return s=x;
}
int n,q,rt,cnt;
int dep[N],mx[N],son[N],dfn[N],rev[N],top[N],lg[N],f[20][N];
vector<int> v[N],up[N];
inline void dfs(int x){
mx[x]=dep[x];
for(int i:v[x])
dep[i]=dep[x]+1,dfs(i),
(mx[i]>mx[son[x]])&&(mx[x]=mx[i],son[x]=i);
}
inline void dfs2(int x,int t){
dfn[rev[++cnt]=x]=cnt,top[x]=t;
if(x==t)
for(int h=x,i=0;i<=mx[x]-dep[x];h=f[0][h],++i)
up[x].push_back(h);
if(!son[x])return;dfs2(son[x],t);
for(int i:v[x])if(i!=son[x])dfs2(i,i);
}
inline int Q(int x,int k){
if(!k)return x;
x=f[lg[k]][x],k-=1<<lg[k];
int t=dep[x]-dep[top[x]];
if(t<k)return up[top[x]][k-t];
return rev[dfn[top[x]]+t-k];
}
signed main(){
n=rd,q=rd,cin>>s;
for(int i=1;i<=n;++i){
v[f[0][i]=rd].push_back(i);
if(!f[0][i])rt=i;
}
for(int i=2;i<=n;++i)lg[i]=lg[i>>1]+1;
for(int i=1;i<=lg[n];++i)
for(int j=1;j<=n;++j)f[i][j]=f[i-1][f[i-1][j]];
dep[rt]=1,dfs(rt),dfs2(rt,rt);
for(int i=1,res=0,x,k;i<=q;++i)
x=(get(s)^res)%n+1,k=(get(s)^res)%dep[x],
out^=1ll*i*(res=Q(x,k));
cout<<out<<'\n';return 0;
}
例题
给定一颗
n 个结点的树。对于每个结点,求出其子树中出现次数最多的最小深度。
记
容易得到
可以直接线段树合并做到时空
不妨仿照 dsu on tree,在处理
不难发现直接继承长儿子的信息最优,因为长儿子的
考虑此时的复杂度。对于一条长链的信息,我们会在长链顶端向父亲合并的时候暴力合并,长链中间都是直接继承。而暴力合并时,子树中的最大深度刚好是长链的长度
故算法的时空复杂度为
#include<bits/stdc++.h>
#define rd read()
#define gc getchar()
using namespace std;
inline int read(){
register int x=0,ss=1,s=gc;
while(!isdigit(s)&&s!='-')s=gc;if(s=='-')ss=-1,s=gc;
while(isdigit(s))x=(x<<1)+(x<<3)+(s^48),s=gc;
return ss*x;
}
const int N=1000005;
int n,mx[N],dep[N],son[N],ans[N],e[N];vector<int> v[N],dp[N];
inline void dfs(int x,int f){
mx[x]=dep[x];
for(int i:v[x])if(i!=f)
dep[i]=dep[x]+1,dfs(i,x),
(mx[i]>mx[son[x]])&&(mx[x]=mx[i],son[x]=i);
}
inline void sol(int x,int f){
if(!son[x])return dp[x].push_back(1);
sol(son[x],x),swap(dp[x],dp[son[x]]);
dp[x].push_back(1);//实现上为方便追加,将数组倒序了
e[x]=dp[x].size()-1,ans[x]=ans[son[x]]+1;
if(dp[x][e[x]-ans[x]]==1)ans[x]=0;
for(int i:v[x])if(i!=f&&i!=son[x]){
sol(i,x);
for(int j=0;j<=e[i];++j)
dp[x][e[x]-j-1]+=dp[i][e[i]-j],
(dp[x][e[x]-j-1]>dp[x][e[x]-ans[x]]||
dp[x][e[x]-j-1]==dp[x][e[x]-ans[x]]&&ans[x]>j+1)&&(ans[x]=j+1);
}
}
signed main(){
n=rd;
for(int i=1,x,y;i<n;++i)
x=rd,y=rd,
v[x].push_back(y),
v[y].push_back(x);
dfs(1,0),sol(1,0);
for(int i=1;i<=n;++i)cout<<ans[i]<<'\n';
return 0;
}
例题
给出一棵有
n 个点的树,求有多少组点(i,j,k) 满足i,j,k 两两之间的距离都相等。(i,j,k) 与(i,k,j) 算作同一组。
与弱化版本在三个结点间的路径的交点处计数不同,考虑在三个点
记录
为什么这样设呢,假如要基于
计算
合并的时候再考虑如何利用
直接计算可以做到
复杂度与上题相同,时空均为
#include<bits/stdc++.h>
#define int long long
#define rd read()
#define gc pa == pb && (pb = (pa = buf) + fread(buf, 1, 100000, stdin), pa == pb) ? EOF : *pa++
using namespace std;
static char buf[100000], * pa(buf), * pb(buf);
inline int read(){
register int x=0,ss=1,s=gc;
while(!isdigit(s)&&s!='-')s=gc;if(s=='-')ss=-1,s=gc;
while(isdigit(s))x=(x<<1)+(x<<3)+(s^48),s=gc;
return ss*x;
}
const int N=100005;
int n,ans,mx[N],son[N],dep[N],ln[N];vector<int> v[N];
int *f[N],*g[N],o[N<<2],*p=o;
inline void dfs(int x,int f){
mx[x]=dep[x]=dep[f]+1;
for(int i:v[x])if(i!=f){
dfs(i,x);
if(mx[i]>mx[son[x]])son[x]=i,mx[x]=mx[i];
}
ln[x]=mx[x]-dep[x]+1;
}
inline void sol(int x,int F){
if(son[x])
f[son[x]]=f[x]+1,g[son[x]]=g[x]-1,
sol(son[x],x);
f[x][0]=1,ans+=g[x][0];
for(int i:v[x])if(i!=F&&i!=son[x]){
f[i]=p,p+=ln[i]<<1,g[i]=p,p+=ln[i]<<1,sol(i,x);
for(int j=0;j<ln[i];++j)
ans+=f[x][j]*g[i][j+1]+g[x][j+1]*f[i][j];
for(int j=0;j<ln[i];++j){
g[x][j+1]+=f[x][j+1]*f[i][j];
f[x][j+1]+=f[i][j];
if(j)g[x][j-1]+=g[i][j];
}
}
}
signed main(){
n=rd;
for(int i=1,x,y;i<n;++i)
x=rd,y=rd,
v[x].push_back(y),
v[y].push_back(x);
dfs(1,0),f[1]=p,p+=ln[1]<<1,
g[1]=p,p+=ln[1]<<1,sol(1,0);
cout<<ans<<'\n';return 0;
}
例题
给定一棵
n 个结点的树,边有边权。q 次查询,每次查询给出x,y ,在树上选y 条路径,求路径之并包含x 且是连通块时的最大边权之和。强制在线。
这题神了。
是否包含
显然的,对于一棵有
又观察到,因为包含
考虑分别令两个直径端点为根,强制选择根,答案取两种情况的最大值,考虑其中一种即可。
选了根后剩下的位置,我们还可以选择
则我们的问题转换为了选
假如没有
但是为了让路径之间联通,
- 找到
x 上方第一个所在长链排名\le 2y-1 的点,将其长链的后半截拼到x 这边。也就是将这条链替换为x 所在的长链。根据性质 3. 可以知道我们替换其他的长链一定不优 - 删去排名为
(2y-1) 的长链,然后向上接上x 所在长链。该链的贡献是x 到其上方第一个所在长链排名\le 2y-2 的点构成的路径。这种情况是在情形 1. 可以找到的长链较优,所以我们在其他无关紧要的地方删去了一条长链。
两种情形都可以使用树上倍增维护,时间复杂度为
#include<bits/stdc++.h>
#define rd read()
#define gc pa == pb && (pb = (pa = buf) + fread(buf, 1, 100000, stdin), pa == pb) ? EOF : *pa++
using namespace std;
static char buf[100000], * pa(buf), * pb(buf);
inline int read(){
register int x=0,ss=1,s=gc;
while(!isdigit(s)&&s!='-')s=gc;if(s=='-')ss=-1,s=gc;
while(isdigit(s))x=(x<<1)+(x<<3)+(s^48),s=gc;
return ss*x;
}
const int N=100005;
int n,q,mx,m1,m2,de[N],lg[N];
vector<pair<int,int> > v[N];
struct solver{
int cnt,mx[N],fa[N],dep[N],son[N],top[N];
int s[N],sm[N],rk[N],F[19][N];
pair<int,int> nu[N];
inline void dfs(int x,int f){
mx[x]=dep[x],fa[x]=f;
for(auto [t,w]:v[x])if(t!=f)
dep[t]=dep[x]+w,dfs(t,x),
(mx[t]>mx[x])&&(mx[x]=mx[son[x]=t]);
}
inline void dfs2(int x,int t){
top[x]=t,s[t]=max(s[t],dep[x]-dep[fa[t]]);
if(!son[x])return;dfs2(son[x],t);
for(auto [u,w]:v[x])
if(u!=fa[x]&&u!=son[x])dfs2(u,u);
}
inline void B(int rt){
dfs(rt,0),dfs2(rt,rt);
for(int i=1;i<=n;++i)if(s[i])nu[++cnt]={-s[i],i};
stable_sort(nu+1,nu+cnt+1);
for(int i=1;i<=cnt;++i)rk[nu[i].second]=i;
for(int i=1;i<=n;++i)rk[i]=rk[top[i]],F[0][i]=fa[i],s[i]=s[top[i]];
for(int i=1;i<=lg[n];++i)
for(int j=1;j<=n;++j)F[i][j]=F[i-1][F[i-1][j]];
for(int i=1;i<=cnt;++i)sm[i]=sm[i-1]-nu[i].first;
}
inline int Q(int x,int y){
y=(y<<1)-1;int p=x,P=x;
if(rk[x]<=y)return sm[min(y,cnt)];
for(int i=18;~i;--i)
(rk[F[i][p]]>=y)&&(p=F[i][p]),
(rk[F[i][P]]>y)&&(P=F[i][P]);
return mx[x]+max(sm[y-1]-dep[fa[p]],sm[y]-mx[fa[P]]);
}
}F,G;
inline void dfs(int x,int f){
if(de[x]>de[m1])m1=x;
for(auto [t,w]:v[x])
if(t!=f)de[t]=de[x]+w,dfs(t,x);
}
signed main(){
for(int i=2;i<N;++i)lg[i]=lg[i>>1]+1;
n=rd,q=rd;
for(int i=1,x,y,l;i<n;++i)
x=rd,y=rd,l=rd,
v[x].push_back({y,l}),
v[y].push_back({x,l});
dfs(1,0),de[m2=m1]=0,dfs(m1,0),
F.B(m1),G.B(m2);
for(int i=1,x,y,lt=0;i<=q;++i)
x=(rd+lt-1)%n+1,y=(rd+lt-1)%n+1,
cout<<(lt=max(F.Q(x,y),G.Q(x,y)))<<'\n';
return 0;
}