前置概念:
-
size[i]表示节点i的子树大小,其中子树大小定义为包括该点自身的子树含有的节点数个数。
-
轻儿子和重儿子:一个节点儿子中size最大的叫做重儿子,剩下的都叫轻儿子。
-
轻重边:连接x及其重儿子的边叫做重边,同理连接x及其轻儿子的边叫做轻边
-
轻重链:多条重边形成的链叫做重链,且规定每个节点都属于某一条重链。
-
top[i]表示i节点所在重链的顶端的点的编号
-
f[i]表示i节点的父节点编号
-
son[i]表示i节点的重儿子编号,编号为0代表无重儿子
-
d[i]表示i节点的深度,规定根节点深度为1
前置基础信息的求解:
-
第一次dfs可以求出,d[i],f[i],son[i],size[i]
供参考具体实现:
void dfs1(int x,int fa)
{
size[x]=1;
d[x]=d[fa]+1;
son[x]=0;
f[x]=fa;//初始化
for(int i=head[x];~i;i=edge[i].next){
int to=edge[i].to;
if(to==fa)continue;
dfs1(to,x);
size[x]+=size[to];
if(size[son[x]]<size[to]) son[x]=to;
}//遍历子节点,更新信息
}
-
第二次dfs借助son[i]等信息求出top[i]。
具体实现:
void dfs2(int x,int topx)//topx记录重链顶端的点
{
top[x]=x;
if(son[x]!=0)dfs2(son[x],topx);
for(int i=head[x];~i;i=edge[i].next)
if(edge[i].to!=son[x]&&edge[i].to!=f[x])
dfs2(edge[i].to,edge[i].to);//遍历轻边,并为其创建新的重链
}
引申:
最近公共祖先(LCA):
-
概念:
u与v的LCA就是u到v的路径上深度最小的点。因为在一棵树中只有可能有一条路径能从一个点到另一个点(如果两点之间有两条路径,那么在无向图中会构成自环,从而违背了树的定义)则LCA有且仅有固定的一个点。
-
求解LCA算法:
-
给定两点u和v,如果两点在同一条重链中,lca即为两点中深度较小的点;
-
若不在,其重链顶端点中较深的点x跳至其f[top[x]]上,每次操作会跳过一条轻边,再来观察两点是否在同一条重链中,重复操作。
代码实现:
void lca(int u,int v)
{
while(top[u]!=top[v])
{
if(d[top[u]]<d[top[v]])v=f[top[v]];//深度越大代表点靠近底部
else u=f[top[u]];
}
printf("%d\n",d[u]<d[v]?u:v);
}
算法时间复杂度分析:
首先最坏情况下,一个点可能不经过任意一条重链只跳轻边达到根节点,那么操作次数就与需跳跃的轻边数量有关,每一次通过轻儿子跳轻边来到父节点,父节点的size一定大于等于,假设从size=1的轻儿子节点处开始跳,那么跳跃的轻边数量一定小于等于
,则实际的时间复杂度应该为O(logn),是非常小的数字。
树上操作(树链剖分加线段树):
树上操作复杂度较高,考虑线段树维护该有根多边树上的节点信息
-
编号,根据第二次dfs进行编号,使得重链上的点新编号是连续的,每个点的子树编号也在一个连续区间内。
-
int dfn=0,nid[maxn],oid[maxn];//nid[x]的值为原来点的新编号,oid[x]的值表示新编号的对应的原来的点编号 void dfs2(int x,int topx) { top[x]=topx; oid[++dfn]=x; nid[x]=dfn;//编新号,重链一堆,子树一堆 na[dfn]=a[x];//对应的权值顺序也要调整 if(son[x]!=0)dfs2(son[x],topx); for(int i=head[x];~i;i=edge[i].next) if(edge[i].to!=son[x]&&edge[i].to!=f[x]) dfs2(edge[i].to,edge[i].to); }
-
树上操作
-
修改(或其它操作)以某个节点为根节点的子树上所有点的权值,即对应修改线段树[nid[x],nid[x]+size[x]-1]区间范围内的值。
-
修改某个点或边的权值(边的权值可存储到点中),对应线段树的单点更新
-
修改某条重链上两点间路径的的所有边权,相当于修改线段树区间[nid[x]+1,nid[y]](前提:x点的深度较小,对应编号较小)
-
修改不在同一重链的两点间的边权或点权;首先,判断dep[top[u]]与dep[top[v]]大小,先跳转深度较小的点,同时修改x到f[top[x]]这条重链之间所有的边权;直到跳转至两点处于同一条重链中,修改两点之间的边权。
代码实现:
#include<bits/stdc++.h>
#define ll long long
#define INF 0x3f3f3f3f
#define maxn 1000
using namespace std;
int n,m,r,P,cnt=0;
ll a[maxn]={0},na[maxn]={0};
int head[maxn]={0},d[maxn]={0},f[maxn]={0},top[maxn]={0},son[maxn]={0},size[maxn]={0};
struct Edge{
int to;
int next;
}edge[maxn<<1];
struct node{
ll val;
ll lazy;
}segtree[maxn<<2];
int dfn=0,nid[maxn],oid[maxn];//nid[x]的值为原来点的新编号,oid[x]的值表示新编号的对应的原来的点编号
void addedge(int u,int v)
{
edge[++cnt].to=v;
edge[cnt].next=head[u];
head[u]=cnt;
}
void dfs1(int x,int fa)
{
d[x]=d[fa]+1;
f[x]=fa;
son[x]=0;
size[x]=1;
for(int i=head[x];~i;i=edge[i].next)
{
int to =edge[i].to;
if(to==fa)continue;
dfs1(to,x);
size[x]+=size[to];
if(size[son[x]]<size[to])son[x]=to;
}
}
void dfs2(int x,int topx)
{
top[x]=topx;
oid[++dfn]=x;
nid[x]=dfn;//编新号,重链一堆,子树一堆
na[dfn]=a[x];
if(son[x]!=0)dfs2(son[x],topx);
for(int i=head[x];~i;i=edge[i].next)
if(edge[i].to!=son[x]&&edge[i].to!=f[x])
dfs2(edge[i].to,edge[i].to);
}
//以下线段树
void pushup(int rt)
{
segtree[rt].val=(segtree[rt*2].val+segtree[rt*2+1].val)%P;
}
void pushdown(int rt,int ln,int rn)
{
if(segtree[rt].lazy)
{
int l=rt*2,r=rt*2+1,lazy=segtree[rt].lazy;
segtree[l].val=(lazy*ln+segtree[l].val)%P;
segtree[l].lazy=(lazy+segtree[l].lazy)%P;
segtree[r].val=(lazy*rn+segtree[r].val)%P;
segtree[r].lazy=(lazy+segtree[r].lazy)%P;
segtree[rt].lazy=0;//不要忘!!!!!!!!!!!!!!
}
}
void build(int l,int r,int rt)
{
segtree[rt].lazy=0;
if(l==r)
{
segtree[rt].val=na[l]%P;//rt指的是segtree的节点编号,l,r指的是原数组的区间编号
return;
}
int mid=(r+l)>>1;
build(l,mid,rt*2);
build(mid+1,r,rt*2+1);
pushup(rt);
}
void updated(int L,int R,int l,int r,int rt,int val)//L,R代表修改区间!!!!//区间更新
{
if(l>=L&&r<=R)
{
segtree[rt].val=(segtree[rt].val+(r-l+1)*val)%P;
segtree[rt].lazy=(val+segtree[rt].lazy)%P;
return;
}
int mid=(l+r)>>1;
pushdown(rt,mid-l+1,r-mid);
if(mid>=L)
updated(L,R,l,mid,rt*2,val);
if(mid<R)//必须是小于,没有等于
updated(L,R,mid+1,r,rt*2+1,val);
pushup(rt);
}
ll query(int L,int R,int l,int r,int rt)//区间查询
{
if(l>=L&&r<=R)
return segtree[rt].val%P;
if(l>R||r<L)
return 0;
int mid=(l+r)>>1;
pushdown(rt,mid-l+1,r-mid);
return (query(L,R,l,mid,rt*2)%P+query(L,R,mid+1,r,rt*2+1)%P)%P;
}
void update_rd(int u,int v,int val)//路径更新
{
while(top[u]!=top[v])
{
if(d[top[u]]>d[top[v]]){
updated(nid[top[u]],nid[u],1,n,1,val);
u=f[top[u]];
}
else {
updated(nid[top[v]],nid[v],1,n,1,val);
v=f[top[v]];
}
}
if(nid[u]>nid[v])
updated(nid[v],nid[u],1,n,1,val);
else
updated(nid[u],nid[v],1,n,1,val);
}
ll query_rd(int u,int v)//路径查询
{
ll res=0;
while(top[u]!=top[v])
{
if(d[top[u]]<d[top[v]])
{
res=(res+query(nid[top[v]],nid[v],1,n,1))%P;
v=f[top[v]];
}
else
{
res=(res+query(nid[top[u]],nid[u],1,n,1))%P;
u=f[top[u]];
}
}
if(nid[u]>nid[v])
res=(res+query(nid[v],nid[u],1,n,1))%P;
else
res=(res+query(nid[u],nid[v],1,n,1))%P;
return (res%P);
}
int main()
{
scanf("%d%d%d%d",&n,&m,&r,&P);
for(int i=1;i<=n;i++)
scanf("%lld",&a[i]);
for(int i=0;i<=n;i++)
head[i]=-1;
for(int i=1;i<=n-1;i++)
{
int u,v;
scanf("%d%d",&u,&v);
addedge(u,v);
addedge(v,u);
}
dfs1(r,r);
dfs2(r,r);
build(1,n,1);//原根新编号为1
for(int i=1;i<=m;i++)
{
int x,y,z,mod;
scanf("%d",&mod);
if(mod==1)
{
scanf("%d%d%d",&x,&y,&z);
update_rd(x,y,z);
}
else if(mod==2)
{
scanf("%d%d",&x,&y);
printf("%lld\n",query_rd(x,y));
}
else if(mod==3)
{
scanf("%d%d",&x,&z);
updated(nid[x],nid[x]+size[x]-1,1,n,1,z);
}
else
{
scanf("%d",&x);
printf("%lld\n",query(nid[x],nid[x]+size[x]-1,1,n,1));
}
}
system("pause");
}//注意事项:1.细节细节细节!不要看错,漏写2.注意该模余数的地方就去模3.最好能熟悉线段树的所有细节,以及将原来的点赋予新编号时,一定要小心谨慎
//4.码量巨大,稳住心态