树分治
点分治
点分治适合处理大规模的树上路径信息问题。
本质思想是选择一点作为分治中心,将原问题划分为几个相同的子树上的问题,进行递归解决。
常见的用于统计树上有关路径的信息。假设当前选定根节点为 \(rt\),则符合条件的路径必然是以下两种之一:
- 经过 \(rt\) 或一段在 \(rt\) 上;
- 在 \(rt\) 的子树上。
P3806
给定一棵树,边有权值。
多次询问,查询树上是否有长度为 \(k\) 的路径。
先随意取一个点作为根节点 \(rt\)。对于经过 \(rt\) 的路径,先枚举其所有子节点 \(v\),以 \(v\) 为根计算 \(v\) 子树中所有节点到 \(rt\) 的距离。
即 \(dis_i\) 为当前节点 \(i\) 到 \(rt\) 的距离,\(pd_{key}\) 为之前处理过的子树中是否存在一个节点 \(v\) 使得 \(dis_v=key\)。
若一个询问的 \(k\) 满足 \(pd_{k-dis_i}={true}\) 则存在一个长度为 \(k\) 的路径。
在计算完 \(v\) 子树中所连的边能否成为答案后,将这些新的距离加入到 \(pd\) 中。
注意清空 \(pd\) 时应将之前占用过的 \(pd\) 位置加入到一个队列中清空,依此来保证时间复杂度。
点分治过程中,每一层的所有递归过程合计对每个点处理一次,假设共递归 \(h\) 层,则总时间复杂度为 \(\mathcal O(hn)\)。
若每次选择子树的重心作为根节点,可以保证递归层数最少,时间复杂度为 \(\mathcal O(n\log n)\)。
const int N=1e4+5;
const int M=1e7+5;
int n,m,q[N],rt,sum,siz[N],dp[N],dis[N],pd[M],ret[N],vis[N],d[N],tot;
vector<tuple<int,int>>G[N];
queue<int>tag;
inline void getroot(int u,int f){
siz[u]=1;dp[u]=0;
for(auto[v,w]:G[u])
if(v!=f and !vis[v]){
getroot(v,u);
siz[u]+=siz[v];
dp[u]=max(dp[u],siz[v]);
}
dp[u]=max(dp[u],sum-siz[u]);
if(dp[u]<dp[rt])
rt=u;
}
inline void getdis(int u,int f){
d[++tot]=dis[u];
for(auto[v,w]:G[u])
if(v!=f and !vis[v]){
dis[v]=dis[u]+w;
getdis(v,u);
}
}
inline void solve(int u,int f){
pd[0]=vis[u]=1;
tag.push(0);
for(auto[v,w]:G[u])
if(v!=f and !vis[v]){
dis[v]=w;
getdis(v,u);
for(int k=1;k<=tot;k++)
for(int i=1;i<=m;i++)
if(q[i]>=d[k])
ret[i]|=pd[q[i]-d[k]];
for(int k=1;k<=tot;k++)
if(d[k]<M)
tag.push(d[k]),pd[d[k]]=1;
tot=0;
}
while(tag.size())
pd[tag.front()]=0,tag.pop();
for(auto[v,w]:G[u])
if(v!=f and !vis[v]){
sum=siz[v];
dp[rt=0]=inf;
getroot(v,u);
solve(rt,u);
}
}
signed main(){
sum=n=read();m=read();
for(int i=1;i<n;i++){
int u=read(),v=read(),w=read();
G[u].eb(mt(v,w));G[v].eb(mt(u,w));
}
for(int i=1;i<=m;i++)
q[i]=read();
dp[rt=0]=inf;
getroot(1,0);
solve(rt,0);
for(int i=1;i<=m;i++)
puts(ret[i]?"AYE":"NAY");
return 0;
}
P4178
给定一棵树,边有权值。
查询树上长度小于等于 \(k\) 的路径数量。
递归时,对于当前根节点 \(rt\),把每一个子节点到 \(rt\) 的距离排序,然后用类似双指针的方法来求小于等于 \(k\) 的边的数量。
在同一子树内的路径是不合法的,但是同样也会算进答案中。
所以对 \(rt\) 的每一条边进行遍历,利用容斥的思想减去不合法的路径,把经过了从重心到新遍历的点的边两次的路径剪掉,统计答案。
const int N=4e4+5;
int n,k,q[N],rt,sum,siz[N],dp[N],dis[N],ret[N],vis[N],d[N],tot,ans;
vector<tuple<int,int>>G[N];
queue<int>tag;
inline void getroot(int u,int f){
siz[u]=1;dp[u]=0;
for(auto[v,w]:G[u])
if(v!=f and !vis[v]){
getroot(v,u);
siz[u]+=siz[v];
dp[u]=max(dp[u],siz[v]);
}
dp[u]=max(dp[u],sum-siz[u]);
if(dp[u]<dp[rt])
rt=u;
}
inline void getdis(int u,int f){
d[++tot]=dis[u];
for(auto[v,w]:G[u])
if(v!=f and !vis[v]){
dis[v]=dis[u]+w;
getdis(v,u);
}
}
inline int work(int u,int w){
tot=0;dis[u]=w;
getdis(u,0);
sort(d+1,d+1+tot);
int l=1,r=tot,res=0;
while(l<=r)
if(d[l]+d[r]<=k)
res+=r-l,l++;
else
r--;
return res;
}
inline void solve(int u,int f){
vis[u]=1;ans+=work(u,0);
for(auto[v,w]:G[u])
if(v!=f and !vis[v]){
ans-=work(v,w);
sum=siz[v];
dp[0]=n,rt=0;
getroot(v,u);
solve(rt,u);
}
}
signed main(){
n=read();
for(int i=1;i<n;i++){
int u=read(),v=read(),w=read();
G[u].eb(mt(v,w));G[v].eb(mt(u,w));
}
k=read();
dp[rt=0]=inf;
getroot(1,0);
solve(rt,0);
write(ans);
return c0;
}
P4149
给一棵树,边有权值。
求一条简单路径,权值和等于 \(k\),且边的数量最小。
递归时,对于当前根节点 \(rt\),计算所有经过 \(r\) 的路径,可以开个桶记录。
具体地,令 \(mn_i\) 为从 \(i\) 开始的权值为 \(i\) 的所有路中边数的最小值。
令 \(dis_i\) 为当前点 \(i\) 到 \(rt\) 的路径长度,\(cnt_i\) 为当前点 \(i\) 到 \(rt\) 的边数。
更新答案时,用当前子树 \(u\) 的 \(dis\) 和前面子树的桶来更新,即 \(ans=\min\limits_{v\in son_u}(ans,cnt_v+mn_{k-dis_v})\)。
const int N=2e6+5;
int n,k,siz[N],vis[N],rt,dp[N],sum,dis[N],d[N],cnt[N],c[N],tot,mn[N],ans;
vector<tuple<int,int>>G[N];
inline void getroot(int u,int fa){
siz[u]=1;dp[u]=0;
for(auto[v,w]:G[u])
if(v!=fa and !vis[v]){
getroot(v,u);
siz[u]+=siz[v];
dp[u]=max(dp[u],siz[v]);
}
dp[u]=max(dp[u],sum-siz[u]);
if(dp[u]<dp[rt])
rt=u;
}
inline void getdis(int u,int fa){
if(dis[u]>k)
return;
d[++tot]=dis[u];c[tot]=cnt[u];
for(auto[v,w]:G[u])
if(v!=fa and !vis[v]){
dis[v]=dis[u]+w;
cnt[v]=cnt[u]+1;
getdis(v,u);
}
}
inline void work(int u){
tot=0;mn[0]=0;
for(auto[v,w]:G[u])
if(!vis[v]){
int tmp=tot;
dis[v]=w;
cnt[v]=1;
getdis(v,u);
for(int i=tmp+1;i<=tot;i++)
ans=min(ans,mn[k-d[i]]+c[i]);
for(int i=tmp+1;i<=tot;i++)
mn[d[i]]=min(mn[d[i]],c[i]);
}
for(int i=1;i<=tot;i++)
mn[d[i]]=inf;
}
inline void solve(int u,int fa){
vis[u]=1;
work(u);
for(auto[v,w]:G[u])
if(v!=fa and !vis[v]){
sum=siz[v];rt=0;
getroot(v,u);
solve(rt,u);
}
}
signed main(){
sum=n=read();k=read();ans=inf;
for(int i=1;i<n;i++){
int u=read()+1,v=read()+1,w=read();
G[u].eb(mt(v,w));G[v].eb(mt(u,w));
}
dp[rt=0]=inf;
memset(mn,0x3f,sizeof(mn));
getroot(1,0);
solve(rt,0);
write((ans>=n)?-1:ans);
return 0;
}
P2634
给一棵树,边有权值。
求任意选定两点,其最短路径的长度为 \(3\) 的倍数的概率。
递归时,对于当前根节点 \(rt\),令 \(cnt_i\) 为其子树内所有点在模 \(3\) 意义下到 \(rt\) 距离为 \(i\) 的数量。
直接统计即可,答案就是 \(cnt_0^2+2\times cnt_1\times cnt_2\)。
const int N=2e4+5;
int n,dis[N],vis[N],d[N],tot,siz[N],dp[N],sum,rt,t[4],ans;
vector<tuple<int,int>>G[N];
inline int gcd(int x,int y){return y?gcd(y,x%y):x;}
inline void getroot(int u,int f){
siz[u]=1;dp[u]=0;
for(auto[v,w]:G[u])
if(v!=f and !vis[v]){
getroot(v,u);
siz[u]+=siz[v];
dp[u]=max(dp[u],siz[v]);
}
dp[u]=max(dp[u],sum-siz[u]);
if(dp[u]<dp[rt])
rt=u;
}
inline void getdis(int u,int f){
t[dis[u]]++;
for(auto[v,w]:G[u])
if(v!=f and !vis[v]){
dis[v]=(dis[u]+w)%3;
getdis(v,u);
}
}
inline int work(int u,int w){
t[0]=t[1]=t[2]=0;
dis[u]=w%3;
getdis(u,0);
return t[0]*t[0]+2*t[1]*t[2];
}
inline void solve(int u,int f){
vis[u]=1;ans+=work(u,0);
for(auto[v,w]:G[u])
if(v!=f and !vis[v]){
ans-=work(v,w);
dp[rt=0]=inf;
sum=siz[v];
getroot(v,u);
solve(rt,u);
}
}
signed main(){
sum=n=read();
for(int i=1;i<n;i++){
int u=read(),v=read(),w=read();
G[u].eb(mt(v,w));G[v].eb(mt(u,w));
}
dp[rt=0]=inf;
getroot(1,0);
solve(rt,0);
int g=gcd(ans,n*n);
printf("%lld/%lld",ans/g,n*n/g);
return 0;
}
P2664
给一棵树,点有颜色。
定义 \(s(i,j)\) 为 \(i\) 到 \(j\) 的颜色的数量。定义 \(sum_i=\sum_{j=1}^ns(i,j)\)。
对所有的 \(1\leq i\leq n\),求所有的 \(sum_i\)。
发现 \(sum_i\) 在点分治中是不好统计答案的。
考虑转化。考虑每个颜色对 \(sum_i\) 的贡献。
对于每个颜色 \(j\),其中一个端点为 \(i\) 且含有颜色 \(j\) 的路径数量记为 \(cnt_j\),那么 \(sum_i=\sum cnt_j\)。
\(cnt_j\) 是较好处理的。只需要每遇到一个新颜色,有 \(cnt_{col_u}\leftarrow cnt_{col_u}+size_u\),表示这个子树里的所有节点都在这个颜色上对 \(u\) 的答案有贡献。
考虑到点分治过程中,需要分别考虑统计:
- 子树中以当前根节点为端点的路径对根的贡献;
- LCA 为当前根节点的路径对子树内每个点的贡献。
对于 1 部分,比较好办,由于点分治中,递归层数不超过 \(\mathcal O(\log n)\),所以每一层都可以遍历所有子树。
此时可以用 \(sum_i\) 的定义来在遍历子树的过程中顺便统计。
对于 2 部分,设当前根节点为 \(rt\),一个子节点为 \(u\),\(u\) 的子树内任取一节点为 \(v\),那么 \(v\) 的答案可分为两部分:
- \((u,v)\) 上出现过的颜色,数量设为 \(num\),\(rt\) 除了 \(u\) 以外的其他所有子树的总大小设为 \(size_1\),那么这些出现过的颜色对 \(v\) 的答案贡献为 \(num\times size_1\)。
- \((u,v)\) 上没出现过的颜色 \(j\),它们的贡献来自于 \(rt\) 除了 \(u\) 以外的所有其他子树的 \(cnt_j\),这一部分的答案为 \(\sum_{j\notin(u,v)}cnt_j\)。
const int N=2e5+5;
int n,dp[N],rt,nn,vis[N],siz[N],c[N];
vector<int>G[N];
inline void getroot(int u,int f){
siz[u]=1;dp[u]=0;
for(auto v:G[u])
if(v!=f and !vis[v]){
getroot(v,u);
siz[u]+=siz[v];
dp[u]=max(dp[u],siz[v]);
}
dp[u]=max(dp[u],nn-siz[u]);
if(dp[u]<dp[rt])
rt=u;
}
int ans[N],sum,cnt[N],mk[N],nowrt;
inline void getdis(int u,int f,int now){
siz[u]=1;
if(!mk[c[u]]){
sum-=cnt[c[u]];
now++;
}
mk[c[u]]++;
ans[u]+=sum+now*siz[nowrt];
for(auto v:G[u])
if(v!=f and !vis[v]){
getdis(v,u,now);
siz[u]+=siz[v];
}
mk[c[u]]--;
if(!mk[c[u]])
sum+=cnt[c[u]];
}
inline void getcnt(int u,int f){
if(!mk[c[u]]){
cnt[c[u]]+=siz[u];
sum+=siz[u];
}
mk[c[u]]++;
for(auto v:G[u])
if(v!=f and !vis[v])
getcnt(v,u);
mk[c[u]]--;
}
inline void clr(int u,int f,int now){
if(!mk[c[u]])
now++;
mk[c[u]]++;
ans[u]-=now;
ans[nowrt]+=now;
for(auto v:G[u])
if(v!=f and !vis[v])
clr(v,u,now);
mk[c[u]]--;
cnt[c[u]]=0;
}
inline void clr2(int u,int f){
cnt[c[u]]=0;
for(auto v:G[u])
if(v!=f and !vis[v])
clr2(v,u);
}
int son[N];
inline void solve(int u){
vis[u]=1;
int tot=0;nowrt=u;ans[u]++;
for(auto v:G[u])
if(!vis[v])
son[++tot]=v;
siz[u]=sum=cnt[c[u]]=1;mk[c[u]]++;
for(int i=1;i<=tot;i++){
int v=son[i];
getdis(v,u,0);getcnt(v,u);
siz[u]+=siz[v];
cnt[c[u]]+=siz[v];
sum+=siz[v];
}
clr2(u,0);
siz[u]=sum=cnt[c[u]]=1;
for(int i=tot;i;i--){
int v=son[i];
getdis(v,u,0);getcnt(v,u);
siz[u]+=siz[v];
cnt[c[u]]+=siz[v];
sum+=siz[v];
}
mk[c[u]]--;
clr(u,0,0);
for(auto v:G[u])
if(!vis[v]){
nn=siz[v];
dp[rt=0]=inf;
getroot(v,u);
solve(rt);
}
}
signed main(){
nn=n=read();
for(int i=1;i<=n;i++)
c[i]=read();
for(int i=1;i<n;i++){
int u=read(),v=read();
G[u].eb(v);G[v].eb(u);
}
dp[rt=0]=inf;
getroot(1,0);
solve(rt);
for(int i=1;i<=n;i++)
write(ans[i]),puts("");
return 0;
}
边分治
和点分治类似,边分治每次选一条边,考虑跨过这条边的路径贡献。
为了保证复杂度,会让两边子树大小尽量接近。
发现菊花图无论怎么分治都是无效的,考虑将原树转化为二叉树。
具体地,如果 \(u\) 有两个儿子,就新建一个节点连接第二个儿子,如果还有更多的儿子,就再新建节点与上一次新建的节点当前的儿子相连,这样复杂度是正确的。
具体过程和点分治类似,求当前的子树大小,求当前的重心边,递归处理子问题。
边分治具有优美的性质,每次分治只会考虑两棵子树之间的贡献,也就完全不需要点分治中容斥掉自己贡献的操作。缺点是点数翻倍。
P4565
给定两棵树,求 \({depth}(x)+{depth}(y)-({depth}({LCA}(x,y))+{depth}^{\prime}({LCA}^{\prime}(x,y)))\) 的最大值。
考虑把答案改写成 \(\dfrac 12\left({dis}(x,y)+{depth}(x)+{depth}(y)-2\times{depth}^{\prime}({LCA}^{\prime}(x,y))\right)\)。
对第一棵树边分治,并建出每个点从根到这个点的边分树,
对于边分树上每个非叶子结点,维护 \(vl,vr\) 分别表示左、右子树所包含的原树中的节点中 \({dis}(x)+{depth}(x)\) 的最大值,其中 \({dis}(x)\) 是 \(x\) 到原树中这个点对应的边某一个端点的距离。
然后对第二棵树 dfs,枚举 \({LCA}^{\prime}(x,y)\),每遇到一条边,就把两个端点的边分树按照线段树的合并方法合并起来,然后在合并过程中统计答案。
假设这两棵边分树同时包括某一个点,这个点在两棵边分树的编号分别是 \(u,v\)。
那么可以用 \(\max(vl_u+vr_v,vr_u+vl_v)-2\times{depth}^{\prime}({LCA}^{\prime}(x,y))\) 来更新全局答案并更新合并后的点的 \(vl,vr\)。
不难发现这样统计的点对 \((x,y)\) 的 \({LCA}\) 一定都是我们当前枚举到的点。
const int N=366666+5;
template<unsigned M>struct graph{
int to[M<<1],pre[M<<1],lst[M],tot,w[M<<1];
inline void add(int u,int y,int z){
to[++tot]=y;
pre[tot]=lst[u];
lst[u]=tot;
w[tot]=z;
}
};
graph<N>g1,g3;graph<N<<1>g2;
int n,now,siz[N<<1],nows,ed,li,cnt,mx[N<<5|1][2],ch[N<<5|1][2],lst[N],dis[N],tmp,ans=-inf,rt[N];
bool del[N<<2];
inline void dfs(int u,int fa){
int las=u;
for(int i=g1.lst[u];i;i=g1.pre[i]){
int v=g1.to[i];
if(v==fa)
continue;
g2.add(las,++now,0),g2.add(now,las,0),g2.add(now,v,g1.w[i]),g2.add(v,now,g1.w[i]);
dis[v]=dis[u]+g1.w[i];las=now;dfs(v,u);
}
}
inline void dfs2(int u,int fa,int nowd,int op){
if(!op)
tmp++;
if(u<=n){
cnt++;
if(!lst[u])
rt[u]=lst[u]=cnt,cnt++;
ch[lst[u]][op]=cnt;
mx[lst[u]][op]=dis[u]+nowd;
lst[u]=cnt;
}
for(int i=g2.lst[u];i;i=g2.pre[i]){
int v=g2.to[i];
if(v==fa or del[i])
continue;
dfs2(v,u,nowd+g2.w[i],op);
}
}
inline void get(int u,int fa){
siz[u]=1;
for(int i=g2.lst[u];i;i=g2.pre[i]){
int v=g2.to[i];
if(v==fa or del[i])
continue;
get(v,u);
siz[u]+=siz[v];
if(max(siz[v],nows-siz[v])<li)
li=max(siz[v],nows-siz[v]),ed=(i+1)>>1;
}
}
inline void solve(int u,int s){
if(s==1)
return;
ed=li=1e9;nows=s;
get(u,0);
int r1=g2.to[(ed<<1)-1],r2=g2.to[ed<<1];
del[(ed<<1)-1]=del[ed<<1]=1;tmp=0;
dfs2(r1,r2,0,0),dfs2(r2,r1,g2.w[ed<<1],1);
int tt=siz[u]-tmp;
solve(r1,tmp);solve(r2,tt);
}
inline int merge(int u,int y,int t){
if((!u)||(!y))return u+y;
ans=max(ans,max(mx[u][0]+mx[y][1],mx[y][0]+mx[u][1])+2*t);
mx[u][0]=max(mx[u][0],mx[y][0]),mx[u][1]=max(mx[u][1],mx[y][1]);
ch[u][0]=merge(ch[u][0],ch[y][0],t),ch[u][1]=merge(ch[u][1],ch[y][1],t);
return u;
}
inline void dfs3(int u,int fa,int nowd){
ans=max(ans,2*(dis[u]-nowd));
for(int i=g3.lst[u];i;i=g3.pre[i]){
int v=g3.to[i];
if(v==fa)
continue;
dfs3(v,u,nowd+g3.w[i]);
rt[u]=merge(rt[u],rt[v],-nowd);
}
}
signed main(){
memset(mx,0xc0,sizeof(mx));
now=n=read();
for(int i=1;i<n;i++){
int u=read(),v=read(),w=read();
g1.add(u,v,w);g1.add(v,u,w);
}
dfs(1,0);
solve(1,now);
for(int i=1;i<n;i++){
int u=read(),v=read(),w=read();
g3.add(u,v,w);g3.add(v,u,w);
}
dfs3(1,0,0);
write(ans>>1);
return 0;
}
点分树
点分树是通过更改原树形态使树的层数变为稳定 \(\mathcal O(\log n)\) 的一种重构树。
通过点分治每次找重心的方式来对原树进行重构。
将每次找到的重心与上一层的重心缔结父子关系,这样就可以形成一棵 \(\mathcal O(\log n)\) 层的树。
常用于解决与树原形态无关的带修改问题。比如路径问题,联通块问题,寻找关键点问题等等。
点分树有以下性质:
- 高度与点分治的深度一样,只有 \(\mathcal O(\log n)\)。
- 对于任意两点 \(u,v\),唯一可以确定的是 \(u,v\) 在点分树上的 LCA 一定在路径 \(u\leadsto v\) 上。
P6329
给定一棵树,点有点权。
多次询问,单点修改,或查询 \(\sum\limits_{{dis}(x,y)\leq k}a_y\)。
考虑枚举 \(x,y\) 在点分树上的 LCA \(z\)。显然有 \({dis}(x,y)={dis}(x,z)+{dis}(z,y)\)。
所以有 \(ans=\sum\limits_{{dis}(x,z)+{dis}(z,y)\leq k\&{LCA}(x,y)=z}a_y=\sum\limits_{{dis}(z,y)\leq k-{dis}(x,z)\&{LCA}(x,y)=z}a_y\)。
考虑什么样的 \(y\) 满足 \({LCA}(x,y)=z\)。显然其组成的集合为 \(z\) 的子树去掉在 \(z\) 在 \(x\) 方向上的儿子 \(s\) 的子树。
现在要求的是在这个集合中距离 \(\leq k-{dis}(x,z)\) 的点权和,这个是可以差分的。
那么对每个点 \(x\) 建一棵动态开点线段树,下标为 \(i\) 的位置维护 \(x\) 子树内所有 \({dis}(x,z)=i\) 的 \(a_z\) 的和。
因此求 \(x\) 子树内到 \(x\) 的距离 \(\leq k-{dis}(x,z)\) 的点权和就在对应线段树上查个区间和就行了。
考虑求 \(z\) 在 \(x\) 方向上的儿子 \(s\) 的子树。对于每个点再建立一棵动态开点线段树,线段树上下标为 \(i\) 的位置维护 \(x\) 子树内到 \(fa_x\) 距离为 \(i\) 的点权和。求解时直接查询 \(\left[0,k-{dis}(x,z)\right]\) 的和即可。
const int N=1e5+5;
int n,q,a[N],siz[N],son[N],dep[N],fa[N],top[N];
vector<int>G[N];
inline void dfs(int u,int f){
fa[u]=f;siz[u]=1;dep[u]=dep[f]+1;
for(auto v:G[u])
if(v!=f){
dfs(v,u);
siz[u]+=siz[v];
if(siz[v]>siz[son[u]])
son[u]=v;
}
}
inline void dfs2(int u,int topf){
top[u]=topf;
if(son[u])
dfs2(son[u],topf);
for(auto v:G[u])
if(v!=fa[u] and v!=son[u])
dfs2(v,v);
}
inline int lca(int x,int y){
while(top[x]!=top[y]){
if(dep[top[x]]<dep[top[y]])
swap(x,y);
x=fa[top[x]];
}
return dep[x]<dep[y]?x:y;
}
inline int getdis(int x,int y){
return dep[x]+dep[y]-(dep[lca(x,y)]<<1);
}
int dp[N],cent;
bool vis[N];
inline void findcent(int x,int f,int tot){
siz[x]=1;dp[x]=0;
for(auto y:G[x])
if(y!=f and !vis[y]){
findcent(y,x,tot);
dp[x]=max(dp[x],siz[y]);
siz[x]+=siz[y];
}
dp[x]=max(dp[x],tot-siz[x]);
if(dp[x]<dp[cent])
cent=x;
}
int dfa[N];
inline void divcent(int x,int tot){
vis[x]=1;
for(auto y:G[x])
if(!vis[y]){
cent=0;
int sz=(siz[y]<siz[x])?siz[x]:(tot-siz[x]);
findcent(y,x,sz);dfa[cent]=x;divcent(cent,sz);
}
}
struct segtree{
int rt[N],cnt=0;
struct node{int l,r,val;}t[N<<5];
inline void pushup(int p){
t[p].val=t[t[p].l].val+t[t[p].r].val;
}
inline void modify(int &p,int l,int r,int x,int v){
if(!p)
p=++cnt;
if(l==r){
t[p].val+=v;
return;
}
int mid=(l+r)>>1;
if(x<=mid)
modify(t[p].l,l,mid,x,v);
else
modify(t[p].r,mid+1,r,x,v);
pushup(p);
}
inline int query(int p,int l,int r,int x,int y){
if(!p)
return 0;
if(x<=l and r<=y)
return t[p].val;
int mid=(l+r)>>1;
if(y<=mid)
return query(t[p].l,l,mid,x,y);
else if(x>mid)
return query(t[p].r,mid+1,r,x,y);
else
return query(t[p].l,l,mid,x,mid)+query(t[p].r,mid+1,r,mid+1,y);
}
}w1,w2;
inline void modify(int x,int v){
int cur=x;
while(cur){
w1.modify(w1.rt[cur],0,n-1,getdis(cur,x),v);
if(dfa[cur])
w2.modify(w2.rt[cur],0,n-1,getdis(dfa[cur],x),v);
cur=dfa[cur];
}
}
inline int query(int x,int k){
int cur=x,pre=0,res=0;
while(cur){
if(getdis(cur,x)>k){
pre=cur;cur=dfa[cur];
continue;
}
res+=w1.query(w1.rt[cur],0,n-1,0,k-getdis(cur,x));
if(pre)
res-=w2.query(w2.rt[pre],0,n-1,0,k-getdis(cur,x));
pre=cur;cur=dfa[cur];
}
return res;
}
signed main(){
n=read();q=read();
for(int i=1;i<=n;i++)
a[i]=read();
for(int i=1;i<n;i++){
int u=read(),v=read();
G[u].eb(v);G[v].eb(u);
}
dfs(1,0);dfs2(1,1);
dp[0]=inf;cent=0;
findcent(1,0,n);divcent(cent,n);
for(int i=1;i<=n;i++)
modify(i,a[i]);
int lst=0;
while(q--){
int opt=read(),x=read(),y=read();
x^=lst;y^=lst;
if(opt==0){
lst=query(x,y);
write(lst);puts("");
}else{
modify(x,y-a[x]);
a[x]=y;
}
}
return 0;
}
P3241
给定一棵树,点有点权,边有边权,点的度数小于等于 3。
多次询问,给定 \(x,l,r\),令 \(a_y\) 为点 \(y\) 的点权,查询 \(\sum\limits_{l\leq a_y\leq r}dis(x,y)\)。
建立完点分树后,对每个点 \(u\),开两个 vector 按照 \(x_i\) 排序并取前缀和,分别存储 \(u\) 子树中的点到 \(u\) 的距离以及 \(u\) 子树中的点到 \(fa_u\) 的距离。
对于每一次查询,在点分树上暴力跳父亲节点,直接在 vector 上二分查找满足 \([l,r]\) 区间的点和距离,直接计算即可。
const int N=2e5+5;
int n,Q,A,x[N],vis[N],rt,ans;
vector<tuple<int,int>>G[N];
struct Father{int u,num,dis;};
vector<Father> fa[150010];
struct Son{
int age,dis;
inline bool operator<(const Son& a)const{
return age<a.age;
}
};
vector<Son>son[N][3];
inline int getsiz(int u,int f){
if(vis[u])
return 0;
int res=1;
for(auto[v,w]:G[u])
if(v!=f and !vis[v])
res+=getsiz(v,u);
return res;
}
inline int getrt(int u,int f,int sum,int &rt){
if(vis[u])
return 0;
int res=1,mx=0;
for(auto[v,w]:G[u])
if(v!=f and !vis[v]){
int V=getrt(v,u,sum,rt);
mx=max(mx,V);
res+=V;
}
mx=max(mx,sum-res);
if(mx<=sum/2)
rt=u;
return res;
}
inline void getdis(int u,int f,int dis,int rt,int k,vector<Son>&p){
if(vis[u])
return;
fa[u].push_back({rt,k,dis,});
p.push_back({x[u],dis});
for(auto[v,w]:G[u])
if(v!=f and !vis[v])
getdis(v,u,dis+w,rt,k,p);
}
inline void divid(int u){
if(vis[u])
return;
getrt(u,0,getsiz(u,0),u);
vis[u]=1;
int k=0;
for(auto[v,w]:G[u])
if(!vis[v]){
auto&p=son[u][k];
p.push_back({-1,0});
p.push_back({A+1,0});
getdis(v,0,w,u,k,p);
sort(p.begin(),p.end());
for(int i=1;i<p.size();i++)
p[i].dis+=p[i-1].dis;
k++;
}
for(auto[v,w]:G[u])
if(!vis[v])
divid(v);
}
inline int query(int u,int l,int r){
int res=0;
for(auto&t:fa[u]){
int Age=x[t.u];
if(Age>=l and Age<=r)
res+=t.dis;
for(int i=0;i<3;i++){
if(i==t.num)
continue;
auto& p=son[t.u][i];
if(p.empty())
continue;
int a=lower_bound(p.begin(),p.end(),Son({l,-1}))-p.begin();
int b=lower_bound(p.begin(),p.end(),Son({r+1,-1}))-p.begin();
res+=t.dis*(b-a)+p[b-1].dis-p[a-1].dis;
}
}
for(int i=0;i<3;i++){
auto&p=son[u][i];
if(p.empty())
continue;
int a=lower_bound(p.begin(),p.end(),Son({l,-1}))-p.begin();
int b=lower_bound(p.begin(),p.end(),Son({r+1,-1}))-p.begin();
res+=p[b-1].dis-p[a-1].dis;
}
return res;
}
signed main(){
n=read();Q=read(),A=read();
for(int i=1;i<=n;i++)
x[i]=read();
for(int i=1;i<n;i++){
int u=read(),v=read(),w=read();
G[u].eb(mt(v,w));G[v].eb(mt(u,w));
}
divid(1);
while(Q--){
int u=read(),l=(read()+ans)%A,r=(read()+ans)%A;
if(l>r)
swap(l,r);
ans=query(u,l,r);
write(ans);puts("");
}
return 0;
}
P3345
给定一棵树,点有点权,边有边权,求带权重心,点带修。
点的度数小于等于 20。
令 \(s_u\) 为 \(u\) 子树内 \(d\) 的和,\(sd_u\) 和 \(sch_u\),记录子树内 \(d\times dis\) 的和,直接容斥即可。
不难注意到一个性质,令当前答案为 \(u\),若 \(u\) 的某个儿子 \(v\) 更优,则有 \(s_v<s_u-s_v\),这个是只有一个的。
所以可以在点分树上暴力跳父亲求答案,时间复杂度为 \(\mathcal O(20n\log^2 n)\)。
const int N=1e5+5;
const int lgN=21;
vector<tuple<int,int>>G[N];
namespace Tree{
int dep[N],siz[N],son[N],fa[N],top[N],dis[N];
inline void dfs(int u,int f){
siz[u]=1;fa[u]=f;dep[u]=dep[f]+1;
for(auto[v,w]:G[u])
if(v!=f){
dis[v]=dis[u]+w;
dfs(v,u);
siz[u]+=siz[v];
if(siz[son[u]]>siz[v])
son[u]=v;
}
}
inline void dfs2(int u,int topf){
top[u]=topf;
if(son[u])
dfs2(son[u],topf);
for(auto[v,w]:G[u])
if(v!=fa[u] and v!=son[u])
dfs2(v,v);
}
inline int lca(int x,int y){
while(top[x]!=top[y]){
if(dep[top[x]]<dep[top[y]])
swap(x,y);
x=fa[top[x]];
}
return dep[x]<dep[y]?x:y;
}
inline int getdis(int x,int y){
return dis[x]+dis[y]-(dis[lca(x,y)]<<1);
}
};
int n,Q,rt,sum,dp[N],siz[N],fa[N],dis[N][lgN],vis[N],top[N],dep[N];
inline void getroot(int u,int f){
siz[u]=1;dp[u]=0;
for(auto[v,w]:G[u])
if(v!=f and !vis[v]){
getroot(v,u);
siz[u]+=siz[v];
dp[u]=max(dp[u],siz[v]);
}
dp[u]=max(dp[u],sum-siz[u]);
if(dp[u]<dp[rt])
rt=u;
}
inline void dfs(int u,int f,int topf){
top[u]=topf;
for(auto[v,w]:G[u])
if(v!=f and !vis[v])
dfs(v,u,topf);
}
inline void divid(int u){
vis[u]=1;
for(auto[v,w]:G[u])
if(!vis[v]){
dp[rt=0]=inf;
sum=siz[v];
getroot(v,0);
dfs(v,0,rt);
fa[rt]=u;dep[rt]=dep[u]+1;
divid(rt);
}
}
int s[N],sd[N],sch[N];
inline int calc(int u){
int res=sch[u];
for(int p=u;fa[p];p=fa[p]){
int d=dis[u][dep[u]-dep[fa[p]]];
res+=d*(s[fa[p]]-s[p])+(sch[fa[p]]-sd[p]);
}
return res;
}
inline int query(int u){
int tmp=calc(u);
for(auto[v,w]:G[u])
if(calc(v)<tmp)
return query(top[v]);
return tmp;
}
signed main(){
sum=n=read();Q=read();
for(int i=1;i<n;i++){
int u=read(),v=read(),w=read();
G[u].eb(mt(v,w));G[v].eb(mt(u,w));
}
dp[rt=0]=inf;
getroot(1,0);int RT=rt;divid(rt);
Tree::dfs(1,0);Tree::dfs2(1,1);
for(int i=1;i<=n;i++)
for(int j=fa[i];j;j=fa[j])
dis[i][dep[i]-dep[j]]=Tree::getdis(i,j);
while(Q--){
int x=read(),y=read();
s[x]+=y;
for(int p=x;fa[p];p=fa[p]){
int d=dis[x][dep[x]-dep[fa[p]]];
s[fa[p]]+=y;sch[fa[p]]+=y*d;sd[p]+=y*d;
}
write(query(RT));puts("");
}
return 0;
}