对于一棵树,我们可以通过深度优先搜索记录到达每一个点的时间戳,由这个时间戳构成的序列就是dfs序,每个时间戳代表一个节点。
我们可以通过以下代码实现:
void dfs(int u)
{
st[u]=++tot;//记录子树开始的访问时间
for(int re i=f[u];i;i=nxp[i])
{
int v=e[i].v;
if(!st[v])
{
dep[v]=dep[u]+1;
dfs(v)
};
}
ed[u]=tot;//记录子树访问结束的时间
}
对于这样的dfs序,我们就能利用树状数组进行维护单点信息,修改单点,维护子树信息,修改子树,维护路径,修改路径等操作。(如果涉及复杂路径修改或一些复杂的信息维护,最好使用树链剖分与线段树实现)
问题一:单点修改,子树查询
对于这样的问题,我们只需要直接用树状数组进行单点修改,通过树状数组求得区间[st[root]-1,ed[root]]即可求得以root为根的子树和。
例题大意:
给出一个苹果树,每个节点一开始都有苹果
C X,如果X点有苹果,则拿掉,如果没有,则新长出一个
Q X,查询X点与它的所有后代分支一共有几个苹果
#include<iostream>
#include<cstdio>
#include<cstring>
#include<string>
#include<algorithm>
#include<cmath>
#include<queue>
#define re register
using namespace std;
int n,m,a,b,s,t;
const int N=2e5+1;
int f[N];
int c[N];
int g[N];
int nxp[N<<2|1];
int cnt=0,tot=0;
struct ndeo{
int u,v;
}e[N];
int idx=0;
inline void add(int u,int v)
{
e[++cnt].u=u;
e[cnt].v=v;
nxp[cnt]=f[u];
f[u]=cnt;
}
inline int low(int x){return x&(-x);}
inline void change(int k,int v)
{
while(k<=idx)
{
c[k]+=v;
k+=low(k);
}
}
inline int ask(int k)
{
int ret=0;
while(k>0)
{
ret+=c[k];
k-=low(k);
}
return ret;
}
int st[N];
int ed[N];
void dfs(int u)
{
st[u]=++idx;
for(int re i=f[u];i;i=nxp[i])
{
int v=e[i].v;
if(!st[v])dfs(v);
}
ed[u]=idx;
}
char p;
int main()
{
scanf("%d",&n);
for(int re i=1;i<n;i++)
{
scanf("%d%d",&a,&b);
add(a,b);
add(b,a);
g[i]=1;
}
g[n]=1;
scanf("%d",&m);
dfs(1);
for(int re i=1;i<=n;i++)
change(st[i],1);
for(int re i=1;i<=m;i++)
{
scanf("\n%c%d",&p,&a);
if(p=='C')
{
if(g[a])change(st[a],-1);
else change(st[a],1);
g[a]^=1;
}
else printf("%d\n",ask(ed[a])-ask(st[a]-1));
}
}
问题二:树上路径修改,单点查询
对于这么一个题,我们可以利用树上差分的思想:
修改x->y的路径,就等价于
x->root +v;
y->root +v;
lca(x,y)->root -v;
fa[lca(x,y)]->root -v;
于是问题的修改又可以转换为单点修改。而对于一个节点y的权值,其它节点会对其产生影响,当且仅当其它节点在y的子树内。若y不受x的路径影响,y子树中必有一部分节点+v,一部分节点-v,则对y没有影响。若y受x的路径影响,则y中的子树和就会增大v。这一点可以画图举例理解。所以我们可以维护子树和,修改单点,就能解决这个问题。
例题大意:
有n个节点N-1条边,这是一颗树,有2个操作:
1 x y v:表示将节点x到y最短路径上所有的点的权值+v
2 x:表示查询节点x的权值
开始的时候每个节点的权值是0
#include<iostream>
#include<cstdio>
#include<cstring>
#include<string>
#include<algorithm>
#include<cmath>
#include<queue>
#define re register
using namespace std;
int n,m,a,b,s,t;
const int N=2e5+1;
int f[N];
int c[N];
int g[N];
int nxp[N<<2|1];
int cnt=0,tot=0;
struct ndeo{
int u,v;
}e[N];
int idx=0;
inline void add(int u,int v)
{
e[++cnt].u=u;
e[cnt].v=v;
nxp[cnt]=f[u];
f[u]=cnt;
}
inline int low(int x){return x&(-x);}
inline void change(int k,int v)
{
while(k<=idx)
{
c[k]+=v;
k+=low(k);
}
}
inline int ask(int k)
{
int ret=0;
while(k>0)
{
ret+=c[k];
k-=low(k);
}
return ret;
}
int fa[N][20];
int st[N];
int ed[N];
int dep[N];
void dfs(int u)
{
st[u]=++idx;
for(int re i=1;(1<<i)<=dep[u];i++)
fa[u][i]=fa[fa[u][i-1]][i-1];
for(int re i=f[u];i;i=nxp[i])
{
int v=e[i].v;
if(!dep[v])
{
dep[v]=dep[u]+1;
fa[v][0]=u;
dfs(v);
}
}
ed[u]=idx;
}
int lca(int a,int b)
{
if(dep[a]<dep[b])swap(a,b);
int t=dep[a]-dep[b];
for(int re i=0;(1<<i)<=t;i++)
if(t&(1<<i))a=fa[a][i];
if(a==b)return a;
for(int re i=18;i>=0;i--)
{
if(fa[a][i]!=fa[b][i])
{
a=fa[a][i];
b=fa[b][i];
}
}
return fa[a][0];
}
char p;
int main()
{
scanf("%d",&n);
for(int re i=1;i<n;i++)
{
scanf("%d%d",&a,&b);
add(a,b);add(b,a);
}
scanf("%d",&m);
dep[1]=1;
dfs(1);
for(int re i=1;i<=m;i++)
{
int q,z;
scanf("%d",&q);
if(q==1)
{
scanf("%d%d%d",&a,&b,&z);
int lc=lca(a,b);
change(st[a],z);
change(st[b],z);
change(st[lc],-z);
if(lc!=1)
change(st[fa[lc][0]],-z);
}
else
{
scanf("%d",&a);
printf("%d\n",ask(ed[a])-ask(st[a]-1));
}
}
}
问题三:树上路径修改,子树查询
我们考虑假设X在Y的子树内,那么对于询问点Y,询问值会加
上W[x] * (depth[x] - depth[y]+ 1)。
故整颗子树所受影响即:
∑
x
在
y
子
树
内
w
[
x
]
∗
(
d
e
p
[
x
]
−
d
e
p
[
y
]
+
1
)
\sum_{x在y子树内}{w[x]*(dep[x]-dep[y]+1)}
x在y子树内∑w[x]∗(dep[x]−dep[y]+1)
拆开可以得到:
∑
x
在
y
子
树
内
w
[
x
]
∗
(
d
e
p
[
x
]
+
1
)
−
d
e
p
[
y
]
∗
∑
x
在
y
子
树
内
w
[
x
]
\sum_{x在y子树内}{w[x]*(dep[x]+1)}-dep[y]*\sum_{x在y子树内} {w[x]}
x在y子树内∑w[x]∗(dep[x]+1)−dep[y]∗x在y子树内∑w[x]
对于每一次修改,我们是可以知道x的,因此我们可以维护两个值w[x]和w[x]*(dep[x]+1)的子树和,询问时再处理回答即可。
例题大意:
有n个节点N-1条边,这是一颗树,有2个操作:
1 x y v:表示将节点x到y最短路径上所有的点的权值+v
2 x:表示查询子树x的权值和
开始的时候每个节点的权值是0
代码:
#include<iostream>
#include<cstdio>
#include<cstring>
#include<string>
#include<algorithm>
#include<cmath>
#include<queue>
#define re register
using namespace std;
int n,m,a,b,s,t;
const int N=2e5+1;
int f[N];
int g[N];
int nxp[N<<2|1];
int cnt=0,tot=0;
struct ndeo{
int u,v;
}e[N];
int idx=0;
inline int low(int x){return x&(-x);}
struct tree{
int c[N];
inline void change(int k,int v)
{
while(k<=idx)
{
c[k]+=v;
k+=low(k);
}
}
inline int ask(int k)
{
int ret=0;
while(k>0)
{
ret+=c[k];
k-=low(k);
}
return ret;
}
}t1,t2;//t1:w,t2:(dep(x)+1)*w
inline void add(int u,int v)
{
e[++cnt].u=u;
e[cnt].v=v;
nxp[cnt]=f[u];
f[u]=cnt;
}
int fa[N][20];
int st[N];
int ed[N];
int dep[N];
void dfs(int u)
{
st[u]=++idx;
for(int re i=1;(1<<i)<=dep[u];i++)
fa[u][i]=fa[fa[u][i-1]][i-1];
for(int re i=f[u];i;i=nxp[i])
{
int v=e[i].v;
if(!dep[v])
{
dep[v]=dep[u]+1;
fa[v][0]=u;
dfs(v);
}
}
ed[u]=idx;
}
int lca(int a,int b)
{
if(dep[a]<dep[b])swap(a,b);
int t=dep[a]-dep[b];
for(int re i=0;(1<<i)<=t;i++)
if(t&(1<<i))a=fa[a][i];
if(a==b)return a;
for(int re i=18;i>=0;i--)
{
if(fa[a][i]!=fa[b][i])
{
a=fa[a][i];
b=fa[b][i];
}
}
return fa[a][0];
}
char p;
int main()
{
scanf("%d",&n);
for(int re i=1;i<n;i++)
{
scanf("%d%d",&a,&b);
add(a,b);add(b,a);
}
scanf("%d",&m);
dep[1]=1;
dfs(1);
for(int re i=1;i<=m;i++)
{
int q,z;
scanf("%d",&q);
if(q==1)
{
scanf("%d%d%d",&a,&b,&z);
int lc=lca(a,b);
t1.change(st[a],z);
t2.change(st[a],z*(dep[a]+1));
t1.change(st[b],z);
t2.change(st[b],z*(dep[b]+1));
t1.change(st[lc],-z);
t2.change(st[lc],-z*(dep[lc]+1));
if(lc!=1)
t1.change(st[fa[lc][0]],-z),
t2.change(st[fa[lc][0]],-z*(dep[fa[lc][0]]+1));
}
else
{
scanf("%d",&a);
printf("%d\n",t2.ask(ed[a])-t2.ask(st[a]-1)-dep[a]*(t1.ask(ed[a])-t1.ask(st[a]-1)));
}
}
}
问题四:单点修改,路径询问
这一类的题我们可以利用lca把路径询问转换为x到根节点的询问。
d
i
s
(
x
,
y
)
=
d
i
s
(
x
,
r
o
o
t
)
+
d
i
s
(
y
,
r
o
o
t
)
−
d
i
s
(
l
c
a
(
x
,
y
)
,
r
o
o
t
)
−
d
i
s
(
f
a
[
l
c
a
(
x
,
y
)
]
,
r
o
o
t
)
dis(x,y)=dis(x,root)+dis(y,root)-dis(lca(x,y),root)-dis(fa[lca(x,y)],root)
dis(x,y)=dis(x,root)+dis(y,root)−dis(lca(x,y),root)−dis(fa[lca(x,y)],root)
可以画图理解一下。
而我们思考什么时候对于x的修改会影响y的询问。显而易见,y到
root的距离被x影响当且仅当y在x的子树内,因此我们可以维护这样一个d数组,每次修改时d[st[x]]+=v,d[ed[x]+1]-=v,前缀和即节点到根的距离。我们可以这么理解,在st[x]左边的区间内,这样的修改不会对前缀和有影响,在st[x]->ed[x]的区间内,前缀和增加了v,在ed[x]右边的区间,显然前缀和也不受到影响,故可以证明这样的修改只会影响子树内节点到root的距离。
例题大意:
有n个节点N-1条边,这是一颗树,有2个操作:
1 x v:表示将节点x的权值+v
2 x y:表示查询x到y的路径权值和
代码:
#include<iostream>
#include<cstdio>
#include<cstring>
#include<string>
#include<algorithm>
#include<cmath>
#include<queue>
#define re register
using namespace std;
int n,m,a,b,s,t;
const int N=2e5+1;
int f[N];
int c[N];
int g[N];
int nxp[N<<2|1];
int cnt=0,tot=0;
struct ndeo{
int u,v;
}e[N];
int idx=0;
inline void add(int u,int v)
{
e[++cnt].u=u;
e[cnt].v=v;
nxp[cnt]=f[u];
f[u]=cnt;
}
inline int low(int x){return x&(-x);}
inline void change(int k,int v)
{
while(k<=idx)
{
c[k]+=v;
k+=low(k);
}
}
inline int ask(int k)
{
int ret=0;
while(k>0)
{
ret+=c[k];
k-=low(k);
}
return ret;
}
int fa[N][20];
int st[N];
int ed[N];
int dep[N];
void dfs(int u)
{
st[u]=++idx;
for(int re i=1;(1<<i)<=dep[u];i++)
fa[u][i]=fa[fa[u][i-1]][i-1];
for(int re i=f[u];i;i=nxp[i])
{
int v=e[i].v;
if(!dep[v])
{
dep[v]=dep[u]+1;
fa[v][0]=u;
dfs(v);
}
}
ed[u]=idx;
}
int lca(int a,int b)
{
if(dep[a]<dep[b])swap(a,b);
int t=dep[a]-dep[b];
for(int re i=0;(1<<i)<=t;i++)
if(t&(1<<i))a=fa[a][i];
if(a==b)return a;
for(int re i=18;i>=0;i--)
{
if(fa[a][i]!=fa[b][i])
{
a=fa[a][i];
b=fa[b][i];
}
}
return fa[a][0];
}
char p;
int sum[N];
int main()
{
scanf("%d",&n);
for(int re i=1;i<=n;i++)scanf("%d",&g[i]);
for(int re i=1;i<n;i++)
{
scanf("%d%d",&a,&b);
add(a,b);add(b,a);
}
scanf("%d",&m);
dep[1]=1;
dfs(1);
for(int re i=1;i<=n;i++)
{
change(st[i],g[i]);
change(ed[i]+1,-g[i]);
}
for(int re i=1;i<=m;i++)
{
int q,z;
scanf("%d",&q);
if(q==1)
{
scanf("%d%d",&a,&b);
change(st[a],b);
change(ed[a]+1,-b);
}
else
{
scanf("%d%d",&a,&b);
int lc=lca(a,b);
printf("%d\n",ask(st[a])+ask(st[b])-ask(st[lc])-ask(st[fa[lc][0]]));
}
}
}
问题五:子树修改,单点查询
我们考虑X对Y 的贡献.显然,当X在Y的子树里才会对Y有贡献,贡献为W。于是转化为修改一个点权,查询点到根的路径的权值和。于是就转化为了问题四。
问题六:子树修改,子树查询
线段树或树状数组即可在dfs序中实现区间修改,区间查询。
(其实上述问题几乎都可以用dfs序加线段树维护。俗话说得好,智商不够,数据结构来凑 )
问题七:子树修改,路径查询
对子树X的所有权值增加W,查询x到y路径上的权值和把最短路转化为x到根的权值和。考虑修改X对Y的贡献,显然Y在X子树中才有贡献,贡献为wx*(dep(x)-dep(y)+1),分离开
发现与Y无关,照例分为2部分处理。每部分相当于修改一个点权,查询某个点到跟路径和,每部分相当于问题四。
【例题】:「HAOI2015」树上操作
【题目描述】
有一棵点数为 N 的树,以点 1 为根,且树点有边权。然后有 M 个操作,分为三种:
1:把某个节点 x 的点权增加 a 。
2:把某个节点 x 为根的子树中所有点的点权都增加 a 。
3:询问某个节点 x 到根的路径中所有点的点权和。
【输入】
第一行包含两个整数 N,M。表示点数和操作数。
接下来一行 N 个整数,表示树中节点的初始权值。
接下来 N−1 行每行两个正整数 fr,to , 表示该树中存在一条边 (fr,to)
再接下来 M 行,每行分别表示一次操作。其中第一个数表示该操作的种类(1-3) ,之后接这个操作的参数(x 或者 x a) 。
【思路】分别对每一种修改对询问的影响值进行维护,答案即所有影响的和。对于点的初值,可以理解为单点修改。
代码:
#include<cstdio>
#include<iostream>
#include<queue>
#include<algorithm>
#include<cstring>
#include<string>
#include<vector>
#define re register
using namespace std;
const long long N=1e5+5;
long long n;
inline long long low(long long x){return x&(-x);}
struct tree{
long long c[100001];
inline void change(long long k,long long v)
{
while(k<=n){
c[k]+=v;
k+=low(k);
}
}
inline long long ask(long long k)
{
long long ret=0;
while(k>0)
{
ret+=c[k];
k-=low(k);
}
return ret;
}
}t1,t2,t3;
/*
opt2
t2:(dep(x)-1)*w2
t3:w2
opt1
t1:w1
*/
long long a,b;
long long st[N];
long long ed[N],tot=0,opt;
long long m,x;
long long f[N];
long long nxp[N<<1|1];
long long cnt=0;
struct node{
long long u,v;
}e[N<<1|1];
inline void add(long long u,long long v)
{
e[++cnt].u=u;
e[cnt].v=v;
nxp[cnt]=f[u];
f[u]=cnt;
}
long long val[N];
long long dep[N];
void dfs(long long u)
{
st[u]=++tot;
for(long long re i=f[u];i;i=nxp[i])
{
long long v=e[i].v;
if(!st[v])dep[v]=dep[u]+1,dfs(v);
}
ed[u]=tot;
}
inline long long query1(long long x)
{
return t1.ask(st[x]);
}
inline long long query2(long long x)
{
return dep[x]*t3.ask(st[x])-t2.ask(st[x]);
}
int main()
{
scanf("%lld%lld",&n,&m);
for(long long re i=1;i<=n;i++)scanf("%lld",&val[i]);
for(long long re i=1;i<=n-1;i++)
{
scanf("%lld%lld",&a,&b);
add(a,b);
add(b,a);
}
dep[1]=1;
dfs(1);
for(long long re i=1;i<=n;i++)
{
t1.change(st[i],val[i]);
t1.change(ed[i]+1,-val[i]);
}
for(long long re i=1;i<=m;i++)
{
scanf("%lld",&opt);
if(opt==1)
{
scanf("%lld%lld",&x,&a);
t1.change(st[x],a);
t1.change(ed[x]+1,-a);
}
else if(opt==2)
{
scanf("%lld%lld",&x,&a);
t2.change(st[x],(dep[x]-1)*a);
t2.change(ed[x]+1,-(dep[x]-1)*a);
t3.change(st[x],a);
t3.change(ed[x]+1,-a);
}
else
{
scanf("%lld",&x);
printf("%lld\n",query1(x)+query2(x));
}
}
}
【总结】
对于这一类型的题,我们其实需要思考修改对询问值的影响,思考每一次修改对某个点的贡献,再根据式子分离已知进行维护。否则,使用线段树数据结构加以辅助也可。