点分治

点分治常用于树上路径统计等问题。

点分治

每次分治过程大致如下:

  • 我们先求出当前连通块树的重心;

  • 处理与重心有关的答案;

  • 删除重心

  • 递归处理与重心相连的子连通块。

伪代码如下:

void solve(int x)
{
	 Find1(x,0),Find2(x,0); // 找到重心 rt 
	 // 处理和 rt 有关的答案
	 used[rt]=true;
	 for(/*与 rt 直接相连并且没有被删除的节点*/) solve(ver);
}

如果答案同时包含多个子树,那么直接从 \(rt\) 开始 solve,否则可以一个一个子树 solve,每次结束后合并答案。

这样保证不会渠道同一个子树内。

P3806 【模板】点分治

模板题,求出整棵树中两点之间是否存在距离为 \(k\) 的点对。

只要点分治后用桶记录当前重心到每一个点的距离以及来自那一个儿子,比较即可。

P4178 Tree

模板题,求出整棵树中两点之间距离不超过 \(k\) 的点对数量。

我们将重心到每一个点的距离处理后排序,双指正扫描(记得减去来自相同子树的情况)即可。

树上 \(0/1\) 背包

给定一棵树,每个点上有一个物品有一个价值 \(w_i\)\(m\) 次询问,每次询问 \(u\)\(v\) 的路径上选择 \(k\) 个物品的最大价值。

每次在重心处理即可。

P6326 Shopping

题意:可以选择树上的一个连通块,连通块内多重背包,且选中的每个点都必须要选择物品。问最大价值。

\(n\le 500,V\le 4000\)

考虑点分治,每次规定重心必须选择,之后再树上进行依赖背包加多重背包即可。

依赖背包可见 背包问题 DP

$\texttt{code}$
#define Maxn 505
#define Maxm 4005
#define pb push_back
int T,n,m,tot,rt,subsiz,ans;
int siz[Maxn],dp[Maxn][Maxm],f[Maxm];
int hea[Maxn],nex[Maxn<<1],ver[Maxn<<1];
int w[Maxn],c[Maxn],d[Maxn];
bool used[Maxn];
inline void add(int x,int y){ ver[++tot]=y,nex[tot]=hea[x],hea[x]=tot; }
void Find1(int x,int fa)
{
	 siz[x]=1,subsiz++;
	 for(int i=hea[x];i;i=nex[i]) if(!used[ver[i]] && ver[i]!=fa)
	 	 Find1(ver[i],x),siz[x]+=siz[ver[i]];
}
void Find2(int x,int fa)
{
	 bool isrt=true;
	 for(int i=hea[x];i;i=nex[i]) if(!used[ver[i]] && ver[i]!=fa)
	 {
	 	 Find2(ver[i],x);
	 	 if((siz[ver[i]]<<1)>subsiz) isrt=false;
	 }
	 if(((subsiz-siz[x])<<1)>subsiz) isrt=false;
	 if(isrt) rt=x;
}
inline void many_pack(int C,int W,int D)
{
	 for(int i=1;i<=D;D-=i,i<<=1) for(int j=m;j>=C*i;j--)
	 	 if(f[j-C*i]!=-inf) f[j]=max(f[j],f[j-C*i]+W*i);
 	 if(D) for(int j=m;j>=C*D;j--) if(f[j-C*D]!=-inf)
 	 	 f[j]=max(f[j],f[j-C*D]+W*D);
}
void dfs(int x,int fa,int dep)
{
	 if((dep+=c[x])>m) return;
	 for(int i=0;i<=m;i++) dp[x][i]=f[i];
	 for(int i=m;i>=dep;i--) f[i]=f[i-c[x]]+w[x];
	 for(int i=0;i<dep;i++) f[i]=-inf;
	 many_pack(c[x],w[x],d[x]-1);
	 for(int i=hea[x];i;i=nex[i]) if(!used[ver[i]] && ver[i]!=fa)
	 	 dfs(ver[i],x,dep);
	 for(int i=0;i<=m;i++) dp[x][i]=f[i]=max(dp[x][i],f[i]);
}
void solve(int x)
{
	 subsiz=0,Find1(x,0),Find2(x,0);
	 for(int i=0;i<=m;i++) f[i]=-inf;
	 f[0]=0,dfs(rt,0,0);
	 for(int i=0;i<=m;i++) ans=max(ans,dp[rt][i]);
	 used[rt]=true;
	 for(int i=hea[rt];i;i=nex[i]) if(!used[ver[i]]) solve(ver[i]);
}
int main()
{
	 T=rd();
	 while(T--)
	 {
	 	 n=rd(),m=rd(),tot=ans=0;
	 	 for(int i=1;i<=n;i++) hea[i]=0,used[i]=false;
	 	 for(int i=1;i<=n;i++) w[i]=rd();
	 	 for(int i=1;i<=n;i++) c[i]=rd();
	 	 for(int i=1;i<=n;i++) d[i]=rd();
	 	 for(int i=1,x,y;i<n;i++) x=rd(),y=rd(),add(x,y),add(y,x);
	 	 solve(1);
	 	 printf("%d\n",ans);
	 }
	 return 0;
}

P4149 [IOI2011]Race

点分治似乎是一眼,但是在如何处理贡献的时候卡了很久。。。

简化处理贡献的操作,我们需要寻找经过根节点的路径,满足边权之和为 \(k\) 时边数的最小值。

\(\bigstar\texttt{Inportant}\):那么我们将根节点一下每一颗子树分开来处理,每次处理一颗,统计完后再更新一颗子树的信息。

这样保证了路径不会自己与自己不会相交。

处理贡献部分代码:

$\texttt{code}$
void update(int x,int fa,int dep,int C)
{
	 if(dep>k) return;
	 dot[++cnt]=(Data){dep,C};
	 for(int i=hea[x];i;i=nex[i]) if(!used[ver[i]] && ver[i]!=fa)
	 	 update(ver[i],x,dep+edg[i],C+1);
}
void solve(int x)
{
	 subsiz=cnt=0,Find1(x,0),Find2(x,0);
	 for(int i=hea[rt],pre;i;i=nex[i]) if(!used[ver[i]])
	 {
	 	 pre=cnt,update(ver[i],rt,edg[i],1);
	 	 for(int j=pre+1;j<=cnt;j++) if(k>=dot[j].dep)
		  	 ans=min(ans,minn[k-dot[j].dep]+dot[j].Cost);
		 for(int j=pre+1;j<=cnt;j++)
		 	 minn[dot[j].dep]=min(minn[dot[j].dep],dot[j].Cost);
	 }
	 for(int i=1;i<=cnt;i++) minn[dot[i].dep]=inf;
	 used[rt]=true;
	 for(int i=hea[rt];i;i=nex[i]) if(!used[ver[i]]) solve(ver[i]);
}

动态点分治

部分参考 辰星凌【学习笔记】树论—点分树(动态点分治)

点分治的核心思想在于依据重心划分子连通块,其良好的性质保证了最多只会分治 \(\log n\) 层。有了这一特性,便可使用各种暴力计算答案。

那么我们按照分治递归的顺序提一颗新树出来,易知树高是 \(O(\log n)\) 的。

具体地说,对于每一个找到的重心,将上一层分治时的重心设为它的父亲,得到一颗大小不变、最多 \(\log n\) 层的虚树(或者理解为重构树,亦可称点分树,意义一样)。

Claris 大大说,我们其实不用把树建出来,只用记下每个分治结构的贡献就可以用容斥的方法抵消贡献了。

模板伪代码:

int weight[Maxn<<1]; // attention!
vector<pa> dot[Maxn]; // 记下一路下来的所有重心以及道重心的距离 

struct Divide_tree
{
	 int _n; // size 
	 inline void init(int x){ _n=x; /* init */ }
	 void change() { /* do something*/ }
	 void query() { /* do something*/ }
}div_tr[Maxn<<1]; // attention!

void dfs(int x,int fa,int dep)
{
	 dot[x].pb(pa(cnt,dep));
	 for(int i=hea[x];i;i=nex[i])
	 	 if(!used[ver[i]] && ver[i]!=fa)
	 	 	 dfs(ver[i],x,dep+edg[i]);
}

void build(int x)
{
	 subsiz=0,Find1(x,0),Find2(x,0);
	 weight[++cnt]=1; // 直接贡献为 1 
	 dfs(rt,0,0);
	 div_tr[cnt].init(/* something need to record */);
	 for(int i=hea[rt];i;i=nex[i])
	 	 if(!used[ver[i]])
		 {
		 	 weight[++cnt]=-1; // 由于在同一个子树内,重复计算,贡献 -1
			 dfs(ver[i],x,edg[i]);
		 	 div_tr[cnt].init(/* something need to record */);
		 }
	 used[rt]=true;
	 for(int i=hea[x];i;i=nex[i]) if(!used[ver[i]]) build(ver[i]);
}

inline void change(int x)
{
	 for(auto v:dot[x]) div_tr[v.fi].change(/* something */,v.se);
}

inline int query(int x)
{
	 int ret=0;
	 for(auto v:dot[x]) ret+=div_tr[v.fi].query(v.se);
	 return ret;
}

P6329 【模板】点分树 | 震波

给定一棵树,有两种操作:

  • 将节点 \(x\) 的点权改为 \(k\)
  • 查询到节点 \(x\) 距离不超过 \(k\) 的所有点的点权之和。

我们考虑先建立出点分树,对于每一个访问的分治结构记录到重心距离为 \(d\) 的点权之和。

查询的之后直接累加一路上分治结构访问到的重心的对应答案集合即可。

$\texttt{code}$
#define inf 0x3f3f3f3f
#define Maxn 100005
#define pa pair<int,int>
#define fi first
#define se second
#define pb push_back
typedef long long ll;
int n,q,tot,cnt,subsiz,rt,deepest;
int val[Maxn],weight[Maxn<<1],siz[Maxn];
int hea[Maxn],nex[Maxn<<1],ver[Maxn<<1];
bool used[Maxn];
vector<pa> dot[Maxn];
struct Divided_tree
{
	 vector<int> tree;
	 int _n,ppp=0;
	 inline void init(int x){ _n=x,tree.resize(x+1); }
	 inline void add(int x,int k)
	 {
	 	 if(x==0) { tree[0]+=k,ppp+=k; return; }
	 	 while(x<=_n) tree[x]+=k,x+=x&(-x);
	 }
	 inline int query(int x)
	 {
	 	 int ret=0; x=min(x,_n);
		 while(x) ret+=tree[x],x-=x&(-x); return ret+tree[0];
	 }
}div_tr[Maxn<<1];
inline void add(int x,int y){ ver[++tot]=y,nex[tot]=hea[x],hea[x]=tot; }
void Find1(int x,int fa)
{
	 siz[x]=1,subsiz++;
	 for(int i=hea[x];i;i=nex[i])
	 	 if(!used[ver[i]] && ver[i]!=fa)
	 	 	 Find1(ver[i],x),siz[x]+=siz[ver[i]];
}
void Find2(int x,int fa)
{
	 bool isrt=true;
	 for(int i=hea[x];i;i=nex[i])
	 	 if(!used[ver[i]] && ver[i]!=fa)
		 {
		 	 Find2(ver[i],x);
		 	 if((siz[ver[i]]<<1)>subsiz) isrt=false;
		 }
	 if(((subsiz-siz[x])<<1)>subsiz) isrt=false;
	 if(isrt) rt=x;
}
void dfs(int x,int dep,int fa)
{
	 dot[x].pb(pa(cnt,dep)),deepest=max(deepest,dep);
	 for(int i=hea[x];i;i=nex[i])
	 	 if(!used[ver[i]] && ver[i]!=fa)
	 	 	 dfs(ver[i],dep+1,x);
}
void build(int x)
{
	 subsiz=0,Find1(x,0),Find2(x,0);
	 weight[++cnt]=1,deepest=0,dfs(rt,0,0),div_tr[cnt].init(deepest);
	 for(int i=hea[rt];i;i=nex[i]) if(!used[ver[i]])
	 	 weight[++cnt]=-1,deepest=0,dfs(ver[i],1,rt),div_tr[cnt].init(deepest);
	 used[rt]=true;
	 for(int i=hea[rt];i;i=nex[i])
	 	 if(!used[ver[i]])
		  	 build(ver[i]);
}
void change(int x,int k) { for(auto v:dot[x]) div_tr[v.fi].add(v.se,k); }
int query(int x,int k)
{
	 int ret=0;
	 for(auto v:dot[x])
	 	 if(k>=v.se)
	 	 	 ret+=weight[v.fi]*div_tr[v.fi].query(k-v.se);
	 return ret;
}
int main()
{
	 n=rd(),q=rd();
	 for(int i=1;i<=n;i++) val[i]=rd();
	 for(int i=1,x,y;i<n;i++) x=rd(),y=rd(),add(x,y),add(y,x);
	 build(1);
	 for(int i=1;i<=n;i++) change(i,val[i]);
	 for(int i=1,opt,x,k,Lastans=0;i<=q;i++)
	 {
	 	 opt=rd(),x=rd()^Lastans,k=rd()^Lastans;
	 	 if(opt) change(x,k-val[x]),val[x]=k;
	 	 else Lastans=query(x,k),printf("%d\n",Lastans);
	 }
	 return 0;
}

LWDB

给定一棵树,有两种操作:

  • 将距离节点 \(x\)\(d\) 的点的颜色都改为 \(c\)
  • 查询节点 \(x\) 的颜色。

我们在每个分治结构中记录这个重心为中心开始覆盖的颜色。

我们发现如果之前有一个距离 \(x\)\(d_1\) 的覆盖 \(c_1\),现在又有一个距离 \(x\)\(d_2\) 的覆盖 \(c_2\),那么前面那个覆盖一定可以省去。

因此我们在每一个分治结构维护一个单调栈记录颜色,查询时二分即可。

$\texttt{code}$
#define Maxn 100005
#define pil pair<int,ll>
#define pii pair<int,int>
#define fi first
#define se second
#define pb push_back
typedef long long ll;
int n,m,tot,subsiz,rt,cnt;
int siz[Maxn];
int hea[Maxn],nex[Maxn<<1],ver[Maxn<<1];
ll edg[Maxn<<1];
bool used[Maxn];
vector<pil> dot[Maxn];
struct Data{ int Color,TIME,Dist; };
struct Divided_tree
{
	 int tp;
	 vector<Data> col;
}div_tr[Maxn];
inline void add(int x,int y,ll d){ ver[++tot]=y,nex[tot]=hea[x],hea[x]=tot,edg[tot]=d; }
void Find1(int x,int fa)
{
	 siz[x]=1,subsiz++;
	 for(int i=hea[x];i;i=nex[i]) if(!used[ver[i]] && ver[i]!=fa)
	 	 Find1(ver[i],x),siz[x]+=siz[ver[i]];
}
void Find2(int x,int fa)
{
	 bool isrt=true;
	 for(int i=hea[x];i;i=nex[i]) if(!used[ver[i]] && ver[i]!=fa)
	 {
	 	 Find2(ver[i],x);
	 	 if((siz[ver[i]]<<1)>subsiz) isrt=false;
	 }
	 if(((subsiz-siz[x])<<1)>subsiz) isrt=false;
	 if(isrt) rt=x;
}
void dfs(int x,int fa,ll dep)
{
	 dot[x].pb(pii(cnt,dep));
	 for(int i=hea[x];i;i=nex[i]) if(!used[ver[i]] && ver[i]!=fa)
	 	 dfs(ver[i],x,dep+edg[i]);
}
void build(int x)
{
	 subsiz=0,Find1(x,0),Find2(x,0);
	 cnt++,dfs(rt,0,0),div_tr[cnt].col.resize(1),used[rt]=true;
	 for(int i=hea[rt];i;i=nex[i]) if(!used[ver[i]]) build(ver[i]);
}
void change(int x,int d,int c,int Time)
{
	 for(auto v:dot[x])
	 {
	 	 int fro=v.fi,tmp=d-v.se;
	 	 if(tmp<0) continue;
	 	 while(div_tr[fro].tp && div_tr[fro].col[div_tr[fro].tp].Dist<=tmp)
		  	 div_tr[fro].tp--;
		 div_tr[fro].tp++;
		 if((int)div_tr[fro].col.size()<div_tr[fro].tp+1)
		 	 div_tr[fro].col.pb((Data){c,Time,tmp});
		 else div_tr[fro].col[div_tr[fro].tp]=(Data){c,Time,tmp};
	 }
}
int query(int x)
{
	 int Time=0,ret=0,Now,nl,nr,fro;
	 for(auto v:dot[x])
	 {
	 	 fro=v.fi,nl=1,nr=div_tr[fro].tp,Now=0;
		 while(nl<=nr)
	 	 {
	 	 	 int mid=(nl+nr)>>1;
	 	 	 if(div_tr[fro].col[mid].Dist>=v.second) Now=mid,nl=mid+1;
	 	 	 else nr=mid-1;
		 }
		 if(Now && div_tr[fro].col[Now].TIME>Time)
		 	 Time=div_tr[fro].col[Now].TIME,ret=div_tr[fro].col[Now].Color;
	 }
	 return ret;
}
int main()
{
	 n=rd();
	 for(int i=1,x,y,d;i<n;i++) x=rd(),y=rd(),d=rd(),add(x,y,d),add(y,x,d);
	 build(1),m=rd();
	 for(int i=1,opt,x,d,c;i<=m;i++)
	 {
	 	 opt=rd();
	 	 if(opt==1) x=rd(),d=rd(),c=rd(),change(x,d,c,i);
		 else x=rd(),printf("%d\n",query(x));
	 }
	 return 0;
}

P3241 [HNOI2015]开店

维护一颗带点权、边权树,每次给出 \(x,l,r\),查询 \(\sum_{l\le A_y \le r}dis(x,y)\),其中 \(A_y\)\(y\) 的点权。
\(n\le 1.5*10^5,A\le 10^9\)

首先想到对于查询的区间用查分变为一个前缀查询操作。

我们按照模板建出所有分治结构,记录这个分治结构中,到所有权值 \(\le d\) 的所有点的距离值和。

按照贡献 \(1\)\(-1\) 可以正好容斥解决所有点到查询的点的距离之和。

$\texttt{code}$
#define Maxn 150005
#define pa pair<int,int>
#define fi first
#define se second
#define pb push_back
typedef long long ll;
int n,q,A,tot,subsiz,rt,cnt,tmp_siz,tmp_li;
int hea[Maxn],nex[Maxn<<1],ver[Maxn<<1],edg[Maxn<<1];
int val[Maxn],siz[Maxn],weight[Maxn<<1],in_tr[Maxn];
bool used[Maxn];
pa tmp[Maxn];
vector<pa> dot[Maxn];
unordered_map<int,int> tr_in;
struct Divided_tree
{
	 int _n,st;
	 vector<ll> sum,Count;
	 vector<int> In_tr;
	 inline void init(int x)
	 { _n=x,sum.resize(x+1),In_tr.resize(x+1),Count.resize(x+1); }
	 inline void to_sum()
	 { for(int i=1;i<=_n;i++) sum[i]+=sum[i-1],Count[i]+=Count[i-1]; }
	 inline ll Query(int x,ll d)
	 {
	 	 if(x<0) return 0;
	 	 int nl=1,nr=_n,ret=0;
	 	 while(nl<=nr)
	 	 {
	 	 	 int mid=(nl+nr)>>1;
	 	 	 if(In_tr[mid]<=x) ret=mid,nl=mid+1;
	 	 	 else nr=mid-1;
		 }
		 return sum[ret]+d*Count[ret];
	 }
}div_tr[Maxn<<1];
inline void add(int x,int y,int d)
{ ver[++tot]=y,nex[tot]=hea[x],hea[x]=tot,edg[tot]=d; }
void Find1(int x,int fa)
{
	 siz[x]=1,subsiz++;
	 for(int i=hea[x];i;i=nex[i])
	 	 if(ver[i]!=fa && !used[ver[i]])
	 	 	 Find1(ver[i],x),siz[x]+=siz[ver[i]];
}
void Find2(int x,int fa)
{
	 bool isrt=true;
	 for(int i=hea[x];i;i=nex[i])
	 	 if(ver[i]!=fa && !used[ver[i]])
	 	 {
	 	 	 Find2(ver[i],x);
	 	 	 if((siz[ver[i]]<<1)>subsiz) isrt=false;
		 }
	 if(((subsiz-siz[x])<<1)>subsiz) isrt=false;
	 if(isrt) rt=x;
}
bool cmp(int x,int y){ return x<y; }
void update1(int x,int fa,int dep)
{
	 tmp[++tmp_siz]=pa(x,dep),in_tr[tmp_siz]=val[x];
	 for(int i=hea[x];i;i=nex[i])
	 	 if(ver[i]!=fa && !used[ver[i]])
	 	 	 update1(ver[i],x,dep+edg[i]);
}
void update2()
{
	 sort(in_tr+1,in_tr+tmp_siz+1,cmp);
	 tmp_li=unique(in_tr+1,in_tr+tmp_siz+1)-in_tr-1;
	 div_tr[cnt].init(tmp_li);
	 for(int i=1;i<=tmp_li;i++)
	 	 tr_in[in_tr[i]]=i,div_tr[cnt].In_tr[i]=1ll*in_tr[i];
	 for(int i=1;i<=tmp_siz;i++)
	 	 dot[tmp[i].fi].pb(pa(cnt,tmp[i].se)),
		 div_tr[cnt].sum[tr_in[val[tmp[i].fi]]]+=tmp[i].se,
		 div_tr[cnt].Count[tr_in[val[tmp[i].fi]]]++;
	 div_tr[cnt].to_sum();
}
void build(int x)
{
	 subsiz=0,Find1(x,0),Find2(x,0);
	 weight[++cnt]=1,tmp_siz=0,update1(rt,0,0),update2(),div_tr[cnt].st=rt;
	 for(int i=hea[rt];i;i=nex[i]) if(!used[ver[i]])
	 	 weight[++cnt]=-1,tmp_siz=0,update1(ver[i],rt,edg[i]),update2(),
	 	 div_tr[cnt].st=rt;
	 used[rt]=true;
	 for(int i=hea[rt];i;i=nex[i]) if(!used[ver[i]]) build(ver[i]);
}
inline ll query(int x,int d)
{
	 ll ret=0;
	 for(auto v:dot[x]) ret+=weight[v.fi]*div_tr[v.fi].Query(d,v.se) ;
	 return ret;
}
int main()
{
	 n=rd(),q=rd(),A=rd();
	 for(int i=1;i<=n;i++) val[i]=rd();
	 for(int i=1,x,y,d;i<n;i++) x=rd(),y=rd(),d=rd(),add(x,y,d),add(y,x,d);
	 build(1);
	 ll Lastans=0;
	 for(int i=1,u,a,b,l,r;i<=q;i++)
	 {
	 	 u=rd(),a=rd(),b=rd();
	 	 l=min((a+Lastans)%A,(b+Lastans)%A);
		 r=max((a+Lastans)%A,(b+Lastans)%A);
		 Lastans=query(u,1ll*r)-query(u,1ll*l-1ll);
		 printf("%lld\n",Lastans);
	 }
	 return 0;
}
原文地址:https://www.cnblogs.com/EricQian/p/15613795.html