「学习笔记」DFS序和7种模型
DFS 序就是将树的节点按照先根的顺序遍历得到的节点顺序
性质:一个子树全在一个连续的区间内,可以与线段树和树状数组搭配使用
很好写,只需在 dfs 中加几行代码即可。
代码:
void dfs(ll u,ll fat)
{
st[u]=++tim;//st记录出现位置,tim记录当前的时间戳
dfn[tim]=u;//dfn记录dfs序
for(rll i=h[u];i;i=e[i].nxt)//遍历
{
ll v=e[i].v;
if(v!=fat)
{
dfs(v,u);//搜索
}
}
en[u]=tim;//记录结束位置
}
你以为这就完了,不,这才刚刚开始。
DFS 序通常与线段树、树状数组配合使用,这个使用,其实就是在 dfs 序上建立树状数组和线段树
DFS 序有 \(7\) 种典型的模型
一、点修改,子树和查询
题目传送门
大体意思:有一棵树,有点权,进行 \(m\) 次操作,有关于点权的修改,有查询子树和的操作,现在,按照要求完成代码。
修改点权,查子树和,子树在 DFS 序上又是连续的,这不就是单点修改,区间查询吗?因为是求和,我们搭配树状数组来完成题目。
先建立 DFS 序,在 DFS 序上建立树状数组,后面操作和树状数组基本一样,只是要注意,查询的区间的范围是这个节点在 DFS 序上的开始点 \(-1\) ~结束点,具体看代码,有注释
代码:
#include<bits/stdc++.h>
#define ll long long
#define rll register long long
using namespace std;
const ll N=1e6+5;
ll n,p,cnt,tim,m;
ll h[N],st[N],en[N],a[N],dfn[N],s[N];
struct edge
{
ll v,nxt;
} e[N<<1];
inline ll read()
{
ll x=0;
bool flag=false;
char ch=getchar();
while(ch<'0'||ch>'9')
{
if(ch=='-') flag=true;
ch=getchar();
}
while(ch>='0'&&ch<='9')
{
x=(x<<3)+(x<<1)+(ch^48);
ch=getchar();
}
return flag?~x+1:x;
}
void add(ll u,ll v)
{
e[++cnt].v=v;
e[cnt].nxt=h[u];
h[u]=cnt;
}
void dfs(ll u,ll fat)
{
st[u]=++tim;//建立DFS序,记录开始点
dfn[tim]=u;//这个其实有没有都一样,后边不会用到,可以用来查错误
//如果MLE了,优先考虑把dfn数组删除
for(rll i=h[u];i;i=e[i].nxt)
{
ll v=e[i].v;
if(v!=fat)
{
dfs(v,u);
}
}
en[u]=tim;//记录结束点
}
void pchange(ll x,ll y)//单点修改
{
for(rll i=x;i<=n;i+=(i&(-i)))
{
s[i]+=y;
}
}
ll sum(ll x)//区间查询
{
ll ans=0;
for(rll i=x;i;i-=(i&(-i)))
{
ans+=s[i];
}
return ans;
}
int main()
{
n=read(),m=read(),p=read();//n 节点数 m 操作数 p 根节点
for(rll i=1;i<=n;++i)
{
a[i]=read();//读入每个节点的信息
}
for(rll i=1;i<n;++i)
{
ll x=read(),y=read();
add(x,y);//建边
add(y,x);
}
dfs(p,0);//搜索
for(rll i=1;i<=n;++i)
{
pchange(st[i],a[i]);//先将各个点的原始信息存入树状数组
}
for(rll i=1;i<=m;++i)
{
ll op=read();
if(op==1)
{
ll x=read(),y=read();
pchange(st[x],y);
}
else
{
ll x=read();
printf("%lld\n",sum(en[x])-sum(st[x]-1));
//这里st[x]-1~en[x]是子树的范围,自己可以模拟理解一下
}
}
return 0;
}
二、三、(题目在一块儿,所以放一块讲)树链修改,点查询,子树和查询
题目传送门:
有一棵树,有点权,进行 \(m\) 次操作,有关于树链的修改,有查询点的操作,有查询子树和的操作,现在,按要求完成代码。
树链修改,就是把点 \(a\) 到点 \(b\) 的链上所有的节点都加上 \(v\)。
在这里,我们要搞清楚一个地方,就是这个修改是给谁做贡献!
这里要用树上差分,以后做补充,这里简单讲一下。
\((a->b)\) 的修改对 \((a->b)\) 上的点的贡献是 \(v\),我们再把他拆分一下,\((a->lca)\) 和 \((b->lca)\),这样改的话,那我们就将 \(a\) , \(b\) 点的修改的含义变为 \((a->root)\) 和 \((b->root)\) 上的点有加 \(v\) ,\(So\), \((lca->root)\) 上的点就加了两次,我们要抵消这次加法,所以在点 \(lca\) 上- \(2 \times v\),但 \(lca\) 属于树链 \(a->\) b上的点,也要 \(+v\) ,所以 \(lca\) 上相当于只 \(-v\) ,我们只能把 \(lca\) 的父节点 \(-v\) ,以到平衡。
代码实现就是
add(st[a],v);add(st[b],v),add(st[lca],-v),add(st[fa[lca]],-v)
我们将每个点的修改都放到树状数组中,最后求和,再加上原数据即可,具体看代码,有注释。
求子树和的链修改也是一样的道理,但是,修改点 \(x\) ,对一棵树 \(y\) 的贡献值是不一样多的,它的贡献是 \((deep_y-deep_x+1) \times v\) ( \(v\) 这里是修改值),我们将式子拆开,就变成了 \(deep_y \times v-(deep_x-1) \times v\) ,\(deep_y\) 、 \(deep_x\) 是可知的, \(v\) 在输入后也是可知的,但对于一整棵子树,公式就变成了 \(\sum deep_y \times v-(deep_x-1) \times v,deep_y \times v\) 我们可以通过树状数组来记录求和,\(deep_x - 1\) 是已知的,我们只要在记录 \(v\) 的和就好了,当然,我们现在统计的只是变化的数值,到时还要在加上原数据,原数据单独存在一个数组中,不会敲可以看代码,有注释。
#include<bits/stdc++.h>
#define ll long long
#define rll register long long
#define rint register int
using namespace std;
const ll N=1e6+5;
ll n,cnt,m,p,tim;
int a[N],h[N],st[N],en[N];//a 每个点的信息 h 遍历需要 st 记录开始位置 en记录结束位置
int stf[N][20],lg[N],deep[N];//stf st表求lca lg 处理log deep 记录深度
ll ss[N],s1[N],s2[N];//ss 存储原始子树和 s1 存储每个数的变化 s2 存储乘积
struct edge
{
int v,nxt;
} e[N<<1];
inline ll read()//快读
{
ll x=0;
bool flag=false;
char ch=getchar();
while(ch<'0'||ch>'9')
{
if(ch=='-') flag=true;
ch=getchar();
}
while(ch>='0'&&ch<='9')
{
x=(x<<3)+(x<<1)+(ch^48);
ch=getchar();
}
return flag?~x+1:x;
}
void add(int u,int v)
{
e[++cnt].v=v;
e[cnt].nxt=h[u];
h[u]=cnt;
}
void dfs(int u,int fat)
{
st[u]=++tim;//记录开始位置
deep[u]=deep[fat]+1;//记录深度
stf[u][0]=fat;//记录父节点,更新st表
ss[u]=a[u];//记录子树和,先把当前节点加上
for(rint i=1;i<=lg[deep[u]];++i)//更新st表
{
stf[u][i]=stf[stf[u][i-1]][i-1];
}
for(rint i=h[u];i;i=e[i].nxt)//遍历
{
int v=e[i].v;
if(v!=fat)//只要不是父节点
{
dfs(v,u);//就搜索
ss[u]+=ss[v];//累加子树和
}
}
en[u]=tim;//记录结束位置
}
int LCA(int x,int y)//基本操作,求lca
{
if(deep[x]<deep[y])
{
x=x^y,y=x^y,x=x^y;
}
while(deep[x]>deep[y])
{
x=stf[x][lg[deep[x]-deep[y]]-1];
}
if(x==y) return x;
for(rint i=lg[deep[x]]-1;i>=0;--i)
{
if(stf[x][i]!=stf[y][i])
{
x=stf[x][i];
y=stf[y][i];
}
}
return stf[x][0];
}
void ad(int x,ll y,ll z)//x:位置 y:变化(+ or -) z:乘积
{
for(rint i=x;i<=n;i+=(i&(-i)))//树状数组修改点
{
s1[i]+=y;
s2[i]+=z;
}
}
void in(int x,ll d)//ad函数的引子,处理特殊情况
{
if(x==0) return;//父节点不能为0
ad(st[x],d,d*deep[x]);//d:每个点的修改 d*deep[x]:乘积
}
ll sum(int x,ll t[])//树状数组求和
{
ll ans=0;
for(rint i=x;i;i-=(i&(-i)))
{
ans+=t[i];
}
return ans;
}
ll query(int x,ll t[])//sum函数的引子,处理sum(en[x],t)-sum(st[x]-1,t)
{
return sum(en[x],t)-sum(st[x]-1,t);
}
int main()
{
n=read(),m=read(),p=read();
for(rll i=1;i<=n;++i)
{
a[i]=read();//读入信息
}
for(rll i=1;i<n;++i)
{
ll u=read(),v=read();
add(u,v);//建边
add(v,u);
}
for(rll i=1;i<=n;++i)
{
lg[i]=lg[i-1]+(1<<lg[i-1]==i);//处理log
}
dfs(p,0);//搜索,建dfs序
for(rll i=1;i<=m;++i)
{
ll op=read();
if(op==1)
{
ll a=read(),b=read(),c=read();
ll lca=LCA(a,b);//求lca
in(a,c);in(b,c);in(lca,-c);in(stf[lca][0],-c);//树上差分
}
if(op==2)
{
ll p=read();
printf("%lld\n",query(p,s1)+a[p]);//查询点
}
if(op==3)//(deep[y]-deep[x]+1)*c=deep[y]*c-(deep[x]-1)*c 求子树和公式
{
ll p=read();
printf("%lld\n",query(p,s2)-query(p,s1)*(deep[p]-1)+ss[p]);//查询子树和
//query(p,s2):deep[y]*c
//query(p,s1):c的总和
//so query(p,s1)*(deep[p]-1):(deep[x]-1)*c
//ss[p] 原来的子树和
}
}
return 0;
}
树链修改,点查询单独代码:
#include<bits/stdc++.h>
#define ll long long
#define rll register long long
using namespace std;
const ll N=1e5+5;
ll n,cnt,m,p,tim;
ll a[N],h[N],dfn[N],st[N],en[N];
ll stf[N][20],lg[N],deep[N];
ll s[N];
struct edge//链式前向星
{
ll v,nxt;
} e[N<<1];
inline ll read()//快读
{
ll x=0;
bool flag=false;
char ch=getchar();
while(ch<'0'||ch>'9')
{
if(ch=='-') flag=true;
ch=getchar();
}
while(ch>='0'&&ch<='9')
{
x=(x<<3)+(x<<1)+(ch^48);
ch=getchar();
}
return flag?~x+1:x;
}
void add(ll u,ll v)//建边
{
e[++cnt].v=v;
e[cnt].nxt=h[u];
h[u]=cnt;
}
void dfs(ll u,ll fat)
{
st[u]=++tim;//记录开始位置
dfn[tim]=u;//记录时间戳
deep[u]=deep[fat]+1;//更新深度
stf[u][0]=fat;//更新st表
for(rll i=1;i<=lg[deep[u]];++i)//更新st表
{
stf[u][i]=stf[stf[u][i-1]][i-1];
}
for(rll i=h[u];i;i=e[i].nxt)
{
ll v=e[i].v;
if(v!=fat)//不能搜到父节点
{
dfs(v,u);
}
}
en[u]=tim;//记录结束位置
}
ll LCA(ll x,ll y)//基本操作求lca
{
if(deep[x]<deep[y])
{
x=x^y,y=x^y,x=x^y;
}
while(deep[x]>deep[y])
{
x=stf[x][lg[deep[x]-deep[y]]-1];
}
if(x==y) return x;
for(rll i=lg[deep[x]]-1;i>=0;--i)
{
if(stf[x][i]!=stf[y][i])
{
x=stf[x][i];
y=stf[y][i];
}
}
return stf[x][0];
}
void ad(ll x,ll y)//修改
{
for(rll i=x;i<=n;i+=(i&(-i)))
{
s[i]+=y;
}
}
ll sum(ll x)//求和(这里求的是修改的值,也就是将要对原数值修改的值)
{
ll ans=0;
for(rll i=x;i;i-=(i&(-i)))
{
ans+=s[i];
}
return ans;
}
int main()
{
n=read(),p=read();
for(rll i=1;i<=n;++i)
{
a[i]=read();
}
for(rll i=1;i<n;++i)
{
ll u=read(),v=read();
add(u,v);
add(v,u);
}
for(rll i=1;i<=n;++i)
{
lg[i]=lg[i-1]+(1<<lg[i-1]==i);
}
dfs(p,0);
m=read();
for(rll i=1;i<=m;++i)
{
ll k=read();
if(k==1)
{
ll a=read(),b=read(),c=read();
ll lca=LCA(a,b);
ad(st[a],c),ad(st[b],c);
ad(st[lca],-c),ad(st[stf[lca][0]],-c);//树上差分
}
else
{
ll p=read();
printf("%lld\n",a[p]+sum(en[p])-sum(st[p]-1));//记得加上原数值
}
}
}
树链修改,子树和查询单独代码:
#include<bits/stdc++.h>
#define ll long long
#define rll register long long
using namespace std;
const ll N=1e5+5;
ll n,cnt,m,p,tim;
ll a[N],h[N],dfn[N],st[N],en[N];
ll stf[N][20],lg[N],deep[N];
ll ss[N],s1[N],s2[N];//s1 记录加减 s2 记录乘积
struct edge
{
ll v,nxt;
} e[N<<1];
inline ll read()
{
ll x=0;
bool flag=false;
char ch=getchar();
while(ch<'0'||ch>'9')
{
if(ch=='-') flag=true;
ch=getchar();
}
while(ch>='0'&&ch<='9')
{
x=(x<<3)+(x<<1)+(ch^48);
ch=getchar();
}
return flag?~x+1:x;
}
void add(ll u,ll v)
{
e[++cnt].v=v;
e[cnt].nxt=h[u];
h[u]=cnt;
}
void dfs(ll u,ll fat)
{
st[u]=++tim;//记录出现位置
dfn[tim]=u;//记录dfs序
deep[u]=deep[fat]+1;//记录深度
stf[u][0]=fat;//记录父节点
ss[u]=a[u];//记录子树和,后面会被加
for(rll i=1;i<=lg[deep[u]];++i)
{
stf[u][i]=stf[stf[u][i-1]][i-1];//更新st表
}
for(rll i=h[u];i;i=e[i].nxt)//遍历
{
ll v=e[i].v;
if(v!=fat)
{
dfs(v,u);//搜索
ss[u]+=ss[v];//更新子树和
}
}
en[u]=tim;//记录结束位置
}
ll LCA(ll x,ll y)//基本操作求lca
{
if(deep[x]<deep[y])
{
x=x^y,y=x^y,x=x^y;
}
while(deep[x]>deep[y])
{
x=stf[x][lg[deep[x]-deep[y]]-1];
}
if(x==y) return x;
for(rll i=lg[deep[x]]-1;i>=0;--i)
{
if(stf[x][i]!=stf[y][i])
{
x=stf[x][i];
y=stf[y][i];
}
}
return stf[x][0];
}
void ad(ll x,ll y,ll z)
{
for(rll i=x;i<=n;i+=(i&(-i)))
{
s1[i]+=y;//记录加减
s2[i]+=z;//记录乘积
}
}
void in(ll x,ll d)
{
if(x==0) return;//防止父节点是0
ad(st[x],d,d*deep[x]);
}
ll sum(ll x,ll t[])//求和
{
ll ans=0;
for(rll i=x;i;i-=(i&(-i)))
{
ans+=t[i];
}
return ans;
}
ll query(ll x,ll t[])
{
return sum(en[x],t)-sum(st[x]-1,t);
}
int main()
{
n=read(),m=read(),p=read();
for(rll i=1;i<=n;++i)
{
a[i]=read();//记录各个点的信息
}
for(rll i=1;i<n;++i)
{
ll u=read(),v=read();
add(u,v);//建边
add(v,u);//建边
}
for(rll i=1;i<=n;++i)
{
lg[i]=lg[i-1]+(1<<lg[i-1]==i);//求log
}
dfs(p,0);//搜索
for(rll i=1;i<=m;++i)
{
ll op=read();//(deep[y]-deep[x]+1)*c=deep[y]*c-(deep[x]-1)*c
if(op==1)
{
ll a=read(),b=read(),c=read();
ll lca=LCA(a,b);
in(a,c);in(b,c);in(lca,-c);in(stf[lca][0],-c);//差分
}
else
{
ll p=read();
printf("%lld\n",query(p,s2)-query(p,s1)*(deep[p]-1)+ss[p]);
//query(p,s2):deep[y]*c
//query(p,s1):c的总和
//so query(p,s1)*(deep[p]-1):(deep[x]-1)*c
//ss[p] 原来的子树和
}
}
}
四、点修改,链和查询
题意:有一棵树,有点权。进行 \(n\) 次点权修改, \(n\) 次提问以某 \(2\) 个节点间的树链的权值和。
点 \(y + v\) ,\(y\) 的子树到 \(root\) 的距离都会 \(+v\) ,所以点修改就转化成了子树修改,子树在 \(\text{dfs}\) 序上是连续的区间,利用差分思想,在起始点 \(+v\),结束点 \(+1\) 的位置 \(-v\) ,求 \(a-b\) 的链和,其实就是\((a->root)+(b->root)-(lca->root)-(st(lca, 0)->root)\) 的值,因为点 \(lca\) 也是在链上的,所以只减去一遍 \((lca->root)\) ,再减去 \((st(lca, 0)->root)\) 来抵消,代码有注释。
代码:
#include<bits/stdc++.h>
#define ll long long
#define rint register int
using namespace std;
const int N=1e6+6;
int n,m,R,cnt,tim;
int h[N],lg[N],deep[N],st[N][20],be[N],en[N];
ll d[N],s[N],size[N];
struct edge
{
int v,nxt;
} e[N<<1];
inline ll read()//快读
{
ll x=0;
bool flag=false;
char ch=getchar();
while(ch<'0'||ch>'9')
{
if(ch=='-') flag=true;
ch=getchar();
}
while(ch>='0'&&ch<='9')
{
x=(x<<3)+(x<<1)+(ch^48);
ch=getchar();
}
return flag?~x+1:x;
}
void add(int u,int v)//建边
{
e[++cnt].v=v;
e[cnt].nxt=h[u];
h[u]=cnt;
}
void dfs(int u,int fat)
{
deep[u]=deep[fat]+1;//记录深度
st[u][0]=fat;//更新st表
be[u]=++tim;//记录开始位置
size[u]+=d[u];//记录点到root的距离
for(rint i=1;i<=lg[deep[u]];++i)//更新st表
{
st[u][i]=st[st[u][i-1]][i-1];
}
for(rint i=h[u];i;i=e[i].nxt)//传统技能——遍历
{
int v=e[i].v;
if(v!=fat)//不能是父节点,否则等RE吧
{
size[v]+=size[u];//子节点到root的距离是父节点的加上这个边的距离
dfs(v,u);//搜索
}
}
en[u]=tim;//记录结束位置
}
void ad(int x,int c)//单点修改
{
if(x==0) return;
for(rint i=x;i<=n;i+=(i&(-i)))
{
s[i]+=c;
}
}
int LCA(int x,int y)//基本操作——求lca
{
if(deep[x]<deep[y])
{
x=x^y,y=x^y,x=x^y;
}
while(deep[x]>deep[y]) x=st[x][lg[deep[x]-deep[y]]-1];
if(x==y) return x;
for(rint i=lg[deep[x]]-1;i>=0;--i)
{
if(st[x][i]!=st[y][i])
{
x=st[x][i];
y=st[y][i];
}
}
return st[x][0];
}
ll sum(int x)//树状数组求和
{
ll ans=0;
for(rint i=x;i;i-=(i&(-i)))
{
ans+=s[i];
}
return ans;
}
int main()
{
n=read(),m=read(),R=read();//n 点数 m 操作数 R 根节点
for(rint i=1;i<=n;++i)
{
d[i]=read();//读入每个点的数据
}
for(rint i=1;i<n;++i)
{
int x=read(),y=read();
add(x,y);//建边
add(y,x);
}
for(rint i=1;i<=n;++i)
{
lg[i]=lg[i-1]+(1<<lg[i-1]==i);//处理log
}
dfs(R,0);//深搜
for(rint i=1;i<=m;++i)
{
int op=read();
if(op==1)
{
int x=read(),y=read();
ad(be[x],y);ad(en[x]+1,-y);//树上差分
}
if(op==2)
{
int a=read(),b=read(),lca=LCA(a,b);
ll ans=0;
ans+=size[a]+size[b]-size[lca]-size[st[lca][0]];
ans+=sum(be[a])+sum(be[b])-sum(be[lca])-sum(be[st[lca][0]]);//(a->root)+(b->root)-(lca->root)-(st[lca][0]->root)
printf("%lld\n",ans);
}
}
}
还请自己多画图理解一下,想清楚每个数组的含义!
五、子树修改,点查询
题意:有一棵树,有点权。进行 \(n\) 次子树权修改,也就是子树上每一个点的权都加 \(v\) ,\(n\) 次提问以某点的权值。
这个比较水,一个子树的修改,不会影响其他子树,子树在 dfs 序上又是连续的区间,直接用差分就好了,点查询就是树状数组求和再加原数据就好了,直接上代码,看到这,你应该对大体基本步骤很清楚了,往下的代码对基本步骤不再进行讲解了。
代码:
#include<bits/stdc++.h>
#define ll long long
#define rint register int
using namespace std;
const int N=1e6+5;
int n,m,R,cnt,tim;
int h[N],be[N],en[N];//看到这应该很熟了,基本操作不讲了
ll d[N],s[N];
struct edge
{
int v,nxt;
} e[N<<1];
inline ll read()
{
ll x=0;
bool flag=false;
char ch=getchar();
while(ch<'0'||ch>'9')
{
if(ch=='-') flag=true;
ch=getchar();
}
while(ch>='0'&&ch<='9')
{
x=(x<<3)+(x<<1)+(ch^48);
ch=getchar();
}
return flag?~x+1:x;
}
void add(int u,int v)
{
e[++cnt].v=v;
e[cnt].nxt=h[u];
h[u]=cnt;
}
void dfs(int u,int fat)
{
be[u]=++tim;
for(rint i=h[u];i;i=e[i].nxt)
{
int v=e[i].v;
if(v!=fat)
{
dfs(v,u);
}
}
en[u]=tim;
}
void ad(int x,ll c)
{
for(rint i=x;i<=n;i+=(i&(-i)))
{
s[i]+=c;
}
}
ll sum(int x)
{
ll ans=0;
for(rint i=x;i;i-=(i&(-i)))
{
ans+=s[i];
}
return ans;
}
int main()
{
n=read(),m=read(),R=read();
for(rint i=1;i<=n;++i)
{
d[i]=read();
}
for(rint i=1;i<n;++i)
{
int x=read(),y=read();
add(x,y);
add(y,x);
}
dfs(R,0);
for(rint i=1;i<=m;++i)
{
int op=read();
if(op==1)
{
int x=read();
ll y=read();
ad(be[x],y);//差分思想
ad(en[x]+1,-y);
}
if(op==2)
{
int x=read();
ll ans=0;
ans+=sum(be[x]);//这里sum加的是修改了多少,仅仅指修改,在程序中,对原数据不做修改
ans+=d[x];//记得加上原数据
printf("%lld\n",ans);
}
}
return 0;
}
六、子树修改,子树查询
题意:有一棵树,有点权。进行 \(n\) 次子树权修改,也就是子树上每一个点的权都加 \(v\) ,\(n\) 次提问以某子树的权值和。
还是那句话,子树在 dfs 序上是连续的区间,子树修改子树查询就相当于区间修改区间查询,线段树或树状数组维护即可,很水。
直接上代码(线段树)
代码:
#include<bits/stdc++.h>
#define ll long long
#define rint register int
#define p1 p<<1
#define p2 p<<1|1
using namespace std;
const int N=1e6+5;
int n,m,root,cnt,tim;
int h[N],be[N],en[N],dfn[N];
ll d[N];
struct edge
{
int v,nxt;
} e[N<<1];
struct tree
{
ll len,dat,laz;
} t[N<<2];
inline ll read()
{
ll x=0;
bool flag=false;
char ch=getchar();
while(ch<'0'||ch>'9')
{
if(ch=='-') flag=true;
ch=getchar();
}
while(ch>='0'&&ch<='9')
{
x=(x<<3)+(x<<1)+(ch^48);
ch=getchar();
}
return flag?~x+1:x;
}
void add(int u,int v)
{
e[++cnt].v=v;
e[cnt].nxt=h[u];
h[u]=cnt;
}
void dfs(int u,int fat)
{
be[u]=++tim;
dfn[tim]=u;
for(rint i=h[u];i;i=e[i].nxt)
{
int v=e[i].v;
if(v!=fat)
{
dfs(v,u);
}
}
en[u]=tim;
}
inline void update(int p)
{
t[p].dat=t[p1].dat+t[p2].dat;
}
inline void lazy(int p,ll v)//打懒标记
{
t[p].laz+=v;
t[p].dat+=(v*t[p].len);
}
inline void pushdown(int p)//懒标记下传
{
if(t[p].laz)
{
lazy(p1,t[p].laz);
lazy(p2,t[p].laz);
t[p].laz=0;
}
}
void build(int p,int l,int r)//建树
{
t[p].len=r-l+1;
if(l==r)
{
t[p].dat=d[dfn[l]];
t[p].laz=0;
return;
}
int mid=l+r>>1;
build(p1,l,mid);
build(p2,mid+1,r);
update(p);
}
void change(int p,int l,int r,int lr,int rr,ll v)//线段树修改
{
if(lr<=l&&r<=rr)
{
lazy(p,v);
return;
}
pushdown(p);
int mid=l+r>>1;
if(lr<=mid) change(p1,l,mid,lr,rr,v);
if(rr>mid) change(p2,mid+1,r,lr,rr,v);
update(p);
}
ll query(int p,int l,int r,int lr,int rr)//查询
{
if(lr<=l&&r<=rr)
{
return t[p].dat;
}
pushdown(p);
int mid=l+r>>1;
ll ans=0;
if(lr<=mid) ans+=query(p1,l,mid,lr,rr);
if(rr>mid) ans+=query(p2,mid+1,r,lr,rr);
return ans;
}
int main()
{
n=read(),m=read(),root=read();
for(rint i=1;i<=n;++i)
{
d[i]=read();
}
for(rint i=1;i<n;++i)
{
int x=read(),y=read();
add(x,y);
add(y,x);
}
dfs(root,0);
build(1,1,tim);
for(rint i=1;i<=m;++i)
{
int op=read();
if(op==1)
{
int x=read();
ll y=read();
change(1,1,n,be[x],en[x],y);//区间修改
}
if(op==2)
{
int x=read();
printf("%lld\n",query(1,1,n,be[x],en[x]));//区间查询
}
}
return 0;
}
七、子树修改,链查询
题意:有一棵树,有点权。进行 \(n\) 次子树权修改,也就是子树上每一个点的权都加 \(v\) ,\(n\) 次提问以某链的权值和。
还是贡献,链的查询,还是将链进行分解,这样就变成了 \((x->root)\) 的权值和。
当修改子树的根节点 \(y\) 是 \(x\) 的祖先时,那么修改对 \((x->root)\) 的权值和有贡献。贡献值为 \((deep_y - deep_x + 1) \times v = v \times deep_y - v \times deep_x + v\) 。\(deep\) 可以预处理,这样只需要两个树状数组维护 \(v \times deep\) 和 \(v\) 就可以了。
代码:
#include<bits/stdc++.h>
#define ll long long
#define rint register int
using namespace std;
const int N=1e6+5;
int n,m,root,cnt,tim;
int h[N],lg[N],be[N],en[N],st[N][20],dep[N];
ll d[N],s1[N],s2[N],size[N];
struct edge
{
int v,nxt;
} e[N<<1];
inline ll read()
{
ll x=0;
bool flag=false;
char ch=getchar();
while(ch<'0'||ch>'9')
{
if(ch=='-') flag=true;
ch=getchar();
}
while(ch>='0'&&ch<='9')
{
x=(x<<3)+(x<<1)+(ch^48);
ch=getchar();
}
return flag?~x+1:x;
}
void add_edge(int u,int v)
{
e[++cnt].v=v;
e[cnt].nxt=h[u];
h[u]=cnt;
}
void dfs(int u,int fat)
{
dep[u]=dep[fat]+1;
st[u][0]=fat;
be[u]=++tim;
size[u]+=d[u];//记录到根节点的距离
for(rint i=1;i<=lg[dep[u]];++i)
st[u][i]=st[st[u][i-1]][i-1];
for(rint i=h[u];i;i=e[i].nxt)
{
int v=e[i].v;
if(v!=fat)
{
size[v]+=size[u];
dfs(v,u);
}
}
en[u]=tim;
}
void add1(int x,ll c)//处理s1 记录v的变化
{
if(x==0) return;
for(rint i=x;i<=n;i+=(i&(-i)))
s1[i]+=c;
}
void add2(int x,ll c)//处理s2 记录dep的乘积
{
if(x==0) return;
for(rint i=x;i<=n;i+=(i&(-i)))
s2[i]+=c;
}
int LCA(int x,int y)
{
if(dep[x]<dep[y]) x=x^y,y=x^y,x=x^y;
while(dep[x]>dep[y]) x=st[x][lg[dep[x]-dep[y]]-1];
if(x==y) return x;
for(rint i=lg[dep[x]]-1;i>=0;--i)
{
if(st[x][i]!=st[y][i])
{
x=st[x][i];
y=st[y][i];
}
}
return st[x][0];
}
ll sum1(int x)//求v的和
{
ll ans=0;
for(rint i=x;i;i-=(i&(-i)))
ans+=s1[i];
return ans;
}
ll sum2(int x)//求乘积的和
{
ll ans=0;
for(rint i=x;i;i-=(i&(-i)))
ans+=s2[i];
return ans;
}
int main()
{
n=read(),m=read(),root=read();
for(rint i=1;i<=n;++i)
d[i]=read();
for(rint i=1;i<n;++i)
{
int x=read(),y=read();
add_edge(x,y);
add_edge(y,x);
}
for(rint i=1;i<=n;++i)
lg[i]=lg[i-1]+(1<<lg[i-1]==i);
dfs(root,0);
for(rint i=1;i<=m;++i)
{
int op=read();//(deep[y]-deep[x]+1)*v=v*(deep[y]+1)-v*deep[x]
if(op==1)
{
int x=read();ll w=read();
add1(be[x],w);add1(en[x]+1,-w);//差分记录v
add2(be[x],dep[x]*w);add2(en[x]+1,-dep[x]*w);//差分记录dep*v
//这里记录的是要预处理的部分
}
if(op==2)
{
int a=read(),b=read(),lca=LCA(a,b),k=st[lca][0];
ll ans=size[a]+size[b]-size[lca]-size[k];//先加上原数据
ans+=((dep[a]+1)*sum1(be[a])-sum2(be[a]));
ans+=((dep[b]+1)*sum1(be[b])-sum2(be[b]));
ans-=((dep[lca]+1)*sum1(be[lca])-sum2(be[lca]));
ans-=((dep[k]+1)*sum1(be[k])-sum2(be[k]));
printf("%lld\n",ans);
//(dep[a]+1)*sum1(be[a]): v*(deep[y]+1)
//-sum2(be[a]): -v*deep[x]
}
}
}