(笔记)点分治 点分树 动态点分治
基础知识
树的重心
定义
定义一颗无根树,其中节点 \(u\) 的子节点编号分别为 \([1,n]\) ,且定义 \(siz_i\) 为节点 \(i\) 的以 \(u\) 为根下计算的子树大小, \(f_u=\max(siz_1,siz_2,...,siz_n)\)
重心即找到一个点 \(u\) 使上述 \(f_u\) 的值尽可能小。
性质
- 假设树的重心 \(root\) 的所有儿子当中,以儿子 \(i\) 为根的子树的节点数最多,记为 \(siz_i\) ,那么\(2siz_i \le size_{root}\)。
点分治
一种通过在树上,将一个点问题拆分为多个部分解决的办法,其中一个点一般指树的重心,即可使树按照重心拆分后子树尽可能平衡,从而降低时间复杂度。
点分治的本质就是删点,每次在母树中找到重心,完成计算后将重心删去,然后在其所有子树内再分别寻找其重心,重复上述操作。由于性质1,递归层数不会超过 \(O(\log n)\),每层 \(O(n)\),总共 \(O(n\log n)\)。
例题
CF161D Distance in Tree
题意概要:给出一棵无根树,边权都是1,求有多少条简单路径的长度等于k,路径长度是指经过的边的权值之和。
下述三个函数,void fr用于寻找重心,void gds即getDistance,统计树内所有节点到根节点的距离,并记录在数组 \(q\) 中。void solve即分治过程,\(cnt_i\) 表示到重心距离为 \(i\) 的节点数量。
代码贴贴:
点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int maxn=2e5+10;
int n,head[maxn],idx,siz[maxn],bcs,rt,L;
int q[maxn],p,cnt[maxn],k;
long long ans;
bool vis[maxn];
struct Edge{
int v,next;
}e[2*maxn];
void ins(int x,int y){
e[++idx].v=y;
e[idx].next=head[x];
head[x]=idx;
}
void fr(int u,int fa,int nodeCnt){
siz[u]=1;
int bs=0;
for(int i=head[u];i;i=e[i].next){
int v=e[i].v;
if(v==fa||vis[v])continue;
fr(v,u,nodeCnt);
siz[u]+=siz[v];
if(siz[v]>bs)
bs=siz[v];
}
if(nodeCnt-siz[u]>bs)
bs=nodeCnt-siz[u];
if(bs<bcs){
rt=u;
bcs=bs;
}
}
void gds(int u,int fa,int dis){
q[++p]=dis;
siz[u]=1;
for(int i=head[u];i;i=e[i].next){
int v=e[i].v;
if(v==fa||vis[v])continue;
gds(v,u,dis+1);
siz[u]+=siz[v];
}
}
void solve(int u,int fa,int nodeCnt){
rt=u;
bcs=nodeCnt-1;
fr(u,fa,nodeCnt);
p=L=1;
q[1]=0;
cnt[0]=1;
vis[rt]=true;
for(int i=head[rt];i;i=e[i].next){
int v=e[i].v;
if(v==fa||vis[v])continue;
gds(v,rt,1);
for(int j=L+1;j<=p;j++)
if(k>=q[j])
ans+=cnt[k-q[j]];
for(int j=L+1;j<=p;j++)
cnt[q[j]]++;
L=p;
}
for(int j=1;j<=p;j++)
cnt[q[j]]--;
for(int i=head[rt];i;i=e[i].next){
int v=e[i].v;
if(v==fa||vis[v])continue;
solve(v,rt,siz[v]);
}
}
signed main(){
ios::sync_with_stdio(0);
cin.tie(0);cout.tie(0);
cin>>n>>k;
for(int i=1;i<n;i++){
int u,v;
cin>>u>>v;
ins(u,v);
ins(v,u);
}
solve(1,0,n);
cout<<ans;
return 0;
}
P5306 [COCI 2018/2019 #5] Transport
古早测试题,当时 Soh_paraMEEMS 大神场切紫,令人敬畏。诸多评论说是点分治板子,实则还是需要一点思维量的。很容易想到要在每棵有根树上对于每个节点 \(v\) 处理出从根到 \(v\) 需要的最少燃油 \(les_v\),\(v\) 到根节点最多能剩 \(gan_v\),\(v\) 能否到根节点?如果能,\(need_v\leftarrow 0\),否则 \(need_v\) 为 \(v\) 到根节点最少仍需多少燃油补充。
注意到只有 \(need_v=0\) 时才能计算 \(gan_v\),只需要分别转移即可,转移过程建议读者手推,并不难。当然这只是一种状态设计,还有更加简明的但是这只是阐述个人思路。
接下来要解决的就是匹配问题。对于 \(u\) 为根子树的两个互异且有序节点 \(v_1,v_2\),它们可以匹配当且仅当 \(gan_{v_1}\geq les_{v_2}\)。这个东西可以先跑一遍点分治,离散化下来然后树状数组什么的处理。但是也可以考虑更加简单的方式,直接容斥计算。只需要双指针先推出总的匹配情况,然后针对 \(u\) 的每个直接儿子 \(v\) 的子树,双指针推出其内部的匹配情况并在总答案上减去即可。加上排序时间复杂度 \(O(n\log^2 n)\)。
点击查看代码
#include<bits/stdc++.h>
using namespace std;
typedef long long LL;
const int N=1e5+5;
const LL INF=1e14;
int n,a[N],rt,siz[N],mxsiz[N];
int head[N],idx,nsiz;
struct Edge{int v,next,w;}e[N<<1];
bool vis[N];
void ins(int x,int y,int z){
e[++idx].v=y;
e[idx].next=head[x];
e[idx].w=z;
head[x]=idx;
}
void fr(int u,int fa){
siz[u]=1;
mxsiz[u]=0;
for(int i=head[u];i;i=e[i].next){
int v=e[i].v;
if(v==fa)continue;
if(vis[v])continue;
fr(v,u);
siz[u]+=siz[v];
mxsiz[u]=max(mxsiz[u],siz[v]);
}
mxsiz[u]=max(mxsiz[u],nsiz-siz[u]);
if(mxsiz[u]<mxsiz[rt])rt=u;
}
LL ans,les[N],gan[N],need[N],god;
vector<LL>q1,q2,s1,s2;
void gd(int u,int fa,LL dis,LL rec){
if(!need[u])s1.push_back(gan[u]),q1.push_back(gan[u]);
s2.push_back(les[u]),q2.push_back(les[u]);
siz[u]=1;
for(int i=head[u];i;i=e[i].next){
int v=e[i].v,w=e[i].w;
if(v==fa||vis[v])continue;
LL disv=dis+w;
LL recv=rec+a[v];
les[v]=max(les[u],disv-rec);
if(a[v]-w>=need[u])need[v]=0;//转移
else need[v]=max(disv-recv,need[u]-(a[v]-w));
gan[v]=god+recv-disv;
gd(v,u,disv,recv);
siz[u]+=siz[v];
}
}
void solve(int u){
vis[u]=1;god=a[u];
int pos=-1;
for(int i=head[u];i;i=e[i].next){
int v=e[i].v,w=e[i].w;
if(vis[v])continue;
les[v]=w;
need[v]=max(0,w-a[v]);
gan[v]=god+a[v]-w;
gd(v,u,w,a[v]);
sort(s1.begin(),s1.end());
sort(s2.begin(),s2.end());
pos=-1;
for(LL p1:s1){
while(pos+1<(int)s2.size()&&s2[pos+1]<=p1)pos++;
ans-=pos+1;
}
s1.clear();s2.clear();
}
q1.push_back(god);
q2.push_back(0);
sort(q1.begin(),q1.end());
sort(q2.begin(),q2.end());
pos=-1;
for(LL p1:q1){
while(pos+1<(int)q2.size()&&q2[pos+1]<=p1)pos++;
ans+=pos+1;
}
ans--;
q1.clear();q2.clear();
for(int i=head[u];i;i=e[i].next){
int v=e[i].v,w=e[i].w;
if(vis[v])continue;
nsiz=siz[v];rt=0;
fr(v,u);
solve(rt);
}
}
int main(){
mxsiz[0]=1e9;
scanf("%d",&n);
for(int i=1;i<=n;i++)
scanf("%d",&a[i]);
for(int i=1;i<n;i++){
int u,v,w;
scanf("%d%d%d",&u,&v,&w);
ins(u,v,w);ins(v,u,w);
}
nsiz=n;rt=0;
fr(1,0);
solve(rt);
printf("%lld",ans);
return 0;
}
P9058 [Ynoi2004] rpmtdq
其实没什么好说的,高质量题解 P9058【[Ynoi2004] rpmtdq】都已经说完了。
挺好的题,trick 是找支配点对。具体来说,对于 \((l,r)\),如果存在 \(l\le l'\land r'\le r\land l'\neq r'\) 的 \((l',r')\),且 \(dis(l,r)\geq(l',r')\),显然这个点对 \((l,r)\) 是无效点对,因为只要能取到 \((l,r)\) 就能取到更优的 \((l',r')\)。
点对问题考虑点分治,观察有效点对的性质,发现记 \(p\) 距离分治中心 \(dis_p\),那么 \(dis(a,b)\le dis_a+dis_b\),如果不在同一子树地取等。我们近似地认为它们相等,对统计答案所造成的影响分两个角度考虑:第一是作为可能的支配点对会变得非法,事实上支配点对一定会在某个分治中心取等,所以如果一开始没有取等说明在当前中心就不优;第二是作为非法点对可能会限制合法点对,但是显然取到的某些 \(dis_u\) 会变大,所以限制是更松的。
对于一个支配点对 \((l,r)\),其满足 \(l < k < r\) 的更劣点对 \((l,k)(k,r)\) 一定满足:\(dis(l,k)>dis(l,r)\land dis(k,r)>dis(l,r)\),且这是一个必要条件。可以转化为 \(dis_k>dis_r\land dis_k>dis_l\),那么扩展到 \(k\in[l+1,r-1]\) 就需要满足 \(\max(dis_l,dis_r)<\min_{k\in(l,r)}dis_k\)。
每层分治计算出 \(dis_u\),按编号从小到大假定枚举的 \(v\) 是 \(dis_l,dis_r\) 中比较大的那个 \(dis_v\),那直接按编号往前找到第一个 \(dis_u\le dis_v\) 的 \(u\) 这样 \((u,v)\) 就是一个合法支配点对。同理,往后找第一个 \(dis_u\le dis_v\) 的 \(u\) 这样 \((v,u)\) 也是合法支配点对,可以用单调栈前后扫两遍简单维护。由于需要排序这里多带个 \(\log\)。这样计算出来点对数量是 \(O(n\log n)\) 的,因为每个点在每个包含它的分治只会加入 \(O(1)\) 组合法点对。
为什么不再往前/往后取呢?试想如果你都能直接取到前面了,那把 \(v\) 换成之前找到的第一个 \(u\) 不是更优吗?
最后点对转化为坐标系上带权点 \((l,r,dis(l,r))\),对于区间 \([l',r']\) 的贡献发现不能简单扫描线因为 \(\min\) 信息不可差分,但是发现这是一个简单二维偏序问题,那么直接 \(r\) 维升序扫,然后每次查询 \(l\) 单点 \(\text{checkmin}\),树状数组后缀 \(\min\) 查询即可(前缀 \(\min\) 下标倒过来)。
时间复杂度 \(O(n\log^2 n+p\log n)\)。
点击查看代码
#include<bits/stdc++.h>
using namespace std;
typedef long long LL;
typedef pair<int,int> PII;
typedef set<int>::iterator IT;
const int N=1e6+5;
const LL INF=2e14;
bool vis[N];
int n,idx,head[N],top[N],fat[N],rt;
int siz[N],son[N],qn,mxsiz[N],tot;
struct Edge{int v,next,w;}e[N<<1];
LL dep[N],ans[N];
vector<PII>q[N];
vector<int>op[N];
void dfs0(int u,int fa){
fat[u]=fa;siz[u]=1;
for(int i=head[u];i;i=e[i].next){
int v=e[i].v,w=e[i].w;
if(v==fa)continue;
dep[v]=dep[u]+w;
dfs0(v,u);
siz[u]+=siz[v];
if(siz[v]>siz[son[u]])
son[u]=v;
}
}
void dfs1(int u){
int fa=fat[u];
if(son[fa]==u)top[u]=top[fa];
else top[u]=u;
if(son[u])dfs1(son[u]);
for(int i=head[u];i;i=e[i].next){
int v=e[i].v;
if(v==fa||v==son[u])continue;
dfs1(v);
}
}
LL Dis(int x,int y){
int orx=x,ory=y,lc=0;
while(top[x]!=top[y]){
if(dep[top[x]]<dep[top[y]])swap(x,y);
x=fat[top[x]];
}
if(dep[x]>dep[y])swap(x,y);
lc=x;
return dep[orx]+dep[ory]-2*dep[lc];
}
void link(int x,int y,int z){
e[++idx].v=y;
e[idx].next=head[x];
e[idx].w=z;
head[x]=idx;
}
struct BIT{
LL av[N];
inline int lowbit(int x){return x&-x;}
void init(){
for(int i=1;i<=n;i++)
av[i]=INF;
}
void chkmin(int p,LL x){
p=n-p+1;
for(int i=p;i<=n;i+=lowbit(i))
av[i]=min(av[i],x);
}
LL que(int p){
p=n-p+1;LL res=INF;
for(int i=p;i;i-=lowbit(i))
res=min(res,av[i]);
return res;
}
}T;
void fr(int u,int fa){
siz[u]=1;
mxsiz[u]=0;
for(int i=head[u];i;i=e[i].next){
int v=e[i].v;
if(vis[v]||v==fa)continue;
fr(v,u);
siz[u]+=siz[v];
mxsiz[u]=max(mxsiz[u],siz[v]);
}
mxsiz[u]=max(mxsiz[u],tot-siz[u]);
if(mxsiz[u]<mxsiz[rt])rt=u;
}
LL dis[N];
vector<int>vec;
void getdis(int u,int fa){
siz[u]=1;
vec.emplace_back(u);
for(int i=head[u];i;i=e[i].next){
int v=e[i].v;LL w=e[i].w;
if(v==fa||vis[v])continue;
dis[v]=dis[u]+w;
getdis(v,u);
siz[u]+=siz[v];
}
}
int stk[N],tp;
void solve(int u){
vis[u]=1;vec.clear();
dis[u]=0;getdis(u,0);
sort(vec.begin(),vec.end());
tp=0;
for(int i:vec){
while(tp&&dis[stk[tp]]>dis[i])tp--;
if(tp)op[max(stk[tp],i)].emplace_back(min(stk[tp],i));
stk[++tp]=i;
}
tp=0;
for(int ID=vec.size()-1;ID>=0;ID--){
int i=vec[ID];
while(tp&&dis[stk[tp]]>dis[i])tp--;
if(tp)op[max(stk[tp],i)].emplace_back(min(stk[tp],i));
stk[++tp]=i;
}
for(int i=head[u];i;i=e[i].next){
int v=e[i].v;
if(vis[v])continue;
rt=0;tot=siz[v];
fr(v,u);solve(rt);
}
}
int main(){
ios::sync_with_stdio(0);
cin.tie(0);cout.tie(0);
cin>>n;
for(int i=1;i<n;i++){
int x,y,z;cin>>x>>y>>z;
link(x,y,z);link(y,x,z);
}
dfs0(1,0);dfs1(1);
mxsiz[rt=0]=N;tot=n;
fr(1,0);solve(rt);
cin>>qn;
for(int i=1;i<=qn;i++){
int l,r;cin>>l>>r;
q[r].emplace_back(make_pair(l,i));
}
T.init();
for(int i=1;i<=n;i++){
for(int v:op[i])
T.chkmin(v,Dis(v,i));
for(PII v:q[i]){
LL res=T.que(v.first);
ans[v.second]=(res==INF?-1:res);
}
}
for(int i=1;i<=qn;i++)
cout<<ans[i]<<'\n';
return 0;
}
点分树 动态点分治
前置概念
树的直径:\(dis(u,v)\) 为树上 \(u,v\) 两点的简单路径长度,找到 \(u,v\) 使得 \(dis(u,v)\) 取最大值。(其实跟这个没有关系)
树的重心:以该点为根时,最大子树的大小最小化。具体地,可以使用树形 DP 求解。
树上 \(K\) 级邻域查询:这是点分树解决的动态问题。具体来说,点有点权,支持动态修改,那么 对 \(u\) 的树上 \(K\) 级邻域查询就是所有和 \(u\) 距离不超过 \(K\) 的点的点权和。
P6329 【模板】点分树 | 震波
由于题目要求使用动态维护,想到用类似线段树的数据结构维护一下。那么这里的点分树是什么?
根据之前学习点分治的经验,我们会将原树不断地分成若干半,其中分割点为每棵树的重心。近似地把搜索过程看成分层结构,那么点分树就是指把这些相邻层的重心两两连边得到的树形结构。
点分治的重心分治结构使得点分树的深度为 \(O(\log n)\) 的。
点分树和原树没有必然联系,有且仅有一个特别好的性质:\(u,v\) 在点分树上的 LCA 一定在原树 \(u\rightarrow v\) 的路径上,即 \(dis(u,lca)+dis(lca,v)=dis(u,v)\),其中 \(dis(x,y)\) 表示 \(x,y\) 在原树上的最短路径长,\(lca\) 指两点在点分树上的 \(\text{LCA}\)。
如上性质我们便可以联想到一种可能的方法:建出点分树后,对每个点 \(u\) 动态开点权值线段树,下标 \(i\) 记录 \(dis(u,z)=i\) 的 \(\sum a_z\)。
由于之前点分治的经历,我们察觉到一个点对答案做出贡献当且仅当:
- 其与上若干层祖先的其他子树中的点连线时。
- 其与下若干层儿子直接连线时。
每次更新时,把自己的信息上推到自己的所有祖先的动态开点线段树上,这样就解决了第一个需求,让其他子树的点可以直接与其匹配。
我们又发现,从祖先的角度看,可以获取自己所有儿子的信息,那不是就满足了第二个需求了吗?
于是乎粗略的思路就是如此。建两棵线段树,第一棵 T1 维护 \(x\) 在点分树子树内与 \(x\) 距离为 \(i\) 的有多少个,第二棵 T2 维护在 \(x\) 点分树子树内距离 \(fa_x\) 距离为 \(i\) 的有多少个,这里的 \(fa_x\) 指的也是点分树上的父亲。
那我们的查询过程只需要从节点 \(z\) 一直往上爬,对于节点 \(z\) 统计其 T1 的 \([0,k]\)(点分树子树内答案),然后子树外答案往上爬。每一层到节点 \(u\),上一层到的节点称为 \(pre\)。答案就加上 \(u\) T1 的 \([0,k-dis(u,z)]\) 部分,同时为了避免算到已经计算过的点权,减去 \(pre\) T2 的 \([0,k-dis(u,z)]\) 部分。
对于修改操作,我们也顺着节点 \(z\) 往上爬然后对应修改祖先线段树的值即可。实际实现中我们不需要真的把点分树建出来,只需要记录每个节点在点分树上的父亲即可。
时间复杂度由于线段树的存在是 \(O(n\log^2 n)\)。
点击查看代码
#include<bits/stdc++.h>
using namespace std;
typedef long long LL;
const int N=1e5+5,INF=1e9;
vector<int>G[N];
inline void ins(int x,int y){
G[x].push_back(y);
}
int ans;
int n,m;
int f[N][21],dep[N];
inline void dfs0(int u,int fa){
dep[u]=dep[fa]+1;
for(int v:G[u]){
if(v==fa)continue;
f[v][0]=u;
for(int j=1;(1<<j)<=dep[u];j++)
f[v][j]=f[f[v][j-1]][j-1];
dfs0(v,u);
}
}
int rt,mx[N],siz[N];
bool vis[N];
inline void fr(int u,int fa,int S){
siz[u]=1;
mx[u]=0;
for(int v:G[u]){
if(v==fa||vis[v])continue;
fr(v,u,S);
siz[u]+=siz[v];
mx[u]=max(mx[u],siz[v]);
}
mx[u]=max(mx[u],S-siz[u]);
if(mx[rt]>mx[u])rt=u;
}
int dfa[N],val[N];
inline int LCA(int x,int y){
if(dep[x]<dep[y])swap(x,y);
for(int i=20;i>=0;i--){
if(dep[f[x][i]]>=dep[y])
x=f[x][i];
}
if(x==y)return x;
for(int i=20;i>=0;i--){
if(f[x][i]!=f[y][i])
x=f[x][i],y=f[y][i];
}
return f[x][0];
}
inline int gtdis(int x,int y){
return dep[x]+dep[y]-2*dep[LCA(x,y)];
}
inline void dvd(int u,int tot){
vis[u]=1;
for(int v:G[u]){
if(vis[v])continue;
rt=0;
int sz=(siz[v]<siz[u]?siz[u]:(tot-siz[u]));
fr(v,u,sz);
dfa[rt]=u;//record father
dvd(rt,sz);
}
}
struct Sgt{
#define ls (t[p].lc)
#define rs (t[p].rc)
#define mid ((l+r)>>1)
struct Node{
int lc,rc,sum;
}t[N<<5];
int rt[N];
int ncnt;
inline void pushup(int p){
t[p].sum=t[ls].sum+t[rs].sum;
}
inline void update(int &p,int l,int r,int pos,int v){
if(!p)p=++ncnt;
if(l==r){t[p].sum+=v;return ;}
if(pos<=mid)update(ls,l,mid,pos,v);
else update(rs,mid+1,r,pos,v);
pushup(p);
}
inline int query(int p,int l,int r,int L,int R){
if(!p)return 0;
if(L<=l&&r<=R)return t[p].sum;
int res=0;
if(L<=mid)res+=query(ls,l,mid,L,R);
if(R>mid)res+=query(rs,mid+1,r,L,R);
return res;
}
#undef ls
#undef rs
#undef mid
}T1,T2;
inline void modi(int pos,int v){
int cur=pos;
while(cur){
T1.update(T1.rt[cur],0,n-1,gtdis(cur,pos),v);
if(dfa[cur])T2.update(T2.rt[cur],0,n-1,gtdis(dfa[cur],pos),v);//例行维护
cur=dfa[cur];
}
}
inline int query(int pos,int k){
int cur=pos,pre=0,res=0;
while(cur){
if(gtdis(cur,pos)>k){
pre=cur,cur=dfa[cur];
continue;//由于原树中两点距离关系可能与点分树中不同,不能break
}
res+=T1.query(T1.rt[cur],0,n-1,0,k-gtdis(cur,pos));
if(pre)res-=T2.query(T2.rt[pre],0,n-1,0,k-gtdis(cur,pos));//文中说的贡献就在此处
pre=cur,cur=dfa[cur];
}
return res;
}
int main(){
ios::sync_with_stdio(0);
cin.tie(0);cout.tie(0);
cin>>n>>m;
for(int i=1;i<=n;i++)
cin>>val[i];
for(int i=1;i<n;i++){
int u,v;
cin>>u>>v;
ins(u,v);
ins(v,u);
}
dfs0(1,0);
mx[0]=INF;rt=0;
fr(1,0,n);
dvd(rt,n);
for(int i=1;i<=n;i++)
modi(i,val[i]);
for(int i=1;i<=m;i++){
int opt,x,y;
cin>>opt>>x>>y;
x^=ans,y^=ans;
if(opt){
modi(x,y-val[x]);
val[x]=y;
}
else {
ans=query(x,y);
cout<<ans<<'\n';
}
}
return 0;
}

浙公网安备 33010602011771号