点分治
淀粉质
考虑一个问题:给定一棵树,统计树的路径中长度小于等于 (k) 的路径条数
显然,我们需要选定一个点为根,将其转化为有根树进行处理,设为 (x)
对于节点 (x) 来说,它子树中的简单路径包括两种情况:
- 经过节点 (x) ,由两条或一条路径拼接而成
- 不经过节点 (x) ,但是包含在 (x) 的子树中
对于所有的满足情况 (2) 的路径,我们都可以对子树递归地处理,从而转化为情况 (1) 的问题
那么最终的答案就是树中经过节点 (x) 的路径条数与 (x) 的所有以子节点为根的子树的答案之和
在处理情况 (1) 的问题时,可以先进行一次 DFS ,该子树中根到其他节点的路径长度处理出来,然后排序,二分,尝试合并
但是在这个时候,我们有可能会将同一棵子树中的路径进行合并,从而导致在之后的递归中重复计算
那么我们可以考虑用容斥,通过对子树的答案的处理,将这种情况减去即可
在处理子树前,注意已经选取了根到子树的这条边 ((u,v)),所以此时计算子树中的符合要求的答案时,目标长度要在原先的长度上减去两倍的 ((u,v)) 的边权
对于这个问题,我们发现一个节点被处理完,之后的计算过程中,它的子节点之间就不会相互影响,那么我们可以通过选取适当的递归节点来保证其时间复杂度
那么,我们就希望保证选取了一个点后,再进行递归计算时,每一个子问题都尽可能的小一些
所以对于一棵树中所选择的递归的子节点即为该树的重心,因为树的重心的子树的大小必定不超过 (lfloor{frac{n}{2}} floor)
那么递归的总次数就不会超过 (O(log ~n)) 级别
总体复杂度即为 (O(n ~log ~ n^2))
例题一
本题与上面所将的引例相似
点分治的大体过程都是相同的,其中最有差别的地方就是对问题的求解计算
我们可以和上述方法一样,将以 (x) 为根的树中的以 (x) 为端点的简单路径都处理出来
在排序之后利用双指针的技巧尝试拼接两条路径,如果可以拼出一条符合要求长度的路径,并且该路径不在同一子树中,那么就存在符合条件的路径
对于多组询问的情况,离线处理即可
#include<iostream>
#include<cstdio>
#include<cstring>
#include<cstdlib>
#include<algorithm>
#include<math.h>
#include<vector>
#include<queue>
#define ll long long
inline ll read()
{
ll x=0,f=1;
char ch=getchar();
while(!isdigit(ch))
{
if(ch=='-') f=-1;
ch=getchar();
}
while(isdigit(ch))
{
x=(x<<1)+(x<<3)+ch-'0';
ch=getchar();
}
return x*f;
}
const ll maxn=1e4+10;
ll n,m,k,tot,root,ret;
ll head[maxn*2],siz[maxn],maxx[maxn],dis[maxn],dep[maxn],vis[maxn],a[maxn],b[maxn];
ll ask[maxn],judge[maxn];
struct node
{
ll u,v,w,nxt;
} s[maxn*2];
inline void add(ll u,ll v,ll w)
{
s[++tot].v=v;
s[tot].w=w;
s[tot].nxt=head[u];
head[u]=tot;
}
inline void dfs(ll x,ll f,ll sum)
{
siz[x]=1;
maxx[x]=0;
for(int i=head[x];i;i=s[i].nxt)
{
ll y=s[i].v;
if(y==f||vis[y]) continue;
dfs(y,x,sum);
siz[x]+=siz[y];
maxx[x]=std::max(siz[y],maxx[x]);
}
maxx[x]=std::max(maxx[x],sum-siz[x]);
if(!root||maxx[x]<maxx[root]) root=x;
}
inline void ddfs(ll x,ll f,ll d,ll top)
{
a[++ret]=x;
b[x]=top;
dis[x]=d;
for(int i=head[x];i;i=s[i].nxt)
{
ll y=s[i].v;
ll w=s[i].w;
if(y==f||vis[y]) continue;
ddfs(y,x,d+w,top);
}
}
inline bool cmp(ll a,ll b)
{
return dis[a]<dis[b];
}
inline void calc(ll x)
{
ret=0;
a[++ret]=x;
dis[x]=0;
b[x]=x;
for(int i=head[x];i;i=s[i].nxt)
{
ll y=s[i].v;
ll z=s[i].w;
if(vis[y]) continue;
ddfs(y,x,z,y);
}
std::sort(a+1,a+ret+1,cmp);
// for(int i=1;i<=ret;i++)
// {
// printf("%lld %lld %lld
",a[i],b[i],dis[i]);
// }
for(int i=1;i<=m;i++)
{
ll l=1,r=ret;
while(l<r)
{
if(dis[a[l]]+dis[a[r]]>ask[i])
{
r--;
}
else if(dis[a[l]]+dis[a[r]]<ask[i])
{
l++;
}
else if(b[a[l]]==b[a[r]])
{
if(dis[a[r]]==dis[a[r-1]]) r--;
else l++;
}
else
{
judge[i]=1;
break;
}
}
}
}
inline void solve(ll x)
{
vis[x]=1;
calc(x);
for(int i=head[x];i;i=s[i].nxt)
{
ll y=s[i].v;
if(vis[y]) continue;
root=0;
dfs(y,x,siz[y]);
solve(root);
}
}
int main(void)
{
n=read(),m=read();
for(int i=1;i<=n-1;i++)
{
ll x=read(),y=read(),z=read();
add(x,y,z);
add(y,x,z);
}
for(int i=1;i<=m;i++)
{
ask[i]=read();
}
maxx[0]=n;
dfs(1,0,n);
// printf("%lld
",root);
solve(root);
for(int i=1;i<=m;i++)
{
if(judge[i]) printf("AYE
");
else printf("NAY
");
}
return 0;
}
例题二
注意到 (k leq 10^6) ,我们可以开桶维护路径长度
记录处理答案过程中对应的当前树中一定长度的且一端为根节点的路径的最短边数,尝试合并
即
每一次计算完成当前树后递归地计算下一个子树即可
#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<math.h>
#include<cmath>
#include<climits>
#define ll int
inline ll read()
{
ll x=0,f=1;
char ch=getchar();
while(!isdigit(ch))
{
if(ch=='-') f=-1;
ch=getchar();
}
while(isdigit(ch))
{
x=(x<<1)+(x<<3)+ch-'0';
ch=getchar();
}
return x*f;
}
const ll maxn=2e5+10;
const ll maxk=1e6+10;
const ll inf=1e9;
ll n,k,sz,tot,rt,mx,ans=inf,sum;
ll head[maxn<<1],siz[maxn],maxx[maxn],vis[maxn],minn[maxk],cnt[maxn],len[maxn];
struct node
{
ll nxt,u,v,w;
} s[maxn<<1];
inline void add(ll u,ll v,ll w)
{
s[++tot].v=v;
s[tot].w=w;
s[tot].nxt=head[u];
head[u]=tot;
}
inline void dfs(ll x,ll f)
{
siz[x]=1;
maxx[x]=0;
for(int i=head[x];i;i=s[i].nxt)
{
ll v=s[i].v;
if(v==f||vis[v]) continue;
dfs(v,x);
siz[x]+=siz[v];
if(maxx[x]<siz[v]) maxx[x]=siz[v];
}
maxx[x]=std::max(maxx[x],sz-siz[x]);
if(mx>maxx[x])
{
rt=x;
mx=maxx[x];
}
}
inline void calc(ll x,ll f,ll dis,ll dep)
{
if(dis>k) return ;
len[++sum]=dis;
cnt[sum]=dep;
for(int i=head[x];i;i=s[i].nxt)
{
ll v=s[i].v;
ll w=s[i].w;
if(v==f||vis[v]) continue;
calc(v,x,dis+w,dep+1);
}
}
inline void get_ans(ll x)
{
minn[0]=0;
sum=0;
for(int i=head[x];i;i=s[i].nxt)
{
ll v=s[i].v;
ll w=s[i].w;
if(vis[v]) continue;
ll ret=sum;
calc(v,x,w,1);
for(int j=ret+1;j<=sum;j++) ans=std::min(ans,minn[k-len[j]]+cnt[j]);
for(int j=ret+1;j<=sum;j++)
{
minn[len[j]]=std::min(minn[len[j]],cnt[j]);
}
}
// printf("%d
",sum);
for(int i=1;i<=sum;i++)
{
// printf("%d %d
",len[i],minn[len[i]]);
minn[len[i]]=inf;
}
}
inline void solve(ll x)
{
vis[x]=1;
get_ans(x);
for(int i=head[x];i;i=s[i].nxt)
{
ll v=s[i].v;
if(vis[v]) continue;
sz=siz[v],mx=inf,rt=0;
dfs(v,x);
solve(rt);
// printf("%d
",rt);
}
}
int main(void)
{
// freopen("1.in","r",stdin);
// freopen("1.ans","w",stdout);
n=read(),k=read();
for(int x,y,z,i=1;i<=n-1;i++)
{
x=read()+1,y=read()+1,z=read();
add(x,y,z);
add(y,x,z);
}
sz=n,mx=inf,rt=0;
dfs(1,0);
memset(minn,0x3f,sizeof(minn));
// printf("%d
",rt);
solve(rt);
if(ans>=n)
{
printf("-1
");
}
else
{
printf("%d
",ans);
}
return 0;
}
例题三
首先,我们要求出一棵最短路径生成树
对 dijkstra
算法加以改造即可,即在进行松弛操作时,特殊处理一下 (dis[v] = dis[u] + w) 的情况,对其额外连一条边即可
加边,加边,加边,然后并查集查询(
构造最短路径生成树的代码:
inline void dij(ll x)
{
std::priority_queue<std::pair<ll,ll> > q;
memset(vis,0,sizeof(vis));
memset(dis,0x7f,sizeof(dis));
dis[x]=0;
q.push(std::make_pair(0,x));
while(q.size())
{
ll u=q.top().second;
q.pop();
if(vis[u]) continue;
vis[u]=1;
for(int i=head[u];i;i=s[i].nxt)
{
ll v=s[i].v;
ll w=s[i].w;
if(dis[v]>dis[u]+w)
{
dis[v]=dis[u]+w;
pre[v]=u;
val[v]=w;
q.push(std::make_pair(-dis[v],v));
}
else if(dis[v]==dis[u]+w&&w>val[v])
{
pre[v]=u;
val[v]=w;
}
}
}
}
inline void make()
{
for(int i=2;i<=n;i++)
{
add(i,pre[i],val[i]);//加边
add(pre[i],i,val[i]);
}
}
对于之后需要处理的经过 (k) 个点的最长路径长度及条数,采取与例题二相同的维护计算的方法,对最长的端点链与条数维护计算即可
点分树
上述的点分治是一种在树上静态维护的优秀算法,对于有点权或者边权修改的树上路径信息统计问题,可采用点分树解决
点分树,实际上就是利用点分治的思想,将点分治时的递归树建立出来,构成一棵新的树,当然,在具体处理中,我们往往不用真实地将这个树构建出来,只需要维护其中节点之间的父子关系即可
具体地,即在点分治的过程中,对于每一个子树的重心,将上一层分治时所找到的重心即为其父亲,即可构建出这棵点分树
那么,此时树的深度不超过 (O(log ~ n)) ,总复杂度即为 (O(n ~ log n))
对于每一次操作,在构建出的点分树上暴力向上找父亲节点,更改或查询即可
例题
首先,我们可以按照上述方法,通过点分治,将这个树进行处理,构建出一棵点分树,实际上就是把点分治过程中的答案统计变为了子树维护
设 (fa[i]) 表示 (i) 在点分树上的父亲节点, (father_i) 为 (i) 的父亲节点集合, (son_i) 为 (i) 在点分树上的子树集合, (dis_{i,j}) 为原树上 (i) , (j) 之间的距离
设 (f_1(i,j)) 为在虚树上以 (i) 为根的子树中到 (i) 点的距离小于等于 (j) 的点的权值之和
设 (f_2(i,j)) 为在虚树上以 (i) 为根的子树中到 (fa[i]) 点的距离小于等于 (j) 的店的全职之和
则:
那么,对于 (ans(x,k)) ,有
其中
即利用容斥原理, (G) 函数为以 (fa[i]) 为根的子树中除去 (i) 为根的子树的贡献, (ans(x,k)) 中单独加上一次 (f_1(x,k)) ,即可将容斥中被楼下的以 (i) 为根的子树的满足条件的答案统计上
维护时,由于需要频繁用到两点间的 LCA , 可以用 ST 表加欧拉序维护(或者是树链剖分)
对于每一个节点,我们可以维护两个动态开点线段树,分别处理 (f_1) 与 (f_2) 的值
而本题只需单点修改,区间查询,可以维护两个树状数组,利用 vector
维护即可实现,为了控制空间,以防 vetcor
导致的 (RE) ,可以原先进行 resize
分配其空间
Code
#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<vector>
#include<math.h>
#define ll int
using namespace std;
inline ll read()
{
ll x=0,f=1;
char ch=getchar();
while(!isdigit(ch))
{
if(ch=='-') f=-1;
ch=getchar();
}
while(isdigit(ch))
{
x=(x<<1)+(x<<3)+ch-'0';
ch=getchar();
}
return x*f;
}
const ll inf=1e9+7;
const ll maxn=2e5+10;
ll n,m,tot,mx,sz,rt,ans,cnt,sum;
ll head[maxn<<1],val[maxn],siz[maxn],maxx[maxn],fa[maxn],vis[maxn],dep[maxn],pos[maxn],lg[maxn<<1];
ll st[maxn<<1][21];
vector<ll> tre[2][maxn];
struct node
{
ll v,nxt;
} s[maxn<<1];
inline void add(ll u,ll v)
{
s[++tot].v=v;
s[tot].nxt=head[u];
head[u]=tot;
}
inline ll lowbit(ll x)
{
return x & -x;
}
inline ll qry(ll num,ll rot,ll p)
{
ll ret=0;
p+=1;
p=min(p,siz[rot]);
for(int i=p;i;i-=lowbit(i)) ret+=tre[num][rot][i];
return ret;
}
inline void upd(ll num,ll rot,ll p,ll x)
{
p+=1;
for(int i=p;i<=siz[rot];i+=lowbit(i)) tre[num][rot][i]+=x;
}
inline void dfs(ll x,ll f)
{
dep[x]=dep[f]+1,st[++cnt][0]=x,pos[x]=cnt;
for(int i=head[x];i;i=s[i].nxt)
{
ll v=s[i].v;
if(v==f) continue;
dfs(v,x);
st[++cnt][0]=x;
}
}
inline ll get_min(ll a,ll b)
{
return dep[a]<dep[b] ? a : b;
}
inline void pre()
{
for(int i=1;i<=cnt;i++) lg[i]=31-__builtin_clz(i);
for(int j=1;(1<<j)<=cnt;j++)
{
for(int i=1;i+(1<<j)<=cnt;i++)
{
st[i][j]=get_min(st[i][j-1],st[i+(1<<(j-1))][j-1]);
}
}
}
inline ll get_dis(ll u,ll v)
{
if(pos[u]>pos[v]) std::swap(u,v);
ll eu=pos[u],ev=pos[v];
ll len=ev-eu+1;
ll lca=get_min(st[eu][lg[len]],st[ev-(1<<lg[len])+1][lg[len]]);
return dep[u]+dep[v]-2*dep[lca];
}
inline void get_zx(ll x,ll f)
{
ll ret=0;
sum++;
siz[x]=1;
for(int i=head[x];i;i=s[i].nxt)
{
ll v=s[i].v;
if(vis[v]||f==v) continue;
get_zx(v,x);
siz[x]+=siz[v];
ret=max(ret,siz[v]);
}
ret=max(ret,sz-siz[x]);
if(mx>ret)
{
mx=ret;
rt=x;
}
}
inline void build(ll x)
{
vis[x]=1;
siz[x]=sum+1;
tre[0][x].resize(siz[x]+2);
tre[1][x].resize(siz[x]+2);
for(int i=head[x];i;i=s[i].nxt)
{
ll v=s[i].v;
if(vis[v]) continue;
sz=siz[v],rt=0,mx=inf,sum=0;
get_zx(v,0);
fa[rt]=x;
build(rt);
}
}
inline void fx(ll x,ll w)
{
for(int i=x;i;i=fa[i]) upd(0,i,get_dis(i,x),w);
for(int i=x;fa[i];i=fa[i]) upd(1,i,get_dis(fa[i],x),w);
}
int main(void)
{
n=read(),m=read();
for(int i=1;i<=n;i++) val[i]=read();
for(int i=1;i<=n-1;i++)
{
ll x=read(),y=read();
add(x,y);
add(y,x);
}
dfs(1,0);
pre();
mx=inf,sz=n,rt=0;
get_zx(1,0);
build(rt);
for(int i=1;i<=n;i++) fx(i,val[i]);
while(m--)
{
ll op=read(),x=read(),y=read();
x^=ans,y^=ans;
if(op==0)
{
ans=0;
ans+=qry(0,x,y);
for(int i=x;fa[i];i=fa[i])
{
// puts("1");
ll d=get_dis(x,fa[i]);
if(y>=d) ans+=qry(0,fa[i],y-d)-qry(1,i,y-d);
// printf("%lld %lld
",qry(0,fa[i],y-d),qry(1,i,y-d));
}
printf("%d
",ans);
}
if(op==1)
{
fx(x,y-val[x]);
val[x]=y;
}
}
return 0;
}