(氡态)淀粉质
点分治常用于树上路径统计等问题。
点分治
每次分治过程大致如下:
-
我们先求出当前连通块树的重心;
-
处理与重心有关的答案;
-
删除重心
-
递归处理与重心相连的子连通块。
伪代码如下:
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\) 层,由点分治每次选择重心可以得出这个结论。那么以后的操作我们完全可以花这 \(\log n\) 的时间从这个点走到根节点。
- 如果将每个虚点看作一个连通块(这个点就是这个连通块的重心),那么父亲的连通块一定包含所有儿子的连通块。
- 接下来我们可利用容斥 zhuo 题啦。
古老模板伪代码
// 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\) 的所有点的点权之和。
以这道题为例,怎么在虚树上容斥呢?
设:\(f1(p,k)=\sum_{x\in subtree(p),dis(x,p)\le k}A_x\),表示在和 \(p\) 一个联通块内所有到 \(p\) 距离小于等于 \(k\) 的点权之和。
\(f2(p,k)=\sum_{x\in subtree(p),dis(x,fa_p)\le k}A_x\),表示在和 \(p\) 一个连通块内所有到 \(fa_p\) 距离小于等于 \(k\) 的点权之和。
那么 \(F(p,k)=f1(p,k)-f2(p,k)\) 就是在 \(p\) 的连通块内所有 \(dis(x,p)\le k\) 且 \(dis(x,fa_p)>k\) 的点权之和。
答案就是 \(f1(rt,k)+\sum_{i\in fatree(x)}F(i,k)\)。
存储的时候不用将整棵树建出来,建出来也没有太大用处,可以对每个点记录有哪些分支结构经过了它即可。
$\texttt{code}$
#define inf 0x3f3f3f3f
#define Maxn 100005
#define pa pair<int,int>
#define fi first
#define se second
#define9 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;
}
P3920 [WC2014]紫荆花之恋
点分治在线了怎么做?
先不考虑其他的,假设已经在之前的树建好了点分树,那么新加入的点的贡献怎么算。
如果 \(dis(i,j)\le r_i+r_j\),可以找到路径上一个点使得 \(dis(i,u)+dis(j,u)\le r_i+r_j\),移项得 \(dis(i,u)-r_i\le r_j-dis(u,j)\)。
发现要按照震波的套路,从下往上依次统计即可。
\(\bigstar\texttt{Attention}\):由于这里的权值非常的大,而且没有办法离散化,所有考虑用平衡树代替。
考虑如何构建点分树:将新的点在点分树上挂到父亲点的下边,那么这个询问就解决了,但是如果一直是这样的话最终树的形态会变得极为丑陋,怎么办?定期重构!
类似替罪羊树的思想,我们每加入一个点的时候检查一下是否存在一对父子是的父亲的 \(size\times \alpha\) 小于孩子的 \(size\),如果存在这样的不平衡就找到最高的这样的点,对着整一个连通块进行重构。
接着只要对所有信息重新获取即可。
口胡之,跑路。