点分治/点分树 心得
【学习笔记】点分治、点分树
前言
我写这篇学习笔记的原因主要是因为我太菜了,每次学完就忘。趁着这次集训刚好学了点分树,就写一篇学习笔记,既方便自己复习,也方便新学者学习。其实这篇文章写了好久了,发现已经有同学发类似的了QWQ。祝大家 csp/NOIP/省选/NOI rp++
一般来说以下讨论中提到的“子树”,都是指的在点分治意义上的。“自己的块”是指在删除去过的根,所留下的连通块。
点分治
适用场景
对于一些在树上统计最值路径,路径计数,或是一个个连通分量的题,还有一种是与树的形态无直接关系。
通用做法
点分治,顾名思义,以某一个点为分界点,随后递归向其子树去处理子问题。
一般是以当前递归的树的重心为分界点,这里的重心是指 \(u\) 满足:
这里的 \(siz_u(v)\) 指的是以 \(u\) 为根时子树 \(v\) 的大小。
然后可以以子树的信息来更新当前的信息以此求出答案,并且每次只处理自己块内的点,为什么这样能对下面会依据例题解释因为如果不对就用不了点分治了。这样子因为子树大小不断减半,所以层数是 \(O(\log{n})\)。又因为每一层之间的区间不会有交,所以每一层的时间复杂度也是 \(O(n)\) 的。总时间复杂度 \(O(n\log{n})\),可能还要乘上一些处理信息的常数。
统计路径的例题:\(\color{#3498D8}P3806\) 【模板】点分治
题意
给定一颗 \(n\) 个节点有边权的树。
\(m\) 次询问,每次问是否存在一条长度恰好为 \(k\) 的路径。
\(1 \leq n\leq 10^4\),\(1 \leq m\leq 100\),\(1 \leq k \leq 10^7\),\(1 \leq u, v \leq n\),\(1 \leq w \leq 10^4\)
思路
我们考虑跨过块中心(以块中心为端点也行)的路径即可,而这样就可以统计所有的路径,证明:
如果存在一条路径的两个端点在两个同一层的不同块中,因为每一个块与同层的块的分界点都一定作为过中心,所以其肯定在更早的层里被统计过。
回到这道题,所以我们只需要处理到每个点存一个 \(k\) 大小的桶,表示是否存在离中心点一定长度的点。这样,在遍历一棵新的子树时,没遍历到一个点,就匹配一下当前的桶是否存在长度加起来为 \(k\) 的即可。具体的,可以看一下这段核心代码:
void gd(int u,int fa){//子树内的距离
if(d[u]>=K) return ;
t[++tot]=d[u];//这个子树要尝试配对的距离
int v;
for(pii it:g[u]){
v=it.fir;
if(v^fa&&!vis[v])
d[v]=d[u]+it.sec,
gd(v,u);
}
}
inline void js(int u){//计算以u为根的块的答案
int v,cnt=0;
for(pii it:g[u]){//pair{v,w}
v=it.fir;
if(vis[v]) continue;//块的边缘
tot=0,dep[v]=it.sec,gd(v,u);//dep就是离根的距离
for(int j = 1;j<=m;++j) if(!ans[j])//每一个询问
for(int i = 1;i<=tot;++i)//尝试配对
if(k[j]>=t[i]) ans[j]=ans[j]|nw[k[j]-t[i]];
for(int i = 1;i<=tot;++i) q[++cnt]=t[i],nw[t[i]]=1;//将统计的距离加入桶中
}
for(int i = 1;i<=cnt;++i) nw[q[i]]=0;//1e7的桶只能开一个,所以每次用完就删
}
::::success[AC code]
#include <bits/stdc++.h>
using namespace std;
#define freop(s) freopen(s".in","r",stdin);freopen(s".out","w",stdout);
//#define int long long
#define double long double
#define re register
#define endl '\n'
#define inf 0x7f7f7f7f
#define lb(x) (x&-x)
#define pii pair<int,int>
#define fir first
#define sec second
#define umap unordered_map
#define uset unordered_set
const int N=1e4+1,K=1e7+1,M=1e2+1;
int n,m,rt,S,tot,k[M],siz[N],d[N],t[N],q[N],mx[N]={inf};
vector<pii> g[N];
bitset<N> vis;
bitset<M> ans;
bitset<K> nw;
void grt(int u,int fa){
int v;
mx[u]=0;
siz[u]=1;
for(pii it:g[u]){
v=it.fir;
if(v^fa&&!vis[v]){
grt(v,u),
siz[u]+=siz[v];
mx[u]=max(mx[u],siz[v]);
}
}
mx[u]=max(mx[u],S-siz[u]);
if(mx[u]<mx[rt]) rt=u;
}
void gd(int u,int fa){
if(d[u]>=K) return ;
t[++tot]=d[u];
int v;
for(pii it:g[u]){
v=it.fir;
if(v^fa&&!vis[v])
d[v]=d[u]+it.sec,
gd(v,u);
}
}
inline void js(int u){
int v,cnt=0;
for(pii it:g[u]){
v=it.fir;
if(vis[v]) continue;
tot=0,
d[v]=it.sec,
gd(v,u);
for(int j = 1;j<=m;++j) if(!ans[j])
for(int i = 1;i<=tot;++i)
if(k[j]>=t[i]) ans[j]=ans[j]|nw[k[j]-t[i]];
for(int i = 1;i<=tot;++i) q[++cnt]=t[i],nw[t[i]]=1;
}
for(int i = 1;i<=cnt;++i) nw[q[i]]=0;
}
void solve(int u){
int v;
vis[u]=nw[0]=1,js(u);
for(pii it:g[u]){
v=it.fir;
if(vis[v]) continue;
S=siz[v],
rt=0,grt(v,0),
solve(rt);
}
}
int main(){
cin.tie(0)->ios::sync_with_stdio(false);
cout.tie(0);
// freop("in");
//↑以上为初始化↑
//------------------------------
cin >> n >> m,S=n;
for(int i = 1;i<n;++i){
int u,v,w;
cin >> u >> v >> w;
g[u].push_back({v,w}),
g[v].push_back({u,w});
}
for(int i = 1;i<=m;++i) cin >> k[i];
rt=0,grt(1,0),
// cout << rt << endl,
solve(rt);
for(int i = 1;i<=m;++i) cout << (ans[i]?"AYE":"NAY") << endl;
return 0;
}
::::
连通分量的例题:\(\color{#9C3DCF}P3806\) [JOISC 2020] 首都
题意
有 \(n\) 个点,每个点有一个颜色 \(c_i\)(最多 \(k\) 种)。
你现在有一种操作,可以使得两种颜色合并,也就是视为同一种颜色。
现在要求最少几次操作,可以找到得某一种颜色全部在一个连通分量里,且这个联通分量里没有别的颜色。
\(1 \le k,u,v \le n \le 2 \times 10^5\),\(1 \le c_i \le K\)
思路
点分治,以中心为根向外扩张,看最少几次操作。具体的,从根的颜色开始搜索,对于每个点只搜索一定要加的点,也就是根方向上的点。发现新颜色就把这颜色的点也加入搜索队列,如果发现有颜色的点不在当前块内,那么就直接 return 掉不更新答案。如果看不太懂可以看以下这段关键代码:
//fr[i]:是否是当前块内(是当前块内fr[i]就是rt)
//fa[i]:以rt为根的父亲,向根搜索用的
//fc[i]:表示颜色i是否已经加入搜索队列
//que[]:搜索队列
//tot:操作次数,每次搜到新颜色就++
void init(int u,int fat){
fr[u]=rt,fa[u]=fat,fc[c[u]]=0;
for(int v:g[u])
if(v^fat&&!vis[v])
init(v,u);
}
inline void get_ans(int u){
que[tot=1]=c[u],fc[c[u]]=1;
for(int i = 1;i<=tot;++i)
for(int v:cit[que[i]]){
if(fr[v]^u) return ;
v=fa[v];//往上跳
while(!fc[c[v]])
//向根搜索新颜色,如果颜色有了说明会搜到的
que[++tot]=c[v],
fc[c[v]]=1,v=fa[v];
}
ans=min(ans,tot-1);//记录答案
}
为什么这样是对的?考虑到如果搜到一个点,无论是否是从这点出发都要把这个颜色加入,而只要某一颜色 \(c\) 加入,就说明你的答案不会优于从 \(c\) 颜色的点出发,因为你总是要把穿插在 \(c\) 颜色内的其他颜色加入。所以这相当于将颜色之间建立起了一些联通分量,而同前面,同一层的块之间的分界点,就是前面层的一个根,也就说这个颜色的联通分量已经处理过了,所以这样就能保证每一个点都不会搜超过块的答案。
::::success[AC code]
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
typedef long double ld;
#define freop(s) freopen(s".in","r",stdin);freopen(s".out","w",stdout);
#define endl '\n'
#define inf 0x7f7f7f7f
#define lb(x) ((x)&-(x))
#define pii pair<int,int>
#define fir first
#define sec second
#define mkp(a,b) make_pair(a,b)
const int N=2e5+10;
int n,k,S,rt,ans=inf,c[N],sm[N],mx[N]={inf},que[N],tot,fr[N],fa[N],fc[N],vis[N];
vector<int> g[N],cit[N];
void get_rt(int u,int fa){
sm[u]=1,mx[u]=0;
for(int v:g[u]) if(v^fa&&!vis[v]){
get_rt(v,u),sm[u]+=sm[v],
mx[u]=max(mx[u],sm[v]);
}
mx[u]=max(mx[u],S-sm[u]);
if(mx[u]<mx[rt]) rt=u;
}
void init(int u,int fat){
fr[u]=rt,fa[u]=fat,fc[c[u]]=0;
for(int v:g[u])
if(v^fat&&!vis[v])
init(v,u);
}
inline void get_ans(int u){
que[tot=1]=c[u],fc[c[u]]=1;
for(int i = 1;i<=tot;++i)
for(int v:cit[que[i]]){
if(fr[v]^u) return ;
v=fa[v];
if(!fc[c[v]])
que[++tot]=c[v],
fc[c[v]]=1,v=fa[v];
}
ans=min(ans,tot-1);
}
void solve(int u){
init(u,u),get_ans(u),vis[u]=1;
for(int v:g[u]) if(!vis[v])
rt=0,S=sm[v],get_rt(v,u),solve(rt);
}
int main(){
cin.tie(0)->ios::sync_with_stdio(false);
cout.tie(0);
// freop();
//↑以上为初始化↑
//------------------------------
cin >> n >> k;
for(int i = 1,u,v;i<n;++i)
cin >> u >> v,
g[u].push_back(v),
g[v].push_back(u);
for(int i = 1;i<=n;++i)
cin >> c[i],cit[c[i]].push_back(i);
S=n,get_rt(1,0),solve(rt);
cout << ans << endl;
return 0;
}
::::
点分树
本质是可修改的在线的点分治,把每个根都对其子树连边,这样树高一定是点分治的层数,也就是 \(O(\log{n})\) 的。对于某一个点的询问只会在它和它点分树的祖先上有贡献,所以每次查询或修改都只会有 \(O(w\log{n})\),\(w\) 是统计答案的复杂度。一般来说点分树处理“与树形态无关”的问题更多(?),当然我只做了模板我自然是理解低下的。
具体的,点分树的实现一般是在点分治往下递归的时候把这个树存下来,比如存下每个节点的父亲。对于点分树最常用的对于点的修改和查询:因为每次的修改都只会影响这个节点的自身和祖先,因为你这个点的儿子的子树压根就没覆盖不到根,所以查询也是同理。随后让我们考虑这颗点分树有什么性质?因为点分治最多 \(O(\log{n})\) 层,所以自然点分树也有树高是 \(O(\log{n})\) 的,所以可以一步步从修改(查询)的点往上跳父亲去修改(统计)。
与树形态无关的例题:\(\color{#9C3DCF}P6329\) 【模板】点分树 / 震波
题意
给定一棵 \(n\) 个点的带点权的树,这里距离定义为点与点之间经过的边数。
总共有 \(m\) 次操作,操作共有两种:
0 x k查询距离 \(x\) 点不超过 \(k\) 的点的价值和(包括 \(x\) 本身)。1 x y将点 \(x\) 的点权改为 \(y\)。
对于 \(100 \%\) 的数据,有 \(1\leq n,m\leq 10^5, 1\leq u,v,x\leq n, 1\leq value_i,y\leq 10^4,0\leq k\leq n-1\) 。
思路
首先建立一棵点分树(上面有讲建立过程)。我们先想查询,还是从目标点向上走,发现本来是要查询
经过一位祖先节点 \(w\) 的转接就变为了
这时候聪明的你肯定发现了:“这不对啊,在 \(w\) 的 \(u\) 的方向上的点并不满足 \(dis(u,v)=dis(u,w)+dis(w,v)\) 的转换啊”
解决方法也很简单,只要儿子节点也存一个关于其父亲在自己方向上的贡献,然后减去即可,这就是与树形态无关的常用做法。可以看以下图片来辅助理解,红色是加上的,蓝色是删除的,只要维护这两者即可。

这时候聪明的你肯定发现了:“这样空间复杂度不会退化会 \(O(n^2)\) 吗?”
很简单依旧注意到因为点分树的树高是 \(O(\log{n})\) 的,所以给每个点建一个 \(O(siz_u)\) (子树大小)的数据结构是可以的。所以只要给每个点开一个桶,用 vector 的树状数组就可以存下“当前块内距离当前点的距离小于一定值的点权和”。注意,存父亲在当前块内的信息,也只需要开到当前子树大小。
::::succes[AC code]
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
typedef long double ld;
#define freop(s) freopen(s".in","r",stdin);freopen(s".out","w",stdout);
#define endl '\n'
#define inf 0x7f7f7f7f
#define lb(x) ((x)&-(x))
#define pii pair<int,int>
#define fir first
#define sec second
#define mkp(a,b) make_pair(a,b)
const int N=1e5+5,M=18;
int n,q,lans,a[N],dep[N],bz[N][M];
int m,rt,mn,siz[N],fl[N],fa[N];
vector<int> g[N];
struct tree_array{
int sz;vector<int> sum;
inline void init(int x){sum.resize((sz=x)+1);}
inline void add(int x,int y){
++x;
while(x<=sz) sum[x]+=y,x+=lb(x);
}
inline int ask(int x){
if(x<0) return 0;
int sm=0;x=min(x+1,sz);
while(x) sm+=sum[x],x-=lb(x);
return sm;
}
}tr[N][2];
void grt(int u,int fa){
siz[u]=1;int mx=0;
for(int v:g[u]) if(v^fa&&!fl[v])
grt(v,u),mx=max(mx,siz[v]),siz[u]+=siz[v];
if((mx=max(mx,m-siz[u]))<=mn) rt=u,mn=mx;
}
void build(int u){
fl[u]=1,tr[u][0].init(siz[u]=m+1);
tr[u][1].init(siz[u]);
for(int v:g[u]) if(!fl[v])
m=siz[v],mn=inf,grt(v,0),
fa[rt]=u,build(rt);
}
void init(int u=1,int fa=0){
bz[u][0]=fa,dep[u]=dep[fa]+1;
for(int i = 1;bz[u][i-1];++i)
bz[u][i]=bz[bz[u][i-1]][i-1];
for(int v:g[u]) if(v^fa) init(v,u);
}
inline int LCA(int u,int v){
if(dep[u]<dep[v]) swap(u,v);
for(int i = M-1;~i;--i)
if(dep[u]-dep[v]>=(1<<i)) u=bz[u][i];
if(u==v) return u;
for(int i = M-1;~i;--i)
if(bz[u][i]^bz[v][i])
u=bz[u][i],v=bz[v][i];
return bz[u][0];
}
inline int dis(int u,int v){
int lca=LCA(u,v);
return dep[u]+dep[v]-(dep[lca]<<1);
}
inline void change(int x,int k){
for(int u = x;u;u=fa[u]) tr[u][0].add(dis(x,u),k);
for(int u = x;fa[u];u=fa[u]) tr[u][1].add(dis(x,fa[u]),k);
}
inline int solve(int x,int k){
int ans=tr[x][0].ask(k);
int v=x,u=fa[x],t;
while(u){
t=k-dis(x,u);
ans+=tr[u][0].ask(t)-tr[v][1].ask(t);
v=u,u=fa[u];
}
return ans;
}
int main(){
cin.tie(0)->ios::sync_with_stdio(false);
cout.tie(0);
// freop();
//↑以上为初始化↑
//------------------------------
cin >> n >> q;
for(int i = 1;i<=n;++i) cin >> a[i];
for(int i = 1,u,v;i<n;++i)
cin >> u >> v,
g[u].push_back(v),
g[v].push_back(u);
m=n,mn=inf,grt(1,0),build(rt),init();
// for(int i = 1;i<=n;++i) cerr << fa[i] << ' ';cerr << endl;
for(int i = 1;i<=n;++i)
change(i,a[i]);
for(int op,x,y;q;--q){
cin >> op >> x >> y;x^=lans,y^=lans;
if(!op) cout << (lans=solve(x,min(y,n))) << endl;
else change(x,y-a[x]),a[x]=y;
}
return 0;
}
::::

浙公网安备 33010602011771号