树链剖分
重链剖分
树剖是一种将树链转化为序列问题的方法,于是可以把序列上的大部分东西都乘 \(log\) 后搬到重链上来做
注意对于静态问题,树剖的复杂度是可以做到单 \(log\) 的,具体地,预处理出每条重链的前缀信息,那么查询的时候大部分都是某一个重链的前缀,只有一次是一条链的一部分,所以 \(log\) 是加起来的
题意:树上有 \(2\le c \le 5\) 个人,选择一个地点,每个人到这个点路径上的物品为这个人可以获得的物品,要求求一个最大的 \(k\),使得每个人都可以带 \(k\) 个,且没有重复
由于 \(c\) 非常小,可以暴力使用 \(hall\) 定理来求出每个集合最大的 \(k\) 是多少再取 \(min\) 即可
那么现在需要知道每个人到某个点的路径上的颜色,由于颜色数不多,可以使用 \(bitset\) 维护,由于是静态问题,使用树剖时只需要在线段树上查询一次即可
用矩乘优化 \(AC\) 自动机转移,在树上就用树剖实现可以做到单 \(log\)
再次记录一下这个去除倍增的科技
考虑预处理每个点 \(1\sim dep_i\) 的信息
然后完全模拟树状数组,可以发现这样子的一种跳跃模式下一定可以从某个点调到任意一个祖先的(首先跳到根肯定可以,考虑最后一步如果深度差小于深度的 \(low\) 了,就直接跳 \(highbit\))
(然而滑稽了半天被树剖完全暴踩……)
这个算是经典题目了
考虑把深度转化一下,那么变为修改是根链加,查询同样根链加
但是用点分树等维护起来会更有扩展性
下面隆重推出大名鼎鼎名声远扬广为人知奥妙重重浓缩了万千精华的 \(Cyber_trick\)!
考虑动态维护 \(\sum_{u\in S}LCA(u,x)\)
修改的时候每次跳重链的时候加在树状数组中
比如这张图上查询 \(7\) 号点,那么跳到 \(3\) 号点后,应该先加上 \(3\times\) 乘 \(5\) 号子树的 \(size\)(如果有更多子树也要加),然后查询 \(3\) 号点父亲这个前缀的信息即可
管辖点问题
一类题目是对于双色树定义管辖点,并对管辖区域做一系列操作
对于信息的维护不同的题并不一样,而寻找管辖点可以用树剖实现
对于每一条重链维护一个 \(set\) 记录所有关键点的深度,可以发现这个做法是单 \(log\) 的,因为只要维护出重链上的关键点集合以及最深关键点的深度,查询的时候只有最后一条链是需要二分的
而修改是单点的,也只有一个 \(log\)
所有白色点的信息均记录在其被管辖的黑色点上面
每个黑色点维护出管辖的点的数量、和及其乘积
可以发现所有操作均可实现了
也是一个类似的问题
由于其具体维护难度更加偏重于线段树,写到线段树里吧
动态 \(dp\)
这个说实话其实也没有什么特别的,本质上就是树剖+矩乘
只不过上树时复杂度出锅,那么变成树剖线段树维护重链矩乘,也就是对于 \(dp\) 数组轻重链的 \(dp\) 值分别维护
修改时暴力跳链的过程中在接口处暴力 \(dp\) 出轻儿子的新 \(dp\) 值即可
当然并不是所有的动态 \(dp\) 都需要矩乘,有许多题只是用到动态 \(dp\) 的思想,本质上还是树剖
关键还是把握住维护的是儿子的信息
经过转化后需要求出 \(\sum_{v\in son_u}S_v^2\)
那么单点加的影响是根链加
像动态 \(dp\) 那样只维护重儿子的贡献,轻儿子的贡献暴力计算即可
长链剖分
一般适用于 \(dp\) 数组以深度为下标,那么父亲可以直接在儿子的数组平移后继承到,那么如果继承长儿子,复杂度可以变为 \(O(n)\)
实现的时候可以用 \(vector\) 或者指针,个人认为 \(vector\) 写起来非常阴间,因为要一直进行 \(siz-x\) 的操作,而指针就直接可以方便地继承长儿子了
注意长剖在父亲转移的时候已经改变了儿子的 \(dp\) 值,因此不支持在线查询
一般长剖的难点同样不在科技,列出 \(dp\) 方程是关键
可以用长剖实现 \(log\) 预处理 \(O(1)\) \(k\) 级祖先
预处理出朴素的倍增数组,以及在每个长链顶端预处理向上向下链长的节点,那么从 \(x\) 向上跳 \(highbit(k)\) 的祖先找到 \(y\),可以肯定 \(y\) 所在的链一定大于 \(k'\)
那么跳到 \(y\) 的链顶一定可以通过向上或向下跑找出答案
仅考虑 \(b\) 在 \(a\) 下面时的答案
设 \(f[i][j]\) 表示 \(i\) 作为 \(a\) 且限制为 \(j\) 时的答案
那么有转移 \(f[i][j]=\sum_v f[v][j-1]+siz[v]-1\)
可以通过打懒标记的长剖实现
代码
#include<bits/stdc++.h>
using namespace std;
#define ll long long
#define pb push_back
const int maxn=3e5+5;
const int maxm=6e5+5;
int n,m,x,y,k[maxn],hd[maxn],cnt,son[maxn],fa[maxn],d[maxn],dep[maxn],sum,siz[maxn];
vector<int>has[maxn];
ll ans[maxn],pool[maxn],*f[maxn],lazy[maxn];
int read(){
int x=0,f=1;char ch=getchar();
while(!isdigit(ch)){if(ch=='-')f=-1;ch=getchar();}
while(isdigit(ch)){x=x*10+ch-48;ch=getchar();}
return x*f;
}
struct Edge{
int nxt,to;
}edge[maxm];
void add(int u,int v){
edge[++cnt].nxt=hd[u];
edge[cnt].to=v;
hd[u]=cnt;
return ;
}
void dfs(int u){
dep[u]=1;siz[u]=1;
for(int i=hd[u],v;i;i=edge[i].nxt){
if((v=edge[i].to)==fa[u])continue;
fa[v]=u;d[v]=d[u]+1;
dfs(v);dep[u]=max(dep[u],dep[v]+1);
if(dep[v]>dep[son[u]])son[u]=v;siz[u]+=siz[v];
}
return ;
}
void dfs1(int u){
if(son[u]){
f[son[u]]=f[u]+1;
dfs1(son[u]);
lazy[u]=lazy[son[u]]+siz[son[u]]-1;
}
for(int i=hd[u],v;i;i=edge[i].nxt){
if((v=edge[i].to)==fa[u]||v==son[u])continue;
f[v]=pool+sum;sum+=dep[v];
dfs1(v);lazy[u]+=lazy[v]+siz[v]-1;
for(int j=1;j<=dep[v];j++)f[u][j]+=f[v][j-1];
}
f[u][0]=-lazy[u];
for(int i:has[u]){
ans[i]+=1ll*min(d[u]-1,k[i])*(siz[u]-1);
ans[i]+=f[u][min(k[i],dep[u]-1)]+lazy[u];
}
return ;
}
signed main(){
n=read(),m=read();
for(int i=1;i<n;i++)x=read(),y=read(),add(x,y),add(y,x);
for(int i=1;i<=m;i++){
x=read(),k[i]=read();
has[x].pb(i);
}
d[1]=1;dfs(1);
f[1]=pool;sum=dep[1];dfs1(1);
for(int i=1;i<=m;i++)printf("%lld\n",ans[i]);
return 0;
}
当然这题用带 \(log\) 的结构做会简单很多,不过这也启发我们使用维护深度的数据结构时可以思考是否能转化为长剖来优化掉 \(log\)