「Note」树论方向
1. 重链剖分
1.1. 简介
重链剖分将树分割成若干链维护信息,将树的结构转换为线性结构,然后可用其他数据结构维护。
定义以下概念:
重子节点 | 轻子节点 | 重边 | 轻边 | 重链 |
---|---|---|---|---|
某节点的子节点中子树大小最大的那个 | 某节点的子节点中的非重子节点 | 由某节点到其重子节点的连边 | 由某节点到其轻子节点的连边 | 若干条头尾相接的重边构成的链(落单的节点也看作一个重链) |
重链剖分有以下性质:
- 剖分时优先遍历重边,于是一条重链上的 DNF 序连续。
- 一颗子树内 DFN 序连续。
根据这两条性质我们可以考虑维两节点路径上的信息,与一个子树内的信息。路径上的信息可以用若干重链信息拼接而成维护,子树内则直接用一定范围内连续 DFN 的节点信息维护即可。
重链剖分同时可以解决求 LCA 问题,当然,这在维护路径上信息时就需要用到。对于两个节点,考虑将所在重链顶点深度大的节点跳转至其所在重链顶点的父节点,进行此操作若干次直到两者在一条重链上。最后一个一个跳转即可。
1.2. 具体实现
实现时,维护以下变量:
\(fa_i\) | \(dep_i\) | \(siz_i\) | \(hson_i\) | \(top_i\) | \(dfn_i\) | \(rk_i\) |
---|---|---|---|---|---|---|
节点 \(i\) 的父节点 | 节点 \(i\) 的深度 | 节点 \(i\) 的子树大小 | 节点 \(i\) 的重子节点 | 节点 \(i\) 所在重链顶点 | 节点 \(i\) 的 DFN 序 | DFN 序为 \(i\) 的节点编号 |
剖分操作需要进行两次 DFS:
- 第一次 DFS 维护 \(fa_i,dep_i,siz_i,hson_i\)。
void DFS1(int now,int fa)
{
p[now].fa=fa,p[now].dep=p[fa].dep+1,p[now].siz=1,p[now].hson=0;
for(int to,i=head[now];i;i=edge[i].nex)
{
to=edge[i].to;
if(to==fa)
continue;
DFS1(to,now);
p[now].siz+=p[to].siz;
if(!p[now].hson||p[to].siz>p[p[now].hson].siz)
p[now].hson=to;
}
return;
}
- 第二次 DFS 维护 \(top_i,dfn_i,rk_i\)。
void DFS2(int now,int top)
{
p[now].top=top,p[now].dfn=++dcnt,rk[dcnt]=now;
if(p[now].hson)
DFS2(p[now].hson,top);
for(int to,i=head[now];i;i=edge[i].nex)
{
to=edge[i].to;
if(to==p[now].fa||to==p[now].hson)
continue;
DFS2(to,to);
}
return;
}
对于路径维护,考虑在跳 LCA 的过程中每次跳转维护跳转路径,直到两节点跳至一条重链上,此时维护两点间路径即可。每次维护 \(dfn_y\to dfn_x\ (dep_x>dep_y)\)。
对于子树维护,直接维护 \(dfn_x\to dfn_x+siz_x-1\) 即可。
1.3. 题目
\(\color{royalblue}{P3384}\)
重链剖分模板。
$\text{Code}$:
#define LL long long
#define UN unsigned
#include<bits/stdc++.h>
using namespace std;
//--------------------//
const int MAXN=1e5+1,MAXM=2e5+1;
int n,m,R,Mod;
int pv[MAXN];
//----------//
//Edge
struct Edge
{
int to,nex;
}edge[MAXM];
int tot,head[MAXN];
void add(int from,int to)
{
edge[++tot].to=to;
edge[tot].nex=head[from];
head[from]=tot;
return;
}
//--------------------//
const int MAXTN=4e5+1;
struct Seg_Tree
{
struct Seg_Tree_Node
{
int l,r;
LL val,lazy;
}t[MAXTN];
int seq[MAXN];
int ls(int rt){return rt<<1;}
int rs(int rt){return rt<<1|1;}
void tag(int rt,LL val)
{
t[rt].lazy+=val,t[rt].lazy%=Mod;
t[rt].val+=(t[rt].r-t[rt].l+1)*val%Mod,t[rt].val%=Mod;
return;
}
void push_up(int rt)
{
t[rt].val=(t[ls(rt)].val+t[rs(rt)].val)%Mod;
return;
}
void push_down(int rt)
{
if(!t[rt].lazy)
return;
tag(ls(rt),t[rt].lazy);
tag(rs(rt),t[rt].lazy);
t[rt].lazy=0;
return;
}
void build(int rt,int l,int r)
{
t[rt].l=l,t[rt].r=r,t[rt].lazy=0;
if(l==r)
{
t[rt].val=seq[l]%Mod;
return;
}
int mid=l+r>>1;
build(ls(rt),l,mid);
build(rs(rt),mid+1,r);
push_up(rt);
return;
}
void change(int rt,int l,int r,LL val)
{
if(t[rt].l>=l&&t[rt].r<=r)
{
tag(rt,val);
return;
}
push_down(rt);
int mid=t[rt].l+t[rt].r>>1;
if(l<=mid)
change(ls(rt),l,r,val);
if(r>mid)
change(rs(rt),l,r,val);
push_up(rt);
return;
}
LL query(int rt,int l,int r)
{
if(t[rt].l>=l&&t[rt].r<=r)
return t[rt].val;
push_down(rt);
LL res=0;
int mid=t[rt].l+t[rt].r>>1;
if(l<=mid)
res+=query(ls(rt),l,r),res%=Mod;
if(r>mid)
res+=query(rs(rt),l,r),res%=Mod;
return res;
}
}T;
//--------------------//
//DPH
struct Poi
{
int fa,dep,siz,hson;
int top,dfn;
}p[MAXN];
int dcnt,rk[MAXN];
void DFS1(int now,int fa)
{
p[now].fa=fa,p[now].dep=p[fa].dep+1,p[now].siz=1,p[now].hson=0;
for(int to,i=head[now];i;i=edge[i].nex)
{
to=edge[i].to;
if(to==fa)
continue;
DFS1(to,now);
p[now].siz+=p[to].siz;
if(!p[now].hson||p[to].siz>p[p[now].hson].siz)
p[now].hson=to;
}
return;
}
void DFS2(int now,int top)
{
p[now].top=top,p[now].dfn=++dcnt,rk[dcnt]=now;
if(p[now].hson)
DFS2(p[now].hson,top);
for(int to,i=head[now];i;i=edge[i].nex)
{
to=edge[i].to;
if(to==p[now].fa||to==p[now].hson)
continue;
DFS2(to,to);
}
return;
}
void init()
{
for(int i=1;i<=n;i++)
T.seq[p[i].dfn]=pv[i];
T.build(1,1,n);
return;
}
void changer(int x,LL val)
{
T.change(1,p[x].dfn,p[x].dfn+p[x].siz-1,val);
return;
}
LL queryr(int x)
{
return T.query(1,p[x].dfn,p[x].dfn+p[x].siz-1);
}
void changep(int x,int y,LL val)
{
while(p[x].top!=p[y].top)
{
if(p[p[x].top].dep<p[p[y].top].dep)
swap(x,y);
T.change(1,p[p[x].top].dfn,p[x].dfn,val);
x=p[p[x].top].fa;
}
if(p[x].dep<p[y].dep)
swap(x,y);
T.change(1,p[y].dfn,p[x].dfn,val);
return;
}
LL queryp(int x,int y)
{
LL res=0;
while(p[x].top!=p[y].top)
{
if(p[p[x].top].dep<p[p[y].top].dep)
swap(x,y);
res+=T.query(1,p[p[x].top].dfn,p[x].dfn),res%=Mod;
x=p[p[x].top].fa;
}
if(p[x].dep<p[y].dep)
swap(x,y);
res+=T.query(1,p[y].dfn,p[x].dfn),res%=Mod;
return res;
}
//--------------------//
int main()
{
scanf("%d%d%d%d",&n,&m,&R,&Mod);
for(int i=1;i<=n;i++)
scanf("%d",&pv[i]);
for(int from,to,i=1;i<n;i++)
scanf("%d%d",&from,&to),add(from,to),add(to,from);
DFS1(R,0);
DFS2(R,R);
init();
LL val;
for(int op,x,y,i=1;i<=m;i++)
{
scanf("%d%d",&op,&x);
if(op==1)
{
scanf("%d%lld",&y,&val);
changep(x,y,val%Mod);
}
else if(op==2)
{
scanf("%d",&y);
printf("%lld\n",queryp(x,y));
}
else if(op==3)
{
scanf("%lld",&val);
changer(x,val%Mod);
}
else if(op==4)
printf("%lld\n",queryr(x));
}
return 0;
}
\(\color{blueviolet}{SP6779}\)
重链剖分,求路径上最大子段和。
显著地,线段树维护以下信息:
- 区间和 \(sum\)。
- 区间最大子段和 \(mx\)。
- 区间左侧最大子段和 \(lmx\)。
- 区间右侧最大子段和 \(rmx\)。
合并时需要注意左右顺序,尤其是在树上合并链时。
$\text{Code}$:
#define LL long long
#define UN unsigned
#include<bits/stdc++.h>
using namespace std;
//--------------------//
const int N=2e5+1,M=4e5+1;
int n,q;
//--------------------//
//Seg-Tree
const int TN=8e5+1;
struct Seg_Tree_Node
{
int l,r;
int sum,mx,lmx,rmx;
int lazy;
Seg_Tree_Node(){l=r=sum=mx=lmx=rmx=0,lazy=10001;}
};
struct Seg_Tree
{
Seg_Tree_Node t[TN];
int s[N];
int ls(int rt){return rt<<1;}
int rs(int rt){return rt<<1|1;}
void tag(int rt,int val)
{
t[rt].lazy=val;
t[rt].sum=(t[rt].r-t[rt].l+1)*val;
t[rt].mx=t[rt].lmx=t[rt].rmx=max(0,t[rt].sum);
return;
}
Seg_Tree_Node merge(Seg_Tree_Node x,Seg_Tree_Node y,int type)
{
if(!y.l)
swap(x,y);
if(!x.l)
{
if(type==1)
y.r-=y.l-1,y.l=1;
return y;
}
Seg_Tree_Node res;
res.sum=x.sum+y.sum;
res.lmx=max(x.lmx,x.sum+y.lmx);
res.rmx=max(y.rmx,y.sum+x.rmx);
res.mx=max(x.rmx+y.lmx,max(x.mx,y.mx));
if(type==1)
res.l=1,res.r=x.r+y.r-x.l-y.l+2;
else
res.l=x.l,res.r=y.r;
return res;
}
void push_down(int rt)
{
if(t[rt].lazy==10001)
return;
tag(ls(rt),t[rt].lazy),tag(rs(rt),t[rt].lazy);
t[rt].lazy=10001;
return;
}
void build(int rt,int l,int r)
{
t[rt].l=l,t[rt].r=r,t[rt].lazy=10001;
if(l==r)
{
t[rt].sum=s[l];
t[rt].mx=t[rt].lmx=t[rt].rmx=max(0,t[rt].sum);
return;
}
int mid=l+r>>1;
build(ls(rt),l,mid);
build(rs(rt),mid+1,r);
t[rt]=merge(t[ls(rt)],t[rs(rt)],0);
return;
}
void change(int rt,int l,int r,int val)
{
if(t[rt].l>=l&&t[rt].r<=r)
{
tag(rt,val);
return;
}
push_down(rt);
int mid=t[rt].l+t[rt].r>>1;
if(l<=mid)
change(ls(rt),l,r,val);
if(r>mid)
change(rs(rt),l,r,val);
t[rt]=merge(t[ls(rt)],t[rs(rt)],0);
return;
}
Seg_Tree_Node query(int rt,int l,int r)
{
if(t[rt].l>=l&&t[rt].r<=r)
return t[rt];
push_down(rt);
int mid=t[rt].l+t[rt].r>>1;
Seg_Tree_Node res;
if(l<=mid)
res=query(ls(rt),l,r);
if(r>mid)
res=merge(res,query(rs(rt),l,r),1);
t[rt]=merge(t[ls(rt)],t[rs(rt)],0);
return res;
}
}T;
//--------------------//
//Graph
struct Edge
{
int to,nex;
}edge[M];
int tot,head[N];
void add(int from,int to)
{
edge[++tot].to=to;
edge[tot].nex=head[from];
head[from]=tot;
return;
}
struct Poi
{
int fa,dep,siz,hson;
int top,dfn;
int val;
}p[N];
int dcnt,rk[N];
void DFS1(int now,int fa)
{
p[now].fa=fa,p[now].dep=p[fa].dep+1,p[now].siz=1;
for(int to,i=head[now];i;i=edge[i].nex)
{
to=edge[i].to;
if(to==fa)
continue;
DFS1(to,now);
p[now].siz+=p[to].siz;
if(!p[now].hson||p[p[now].hson].siz<p[to].siz)
p[now].hson=to;
}
return;
}
void DFS2(int now,int top)
{
p[now].top=top,p[now].dfn=++dcnt,rk[dcnt]=now;
if(p[now].hson)
DFS2(p[now].hson,top);
for(int to,i=head[now];i;i=edge[i].nex)
{
to=edge[i].to;
if(to==p[now].hson||to==p[now].fa)
continue;
DFS2(to,to);
}
return;
}
void init()
{
DFS1(1,0);
DFS2(1,1);
for(int i=1;i<=n;i++)
T.s[p[i].dfn]=p[i].val;
T.build(1,1,n);
return;
}
void change(int x,int y,int val)
{
while(p[x].top!=p[y].top)
{
if(p[p[x].top].dep<p[p[y].top].dep)
swap(x,y);
T.change(1,p[p[x].top].dfn,p[x].dfn,val);
x=p[p[x].top].fa;
}
if(p[x].dep<p[y].dep)
swap(x,y);
T.change(1,p[y].dfn,p[x].dfn,val);
return;
}
int query(int x,int y)
{
Seg_Tree_Node resx,resy;
while(p[x].top!=p[y].top)
{
if(p[p[x].top].dep<p[p[y].top].dep)
swap(x,y),swap(resx,resy);
resx=T.merge(T.query(1,p[p[x].top].dfn,p[x].dfn),resx,1);
x=p[p[x].top].fa;
}
if(p[x].dep<p[y].dep)
swap(x,y),swap(resx,resy);
resx=T.merge(T.query(1,p[y].dfn,p[x].dfn),resx,1);
swap(resx.lmx,resx.rmx);
resx=T.merge(resx,resy,1);
return resx.mx;
}
//--------------------//
int main()
{
scanf("%d",&n);
for(int i=1;i<=n;i++)
scanf("%d",&p[i].val);
for(int from,to,i=1;i<n;i++)
scanf("%d%d",&from,&to),add(from,to),add(to,from);
init();
scanf("%d",&q);
for(int op,x,y,z,i=1;i<=q;i++)
{
scanf("%d%d%d",&op,&x,&y);
if(op==1)
printf("%d\n",query(x,y));
else
{
scanf("%d",&z);
change(x,y,z);
}
}
return 0;
}
\(\color{blueviolet}{P2486}\)
路径上颜色段数,维护信息与上题类似,注意合并细节。
2. 点分治
2.1. 静态点分治
适用于离线处理树上路径问题。
任选一个根,任意一条路径都只有两种状态:完全在此点某子节点子树内、经过根并两端在其子节点子树内(或端点为根)。
对于每一个路径,我们考虑其端点 LCA 节点统计其贡献。所以可以考虑每次找一个点统计其贡献,并把其删掉,统计剩余森林贡献,以此类推。
但这样复杂度仍然不够优秀,我们可以每次选择重心进行分治,这样就可以做到 \(O(n \log n)\) 分治。
具体地,对于当前子树,我们找出其重心作为根,统计其各个子树信息进行合并与统计(不能经过打标记即删除了的节点),然后再处理其各个子节点子树,求解子问题。
特别地,每次寻找重心之后需要更新各个节点子树大小,否则复杂度会有问题。
需要注意的是每次找重心都需要更新当前处理的子树大小,否则复杂度仍然不对。
2.2. 动态点分治
考虑如果有多次询问,如果每次都进行静态点分治那么复杂度我们将不能接受。而多次询问其中分治过程都相同,有大量重复计算。于是我们可以记录分治树,其形态固定,将每一层分治重心与上一层的相连,得到一颗重构树,我们称为点分树。
点分树的性质:
- 它是一棵有根树。
- 树深最多是 \(O(\log n)\) 级别的。
- 树上每个节点度数不大于其在原树上的度数 \(+ 1\),
- 对于 \(u, v\) 两点,其点分树上的 LCA 一定在原树的两点间路径上。
- 设 \(siz_u\) 表示 \(u\) 节点在点分树上的子树大小,有 \(\sum \limits_{u \in T} siz_u\) 是 \(O(n \log n)\) 级别的。
- 点分树上,节点 \(u\) 的子节点 \(v\) 的子树中与原树中对应的节点 \(u\) 子节点子树的点编号相同,树的形态不一定相同。
与点分治类似的,动态点分治也用于解决路径问题;不同的是动态点分治一般是多次查询,每次固定一个端点。
考虑一个点在分治过程中被计入贡献,当且仅当分治中心是其点分树上祖先或者是其本身。所以处理一个点的询问时从其本身出发,枚举其祖先作为此点与其他点的 LCA 进行处理。
特别地,处理当前点贡献时,可能会处理到与父节点重复的贡献,此时用类似容斥的方式减去贡献即可。总之处理点分树某点子树内贡献时需要减去不合法贡献。
接下来以模板题举例。
给定一棵树,每个点有点权。\(q\) 次询问给定 \(x, k\),求树上距离不超过 \(k\) 的所有点的点权和。带修点权,强制在线。\(n, q \le 10 ^ 5\)。
考虑建出点分树,并按上述方式枚举 LCA(分治点)。设 \(fa_u\) 表示节点 \(u\) 在点分树上的父节点,\(dis(u, v)\) 表示 \(u, v\) 两点在原树上的距离。\(dis(u, v)\) 可以 \(O(1)\) 求,只要预处理深度以及 ST 表求 LCA 即可。
初始我们在节点 \(x\) 上,令答案累加 \(\sum \limits_{dis(u, x) \le k} val_u\),然后令跳至 \(fa_x\),累计答案 \(\sum \limits_{dis(u, fa_x) \le k - dis(x, fa_x)} val_u\)。此时我们发现在记录 \(fa_x\) 的贡献过程中多计算了在 \(x\) 子树内的信息,所以令答案减去 \(\sum \limits_{u \in T_x \land dis(u, fa_x) \le k - dis(x, fa_x)} val_u\),以此类推。
所以我们对于一个节点 \(u\) 来说需要维护子树内与 \(u\) 节点不超过某一距离的权值和、子树内与 \(fa_u\) 节点不超过某一距离的权值和。这个东西可以用树状数组或者动态开点线段树解决,注意空间配置即可。
修改某一点显著只会影响点分树一条链上的点,暴力枚举修改即可。
2.3. 题目
\(\color{royalblue}{P3806}\)
静态点分治模板题,在分治过程中处理多组询问以减少常数。
类似于树形 DP,对于一次分治,处理当前正在遍历的子树信息,并且合并到当前树上来,以此类推。
特别地,计算结束需要反向撤销影响来进行下一步操作
$\text{Code}$:
#include <bits/stdc++.h>
using namespace std;
typedef long long LL;
typedef unsigned long long ULL;
typedef unsigned int UIT;
typedef double DB;
//--------------------//
const int N = 1e4 + 5, Q = 100 + 5, M = 2e4 + 5, K = 1e7 + 5;
int n, m, q[Q];
bool ans[Q];
struct Edge {
int to, w, nex;
} edge[M];
int tot, head[N];
void add(int from, int to, int w) {
edge[++tot] = {to, w, head[from]};
head[from] = tot;
}
int stot, siz[N], mxsiz[N];
bool vis[N];
int rt, val;
void find_g(int now, int fa) {
mxsiz[now] = siz[now] = 1;
for (int to, i = head[now]; i; i = edge[i].nex) {
to = edge[i].to;
if (to != fa && !vis[to])
find_g(to, now), siz[now] += siz[to], mxsiz[now] = max(mxsiz[now], siz[to]);
}
mxsiz[now] = max(mxsiz[now], stot - siz[now]);
if (rt && mxsiz[now] == val)
rt = min(rt, now);
if (!rt || mxsiz[now] < val)
rt = now, val = mxsiz[now];
}
int tcnt, tem[N], dep[N];
void calc(int now, int fa) {
tem[++tcnt] = dep[now];
for (int to, i = head[now]; i; i = edge[i].nex) {
to = edge[i].to;
if (to != fa && !vis[to])
dep[to] = dep[now] + edge[i].w, calc(to, now);
}
}
int rcnt, rec[N];
bool dis[K];
void divide(int now) {
dis[0] = true, rec[++rcnt] = 0;
vis[now] = true;
for (int to, i = head[now]; i; i = edge[i].nex) {
to = edge[i].to;
if (vis[to])
continue;
tcnt = 0, dep[to] = edge[i].w;
calc(to, now);
for (int j = 1; j <= m; j++)
for (int k = 1; k <= tcnt && !ans[j]; k++)
if (q[j] >= tem[k])
ans[j] |= dis[q[j] - tem[k]];
for (int j = 1; j <= tcnt; j++)
if (tem[j] <= 1e7)
dis[tem[j]] = true, rec[++rcnt] = tem[j];
}
while (rcnt)
dis[rec[rcnt]] = false, rcnt--;
for (int to, i = head[now]; i; i = edge[i].nex) {
to = edge[i].to;
if (vis[to])
continue;
stot = siz[to], rt = 0, val = 0;
find_g(to, 0);
int temp = rt, tempv = val;
find_g(rt, 0);
// if (rt != temp)
// printf("%d %d %d %d %d %d\n", temp, rt, mxsiz[temp], mxsiz[rt], stot, tempv), exit(0);
divide(rt);
}
}
//--------------------//
int main() {
// double t1 = clock();
scanf("%d%d", &n, &m);
for (int u, v, w, i = 1; i < n; i++)
scanf("%d%d%d", &u, &v, &w), add(u, v, w), add(v, u, w);
for (int i = 1; i <= m; i++)
scanf("%d", &q[i]);
stot = n;
find_g(1, 0);
int temp = rt;
find_g(rt, 0);
// if (temp != rt)
// printf("fuck"), exit(0);
divide(rt);
for (int i = 1; i <= m; i++)
puts(ans[i] ? "AYE" : "NAY");
// printf("%.0lfms", clock() - t1);
return 0;
}
\(\color{blueviolet}{P4178}\)
静态点分治,与其模板不同的是要求不超过 \(k\) 的路径数,拿树状数组维护即可。
\(\color{blueviolet}{P4149}\)
静态点分治,类似地,对于一定长度的路径,取边数量最小值即可。
\(\color{black}{P2664}\)
静态点分治。
考虑颜色对端点的贡献,首先特殊化根的答案以及贡献,这部分是简单的,然后考虑子树内节点的答案以及贡献。
设 \(f_x\) 表示节点 \(x\) 的贡献,对于一个节点 \(x\),若其祖先节点中没有和其同色的,有 \(f_x = siz_x\),否则 \(f_x = 0\)。
对于某点 \(x\),考虑一条 \(x\) 到 \(y\) 的路径,将其分为 \(x \to \mathrm{root}\) 以及 \(\mathrm{root} \to y\) 两部分分别考虑贡献。
对于前者,考虑此部分的每个颜色,都会在 \(y\) 取到不同的点的时候造成一次贡献,所以每个颜色会产生 \(siz_{\mathrm{root}} - siz_{\mathrm{branch}}\) 的贡献,其中 \(\mathrm{branch}\) 表示 \(x\) 所在的子树。
对于后者,考虑每个节点都会造成一次贡献,所以有贡献 \(\sum \limits _ {y \notin \mathrm{branch}} f_y\)。
考虑两部分贡献可能多算,因为颜色可能有重复,也就是说统计 \(\mathrm{root} \to y\) 部分贡献时只能累加不在 \(x \to \mathrm{root}\) 路径上的颜色的贡献,我们选择额外减去其贡献。
对于一个点 \(x\),一次计算中有其答案(设 \(x \to \mathrm{root}\) 路径上的颜色集合为 \(C\)):
其中 \(\sum \limits _ {y \notin \mathrm{branch}} f_y = \sum \limits_{i \in T_{\mathrm{root}}} f_i - \sum \limits_{i \in mathrm{branch}} f_i\),然后写巨大多 DFS 就可以解决了。
\(\color{blueviolet}{P6329}\)
动态点分治模板。
$\text{Code}$:
#include <bits/stdc++.h>
using namespace std;
typedef long long LL;
typedef unsigned long long ULL;
typedef unsigned int UIT;
typedef double DB;
//--------------------//
const int N = 1e5 + 5, M = 4e5 + 5;
int n, m, pv[N];
struct Edge {
int to, nex;
} edge[M];
int tot, head1[N], head2[N];
void add(int from, int to, int *head) {
edge[++tot] = {to, head[from]};
head[from] = tot;
}
int root, rt, rsiz;
int stot, siz[N], mxsiz[N];
bool vis[N];
void calc_g(int now, int fa) {
siz[now] = 1, mxsiz[now] = 0;
for (int to, i = head1[now]; i; i = edge[i].nex) {
to = edge[i].to;
if (to != fa && !vis[to])
calc_g(to, now), siz[now] += siz[to], mxsiz[now] = max(mxsiz[now], siz[to]);
}
mxsiz[now] = max(mxsiz[now], stot - siz[now]);
if (rt && mxsiz[now] == rsiz)
rt = min(rt, now);
if (!rt || mxsiz[now] < rsiz)
rt = now, rsiz = mxsiz[now];
}
void divide(int now) {
vis[now] = true;
for (int to, i = head1[now]; i; i = edge[i].nex) {
to = edge[i].to;
if (vis[to])
continue;
rt = rsiz = 0, stot = siz[to];
calc_g(to, 0);
calc_g(rt, 0);
add(now, rt, head2), add(rt, now, head2);
// printf("%d %d\n", now, rt);
divide(rt);
}
}
const int LG = 17 + 5;
int dcnt, dfn[N], dep[N];
int get(int x, int y) {return dfn[x] < dfn[y] ? x : y;}
struct ST {
int val[LG][N];
void init() {
int mxl = __lg(n);
for (int i = 1; i <= mxl; i++)
for (int j = 1; j + (1 << i) - 1 <= n; j++)
val[i][j] = get(val[i - 1][j], val[i - 1][j + (1 << (i - 1))]);
}
int query(int u, int v) {
if (u == v)
return u;
if ((u = dfn[u]) > (v = dfn[v]))
swap(u, v);
int lgl = __lg(v - u++);
return get(val[lgl][u], val[lgl][v - (1 << lgl) + 1]);
}
} S;
void DFS(int now, int fa) {
dfn[now] = ++dcnt, S.val[0][dfn[now]] = fa;
for (int to, i = head1[now]; i; i = edge[i].nex) {
to = edge[i].to;
if (to != fa)
dep[to] = dep[now] + 1, DFS(to, now);
}
}
#define lobit(x) ((x) & (-(x)))
struct BIT {
int mxv;
vector<int> sum;
void init() {
sum.resize(mxv + 1);
}
void change(int pos, int val) {
while (pos <= mxv)
sum[pos] += val, pos += lobit(pos);
}
int query(int pos) {
if (pos <= 0)
return 0;
int res = 0; pos = min(pos, mxv);
while (pos)
res += sum[pos], pos -= lobit(pos);
return res;
}
} T[N][2];
int f[N];
void build(int now, int fa) {
T[now][0].mxv = 1, f[now] = fa;
for (int to, i = head2[now]; i; i = edge[i].nex) {
to = edge[i].to;
if (to != fa)
build(to, now), T[now][0].mxv += T[to][0].mxv;
}
T[now][0].init();
T[now][1].mxv = T[now][0].mxv + 1;
T[now][1].init();
}
int dis(int u, int v) {
int lca = S.query(u, v);
return dep[u] + dep[v] - 2 * dep[lca];
}
void change(int now, int val) {
int pos = now;
while (pos != root) {
T[pos][0].change(dis(now, pos) + 1, val - pv[now]), T[pos][1].change(dis(now, f[pos]) + 1, val - pv[now]);
// printf("c %d %d %d\n", now, pos, val - pv[now]);
pos = f[pos];
}
T[root][0].change(dis(now, root) + 1, val - pv[now]);
pv[now] = val;
}
int query(int now, int val) {
int res = 0, pos = now;
while (pos != root) {
res += T[pos][0].query(val - dis(now, pos) + 1) - T[pos][1].query(val - dis(now, f[pos]) + 1);
// printf("pos %d %d %d\n", pos, res, val);
pos = f[pos];
}
res += T[root][0].query(val - dis(now, root) + 1);
return res;
}
//--------------------//
int main() {
scanf("%d%d", &n, &m);
for (int i = 1; i <= n; i++)
scanf("%d", &pv[i]);
for (int u, v, i = 1; i < n; i++)
scanf("%d%d", &u, &v), add(u, v, head1), add(v, u, head1);
DFS(1, 0), S.init();
stot = n;
calc_g(1, 0);
calc_g(rt, 0);
// puts("");
divide(root = rt);
// printf("root : %d\n", root);
build(root, 0);
for (int tem, i = 1; i <= n; i++)
tem = pv[i], pv[i] = 0, change(i, tem);
int las = 0;
for (int op, x, y, i = 1; i <= m; i++) {
scanf("%d%d%d", &op, &x, &y);
x ^= las, y ^= las;
if (op == 0)
printf("%d\n", las = query(x, y));
else
change(x, y);
}
return 0;
}
\(\color{blueviolet}{P3345}\)
(此部分抄自 Alex_Wei 博客。)
如果一个节点的某个子节点子树内权值和大于剩余部分,那么此节点的这个子节点比此节点更优。相当于求树上带权重心。
建出点分树暴力找出更优的节点,考虑对一个节点 \(u\) 求 \(f(u) = \sum\limits_v d_v \times dis(u, v)\),维护 \(s_{0, u}\) 表示 \(u\) 的点分树子树 \(T_u\) 的 \(\sum\limits_{v\in T_u} d_v \times dis(u, v)\),\(s_{1, u}\) 表示 \(\sum\limits_{v\in T_u} d_v\times dis(fa_u, v)\),\(c_u\) 表示 \(\sum \limits_{v \in T_u} d_v\) 则有:
注意,从父节点 \(x\) 移动到子节点 \(u\) 时,记路径 \(x \to u\) 上第一个节点为 \(suc_u\),那么 \(suc_u\) 的子树大小 \(c_{suc_u}\) 需要增加 \(c_u - c_{suc_u}\)。注意修改需要作用于点分树 \(suc_u \to x\) 路径上除了 \(x\) 以外的所有节点。回溯时清空修改。
\(\color{blueviolet}{P3241}\)
先建点分树,对于点 \(u\) 维护一下子树内所有点权及其 \(dis(x, u)\)、\(dis(x, fa_u)\),将他们按照点权排序,每次二分可求贡献区间,前缀和求贡献。
但是这样会有 \(2\) 倍常数,于是我们利用每个点度数不超过 \(3\) 的性质进行优化。考虑去掉我们当前子树的贡献等价于只取当前父节点其他子节点子树贡献,所以从父亲方面求一下其他子节点子树贡献即可,这样只用维护一个 vector。
3. 长链剖分
3.1. 简介
与重链剖分除了重子节点的选择不同外,剩下的剖分部分都是相同的。
定义节点 \(i\) 子树深度为节点 \(i\) 距离其子树内最深的叶子节点的距离。
节点 \(i\) 的重子节点即所有子节点中子树深度最大的那个。
至于实现可以参考重链剖分,一般来讲不需要在链上维护信息,所以不用记录 dfs 序。
需要记录一个子树深度 \(mxd\),初始化叶子节点 \(mxd \leftarrow 0/1\) 至于是 \(0\) 是 \(1\) 则根据维护边和点来判断即可。
3.2. 性质与应用
性质(都很显然):
- 当前节点所在长链末端节点即当前节点子树内最深的节点(显然是一个叶子节点)。
- 从根节点跳到任意一个节点经过长链的数量(等价于轻边数量)不超过 \(\sqrt n\)。
- 一个节点的 \(k\) 级祖先所在长链长度不小于 \(k\)。
- 所有长链点数总和为 \(n\)。
3.2.1. 树上 \(k\) 级祖先
预处理每个长链顶向上向下跳不超过链长的所有节点,这些信息是 \(O(n)\) 级的,再预处理每个节点的倍增跳祖先,复杂度 \(O(n \log n)\)。
还要预处理每个数的二进制下最高位,即 \(\log_2 x\)。
对于一个节点 \(u\),要求其 \(k\) 级祖先,先跳至其 \(2^{\log_2 k}\) 级祖先,根据性质的第三条可以确定当前链链顶可以通过向上向下跳不超过长链长度到达目标点,根据深度计算然后直接跳转即可。
复杂度 \(O(n\log n) - O(1)\),常数稍大。
3.2.2. 优化 DP
一般来讲长链优化 DP 都需要以相对深度为下标,这样子节点信息可以通过下标平移 \(1\) 来传到父节点。参考树上启发式合并,先继承重子节点信息,再将其他子节点的链信息暴力合并,每个节点合并一次,总次数是 \(O(n)\) 级别的。
以 \(\color{blueviolet}{CF1009F}\) 为例。
设 \(f_{i, j}\) 表示节点 \(i\) 子树内距 \(i\) 距离为 \(j\) 的节点个数。
时间复杂度为 \(O(n^2)\),无法接受。
考虑只有一条链的情况,设节点 \(u\) 的子节点为 \(v\)。
可以得到 \(f_{u, i} = f_{u, i - 1}, f_{u, 0} = 1\)。
也就是说当前节点信息可以直接继承子节点信息,但是要处理下标偏移。
想象我们有一个 \(val\) 数组 \(val_i\) 表示这条链上深度为 \(i\) 的节点个数(因为是链显然为 \(1\)),我们可以将 \(f_u\) 作为指针指向其位置设为 \(val_p\),则 \(f_v\) 指向 \(val_{p + 1}\)。
我们将所有长链进行以上操作,对于每个长链分配一个空间进行 DP,合并两条链直接暴力即可,每次将当前节点所有儿子合并到当前长链上,以此类推。每个节点会被合并一次,复杂度是优秀的 \(O(n)\),实现较为巧妙,可以看下文题目的代码。
3.3. 题目
\(\color{royalblue}{P5903}\)
树上 \(k\) 级祖先模板,长链剖分。
$\text{Code}$:
#include <bits/stdc++.h>
using namespace std;
typedef long long LL;
typedef unsigned long long ULL;
typedef unsigned int UIT;
typedef double DB;
typedef pair<int, int> PII;
#define fi first
#define se second
//--------------------//
const int N = 5e5 + 5;
int n, m, root;
UIT s;
inline UIT get(UIT x) {
x ^= x << 13;
x ^= x >> 17;
x ^= x << 5;
return s = x;
}
//----------//
// Graph
struct Edge {
int to, nex;
} edge[N];
int tot, head[N];
void add(int from, int to) {
edge[++tot] = {to, head[from]};
head[from] = tot;
// printf("%d %d\n", from, to);
}
//--------------------//
// LPD
const int LG = 18 + 5;
struct Poi {
int fa[LG], dep, mxd, hson, top;
vector<int> up, dw;
} p[N];
int lg[N], pw2[LG];
void lg_init() {
for (int i = 2; i <= n; i++)
lg[i] = lg[i >> 1] + 1;
for (int i = pw2[0] = 1; i <= lg[n]; i++)
pw2[i] = pw2[i - 1] << 1;
}
void DFS1(int now, int fa) {
p[now].fa[0] = fa, p[now].mxd = 1, p[now].dep = p[fa].dep + 1;
// printf("now %d %d\n", now, p[now].dep);
for (int i = 1; i <= lg[p[now].dep]; i++)
p[now].fa[i] = p[p[now].fa[i - 1]].fa[i - 1];
for (int to, i = head[now]; i; i = edge[i].nex) {
to = edge[i].to;
DFS1(to, now);
p[now].mxd = max(p[now].mxd, p[to].mxd + 1);
if (!p[now].hson || p[to].mxd > p[p[now].hson].mxd)
p[now].hson = to;
}
}
void DFS2(int now, int top) {
if (now == top) {
p[now].up.resize(p[now].mxd), p[now].dw.resize(p[now].mxd);
for (int j = 0, i = now; i && j < p[now].mxd; i = p[i].fa[0], j++)
p[now].up[j] = i;
}
// printf("top %d %d\n", now, p[now].dep - p[top].dep);
p[top].dw[p[now].dep - p[top].dep] = now, p[now].top = top;
if (p[now].hson)
DFS2(p[now].hson, top);
for (int to, i = head[now]; i; i = edge[i].nex) {
to = edge[i].to;
if (to != p[now].hson)
DFS2(to, to);
}
}
int get_kfa(int now, int k) {
if (k == 0)
return now;
now = p[now].fa[lg[k]];
k -= pw2[lg[k]] + (p[now].dep - p[p[now].top].dep), now = p[now].top;
if (k >= 0)
return p[now].up[k];
return p[now].dw[-k];
}
//--------------------//
int main() {
// freopen("P5903_1.in", "r", stdin);
scanf("%d%d%u", &n, &m, &s);
LL ans = 0; int lst = 0;
for (int x, i = 1; i <= n; i++) {
scanf("%d", &x);
if (x)
add(x, i);
else
root = i;
}
lg_init(), DFS1(root, 0), DFS2(root, root);
for (int x, k, i = 1; i <= m; i++) {
x = ((get(s) ^ lst) % n) + 1, k = (get(s) ^ lst) % p[x].dep;
ans ^= 1LL * i * (lst = get_kfa(x, k));
// printf("ans %d %d %d\n", x, k, lst);
}
printf("%lld", ans);
return 0;
}
\(\color{blueviolet}{CF1009F}\)
长链剖分优化 DP 板子,每次继承重子节点信息,指针处理下下标平移,剩余节点暴力合并,复杂度线性。
$\text{Code}$:
#include <bits/stdc++.h>
using namespace std;
typedef long long LL;
typedef unsigned long long ULL;
typedef unsigned int UIT;
typedef double DB;
typedef pair<int, int> PII;
#define fi first
#define se second
//--------------------//
const int N = 1e6 + 5, N2 = 2e6 + 5;
int n, mxv[N], ans[N];
struct Edge {
int to, nex;
} edge[N2];
int tot, head[N];
void add(int from, int to) {
edge[++tot] = {to, head[from]};
head[from] = tot;
}
//--------------------//
int cur, val[N], *f[N];
struct Poi {
int fa, dep, mxd, hson;
} p[N];
void DFS1(int now, int fa) {
p[now].fa = fa, p[now].dep = p[fa].dep + 1;
for (int to, i = head[now]; i; i = edge[i].nex) {
to = edge[i].to;
if (to == fa)
continue;
DFS1(to, now);
p[now].mxd = max(p[now].mxd, p[to].mxd + 1);
if (!p[now].hson || p[p[now].hson].mxd < p[to].mxd)
p[now].hson = to;
}
}
void DFS2(int now) {
f[now][0] = 1, mxv[now] = 1, ans[now] = 0;
if (p[now].hson) {
f[p[now].hson] = f[now] + 1, DFS2(p[now].hson);
if (mxv[p[now].hson] > mxv[now])
mxv[now] = mxv[p[now].hson], ans[now] = ans[p[now].hson] + 1;
}
for (int to, i = head[now]; i; i = edge[i].nex) {
to = edge[i].to;
if (to == p[now].fa || to == p[now].hson)
continue;
f[to] = val + cur, cur += p[to].mxd + 1, DFS2(to);
for (int i = 0; i <= p[to].mxd; i++) {
f[now][i + 1] += f[to][i];
if (f[now][i + 1] > mxv[now] || (f[now][i + 1] == mxv[now] && i + 1 < ans[now]))
mxv[now] = f[now][i + 1], ans[now] = i + 1;
}
}
}
//--------------------//
int main() {
scanf("%d", &n);
for (int u, v, i = 1; i < n; i++)
scanf("%d%d", &u, &v), add(u, v), add(v, u);
DFS1(1, 0);
f[1] = val, cur += p[1].mxd + 1;
DFS2(1);
for (int i = 1; i <= n; i++)
printf("%d\n", ans[i]);
return 0;
}
\(\color{blueviolet}{P5904}\)
长链剖分优化 DP。
设 \(f_{i, j}\) 表示节点 \(i\) 字数内距 \(i\) 距离为 \(j\) 的点的个数。
设 \(g_{i, j}\) 表示满足一下条件的无序点对 \((x, y)\) 个数,\(i\) 子树外若存在距 \(i\) 距离为 \(j\) 的节点 \(z\),则 \((x, y, z)\) 是一组合法三元组。
边转移边统计答案,简单乘法原理处理即可。
\(\color{blueviolet}{P3441}\)
长链剖分优化贪心。
显著的,直径肯定要选,然后每次选的时候肯定跨直径最优,如果存在两对不跨直径的点对,显然可以转换为两个跨直径的点对。
把直径一端拎起来当根节点,然后取最长的 \(2m - 1\) 个长链即可。(因为发现上文说的那个东西和取叶子节点覆盖是等价的。)