树链剖分小结
树链剖分,计算机术语,指一种对树进行划分的算法,它先通过轻重边剖分将树分为多条链,保证每个点属于且只属于一条链,然后再通过数据结构(树状数组、BST、SPLAY、线段树等)来维护每一条链。
——百度百科
重链剖分
概念 1 重儿子
一个父节点的所有儿子中,子树节点最大(\(siz\) 最大)的节点。
记为 \(son[u]=v\)。
概念2 轻儿子
父节点所有儿子中,除过重儿子的所有节点。
概念3 重边
由父亲节点和重儿子连接成的边。
概念4 轻边
由父亲节点和轻儿子连接成的边。
概念5 重链
由多条重边连成的链。
概念5 轻边
由多条轻边连成的链。
实现思路
- 对于一个节点先找出它所在的子树大小,同时我们可以得到它的所有子节点的子树大小 \(siz[v]\),这样我们可以得到此节点的重儿子。
例如,点 1 的三个儿子分别是 2,3,4。
2 所在的子树大小为 5,
3 所在的子树大小为 2,
4 所在的子树大小为 6,
那么 1 的重儿子就是 4。
-
在 \(dfs\) 的过程中顺便记录节点 \(u\) 的父亲 \(f[u]\),从根节点的深度 \(dep[u]\) 等。
-
再来一遍 \(dfs\),连接重链,标记每个节点的 \(dfs\) 序,处理出每个节点所在重链的顶点 \(top[u]\) 和节点编号 \(id[u]\)。
代码实现
第一遍 dfs
作用主要是找出每个节点的深度和重儿子。
int son[500010],deep[500101],f[500101];
int siz[500010];
//son:重儿子
//deep:节点深度
//f:节点的父节点
void dfs1(int u,int fa)
{
f[u]=fa;
deep[u]=deep[fa]+1;
siz[u]=1;
for(int i=head[u];i;i=edge[i].next)
{
int v=edge[i].to;
if(v==fa)continue;
dfs1(v,u);
siz[u]+=siz[v];
if(siz[son[u]]<siz[v])
son[u]=v;
}
}
第二遍 dfs
作用主要是求出每条重链的顶点和节点的 \(dfs\) 序。
//rk:节点编号
//id:dfn序
//top:重链顶端
void dfs2(int u,int t)
{
top[u]=t;
if(son[u])dfs2(son[u],t);
id[u]=++cnt;
rk[cnt]=u;
for(int i=head[u];i;i=edge[i].next)
{
int v=edge[i].to;
if(v==son[u]||v==f[u])continue;
dfs2(v,v);
}
}
这样就完成了树链剖分的基础操作(链式前向星存图)。
线段树操作总结
单点加,区间加
void add(int now,int l,int r,int k)
{
tr[now].lazy+=k;
tr[now].sum+=k*(r-l+1);
}
void pushdown(int now,int l,int r)
{
if(!tr[now].lazy) return ;
int mid=(l+r)>>1;
add(lid,l,mid,tr[now].lazy);
add(rid,mid+1,r,tr[now].lazy);
tr[now].lazy=0;
}
void change(int now,int l,int r,int x,int y,int k)
{
if(x<=l&&r<=y)
{
add(now,l,r,k);return;
}
pushdown(now,l,r);
int mid=(l+r)>>1;
if(x<=mid) change(lid,l,mid,x,y,k);
if(y>mid) change(rid,mid+1,r,x,y,k);
tr[now].sum=tr[lid].sum+tr[rid].sum;
}
单点查,区间查
最基础的线段树操作之一。
别忘了 \(pushdown\)。
int query(int now,int l,int r,int x,int y)
{
if(x<=l&&r<=y) return tr[now].sum;
pushdown(now,l,r);
int mid=(l+r)>>1;
int res=0;
if(x<=mid) res+=query(lid,l,mid,x,y);
if(y>mid) res+=query(rid,mid+1,r,x,y);
return res;
}
子树加
通过调用区间加的函数来实现。
一个节点 \(u\) 的子树的 \(dfs\) 序的范围是 \([id[u],id[u]+siz[u]-1]\)。
其中 \(siz[u]\) 表示子树大小,在 \(dfs1\) 中求得。
void add_tree(int u,int k)
{
change(1,1,n,id[u],id[u]+siz[u]-1,k);
}
树链加
代码基本同求 \(lca\)。
void add_path(int u,int v,int k)
{
while(top[u]!=top[v])
{
if(dep[top[u]]<dep[top[v]]) swap(u,v);
change(1,1,n,id[top[u]],id[u],k);
u=f[top[u]];
}
if(dep[u]>dep[v])swap(u,v);
change(1,1,n,id[u],id[v],k);
}
子树查
代码同子树加。只不过调用的函数不同。
int query_tree(int u)
{
return query(1,1,n,id[u],id[u]+siz[u]-1);
}
树链查
代码同树链加,也是调用的函数不同。
int query_path(int u,int v)
{
int res=0;
while(top[u]!=top[v])
{
if(dep[top[u]]<dep[top[v]])swap(u,v);
res+=query(1,1,n,id[top[u]],id[u]);
u=f[top[u]];
}
if(dep[u]>dep[v])swap(u,v);
res+=query(1,1,n,id[u],id[v]);
return res;
}
边权下放
从网上学的,详见例题 3。
题目讲解
例题 1:P3384 【模板】重链剖分/树链剖分
简明地讲,这道题就是用线段树维护一个树链剖分,要做到子树改,树链改,子树查,树链查。
#include<bits/stdc++.h>
using namespace std;
#define N 200001
int w[N];
int n,m,p,s;
struct node{
int to,next;
}edge[N];
int head[N],ccnt;
inline void add(int u,int v)
{
edge[++ccnt].next=head[u];
edge[ccnt].to=v;
head[u]=ccnt;
}
int dep[N],son[N],f[N],siz[N];
void dfs1(int u,int fa)
{
dep[u]=dep[fa]+1;
f[u]=fa;
siz[u]=1;
for(int i=head[u];i;i=edge[i].next)
{
int v=edge[i].to;
if(v==fa) continue;
dfs1(v,u);
siz[u]+=siz[v];
if(siz[son[u]]<siz[v])son[u]=v;
}
}
int top[N],rk[N],id[N],nw[N];
int cnt;
void dfs2(int u,int t)
{
top[u]=t,id[u]=++cnt,nw[cnt]=w[u];
if(!son[u])return ;
dfs2(son[u],t);
// rk[cnt]=u;
for(int i=head[u];i;i=edge[i].next)
{
int v=edge[i].to;
if(v==son[u]||v==f[u])continue;
dfs2(v,v);
}
}
class wme{
private:
struct nodee
{
int num,lazy;
}tr[N<<2];
public:
#define lid now<<1
#define rid now<<1|1
void build(int now,int l,int r)
{
if(l==r)
{
tr[now].num=nw[r];
return ;
}
int mid=(l+r)>>1;
build(lid,l,mid),build(rid,mid+1,r);
tr[now].num=(tr[lid].num+tr[rid].num)%p;
}
void Add(int now,int l,int r,int k)
{
tr[now].num+=k*(r-l+1);
tr[now].lazy+=k;
}
void pushdown(int now,int l,int r)
{
if(!tr[now].lazy)return;
int mid=(l+r)>>1;
Add(lid,l,mid,tr[now].lazy);
Add(rid,mid+1,r,tr[now].lazy);
tr[now].lazy=0;
}
void upd(int now,int l,int r,int x,int y,int k)
{
if(x<=l&&r<=y)
{
Add(now,l,r,k);return ;
}
pushdown(now,l,r);
int mid=(l+r)>>1;
if(x<=mid)upd(lid,l,mid,x,y,k);
if(y>mid) upd(rid,mid+1,r,x,y,k);
tr[now].num=(tr[lid].num+tr[rid].num)%p;
}
void upd_Path(int u,int v,int k)
{
while(top[u]!=top[v])
{
if(dep[top[u]]<dep[top[v]])swap(u,v);
upd(1,1,n,id[top[u]],id[u],k);
u=f[top[u]];
}
if(dep[u]<dep[v])swap(u,v);
upd(1,1,n,id[v],id[u],k);
}
void upd_Tree(int u,int k)
{
upd(1,1,n,id[u],id[u]+siz[u]-1,k);
}
long long query(int now,int l,int r,int x,int y)
{
if(x<=l&&r<=y) return tr[now].num;
pushdown(now,l,r);
int mid=(l+r)>>1;
long long res=0;
if(x<=mid) res+=query(lid,l,mid,x,y);
if(y>mid) res+=query(rid,mid+1,r,x,y);
return res%p;
}
long long query_Path(int u,int v)
{
long long res=0;
while(top[u]!=top[v])
{
if(dep[top[u]]<dep[top[v]])u^=v^=u^=v;
res+=query(1,1,n,id[top[u]],id[u]);
u=f[top[u]];
}res%=p;
if(dep[u]<dep[v])u^=v^=u^=v;
res+=query(1,1,n,id[v],id[u]);
return res;
}
long long query_Tree(int u)
{
return query(1,1,n,id[u],id[u]+siz[u]-1);
}
}st;
signed main()
{
cin>>n>>m>>s>>p;
for(int i=1;i<=n;i++) cin>>w[i];
for(int i=1;i<n;i++)
{
int u,v;
cin>>u>>v;
add(u,v),add(v,u);
}
dfs1(s,0);
dfs2(s,s);
st.build(1,1,n);
while(m--)
{
int t,u,v,k;
cin>>t>>u;
if(t==1)
{
cin>>v>>k;
st.upd_Path(u,v,k);
}
else if(t==2)
{
cin>>v;
cout<<st.query_Path(u,v)%p<<endl;
}
else if(t==3)
{
cin>>k;
st.upd_Tree(u,k);
}
else cout<<st.query_Tree(u)%p<<endl;
}
return 0;
}
例题 2:#P396. 「ZJOI 2008」树的统计
单点修改,树链求和、最大值。
最大值的求法与求和大同小异。
核心代码:
long long query_Pathmax(int u,int v)
{
long long res=-1145141919;
while(top[u]!=top[v])
{
if(dep[top[u]]<dep[top[v]])swap(u,v);
res=max(res,querymax(1,1,n,id[top[u]],id[u]));
u=f[top[u]];
}
if(dep[u]<dep[v])swap(u,v);
res=max(res,querymax(1,1,n,id[v],id[u]));
return res;
}
例题 3:#P398. [SPOJ375 QTREE]难存的情缘
这道题给出了书上每条边的权值,会修改某条边的权值和查询两点树链边权最大值。
难点在边权下放和修改上。我参考了别人的思路,用了几个 vector
去搞。
记录每条边的下方节点,把边权转化成点权。
void dfs1(int u)
{
siz[u]=1;
for(int i=0;i<edge[u].size();i++)
{
int v=edge[u][i];
if(!siz[v])
{
dep[v]=dep[u]+1;f[v]=u;
indexnode[Index[u][i]]=v;//重点
a[v]=W[u][i];
dfs1(v);
siz[u]+=siz[v];
if(siz[v]>siz[son[u]])
son[u]=v;
}
}
}
for(int i=1;i<n;i++)
{
cin>>u>>v>>w;
Index[u].push_back(i);//Index 向量和 indexnode 数组完成了边权下放
Index[v].push_back(i);//
edge[u].push_back(v);
edge[v].push_back(u);
W[u].push_back(w);
W[v].push_back(w);
// add(u,v,w),add(v,u,w);
}
还有一点,在 query_path
的时候,不能包含两点的 \(lca\),因为 \(lca\) 的点权是 \(lca\) 上面的边的,不包含在树链中。
完整代码:
#include<bits/stdc++.h>
using namespace std;
int n,m;
const int N=2e4+2;
int ww[N];
vector<int>Index[N],edge[N],W[N];
int indexnode[N];
int a[N];
int son[N],siz[N],f[N],dep[N];
void dfs1(int u)
{
siz[u]=1;
for(int i=0;i<edge[u].size();i++)
{
int v=edge[u][i];
if(!siz[v])
{
dep[v]=dep[u]+1;f[v]=u;
indexnode[Index[u][i]]=v;
a[v]=W[u][i];
dfs1(v);
siz[u]+=siz[v];
if(siz[v]>siz[son[u]])
son[u]=v;
}
}
}
int top[N],nw[N],id[N],cnt;
void dfs2(int u,int t)
{
top[u]=t;
id[u]=++cnt;
nw[cnt]=u;
if(son[u])dfs2(son[u],t);
for(int i=0;i<edge[u].size();i++)
{
int v=edge[u][i];
if(v==son[u]||v==f[u])continue;
dfs2(v,v);
}
}
class wmw{
private:
struct nodee{
int mx;
}tr[N<<2];
public:
#define lid now<<1
#define rid now<<1|1
void build(int now,int l,int r)
{
if(l==r)
{
tr[now].mx=a[nw[l]];return ;
}
int mid=(l+r)>>1;
build(lid,l,mid),build(rid,mid+1,r);
tr[now].mx=max(tr[lid].mx,tr[rid].mx);
}
void change(int now,int l,int r,int x,int y,int k)
{
if(x<=l&&r<=y)
{
tr[now].mx=k;
return ;
}
int mid=(l+r)>>1;
if(x<=mid) change(lid,l,mid,x,y,k);
if(y>mid) change(rid,mid+1,r,x,y,k);
tr[now].mx=max(tr[lid].mx,tr[rid].mx);
}
int query(int now,int l,int r,int x,int y)
{
if(x<=l&&r<=y)
{
return tr[now].mx;
}
int res=-INT_MAX-1;
int mid=(l+r)>>1;
if(x<=mid) res=max(res,query(lid,l,mid,x,y));
if(y>mid) res=max(res,query(rid,mid+1,r,x,y));
return res;
}
int query_path(int u,int v)
{
int res=-1145141919;
while(top[u]!=top[v])
{
if(dep[top[u]]<dep[top[v]])swap(u,v);
res=max(res,query(1,1,n,id[top[u]],id[u]));
u=f[top[u]];
}
if(dep[u]>dep[v]) swap(u,v);
res=max(res,query(1,1,n,id[u]+1,id[v]));
return res;
}
}st;
signed main()
{
cin>>n;int u,v,w;
for(int i=1;i<n;i++)
{
cin>>u>>v>>w;
Index[u].push_back(i);
Index[v].push_back(i);
edge[u].push_back(v);
edge[v].push_back(u);
W[u].push_back(w);
W[v].push_back(w);
}
dfs1(1);
dfs2(1,1);
st.build(1,1,n);
string c;cin>>c;
while(c!="DONE")
{
int x,y;
cin>>x>>y;
if(c[0]=='Q')
{
cout<<st.query_path(x,y)<<endl;
}
else if(c[0]=='C')
{
x=indexnode[x];
st.change(1,1,n,id[x],id[x],y);
}
cin>>c;
}
}
例题 4:P3178 [HAOI2015] 树上操作
板子题。题目要求单点修改,子树修改,树链查询。
但是树链查询是从根节点到某一节点的,比较弱化,可以使用差分去做,但是我选择树剖+线段树。
子树查询代码:
void add_tree(int u,int k)
{
change(1,1,n,id[u],id[u]+siz[u]-1,k);
}
例题 5:P3833 [SHOI2012] 魔法树
这道题核心考察了树链加。
依然是 lca 的代码,改几下就好了。
void add_path(int u,int v,int k)
{
while(top[u]!=top[v])
{
if(dep[top[u]]<dep[top[v]]) swap(u,v);
change(1,1,n,id[top[u]],id[u],k);
u=f[top[u]];
}
if(dep[u]>dep[v])swap(u,v);
change(1,1,n,id[u],id[v],k);
}
例题 6:P4281 [AHOI2008] 紧急集合 / 聚会
这道题只需要求出 \(lca\) 即可。但是在求两点距离的时候,不需要用线段树维护,去 query_path
。
只需要用到 \(dep\) 数组。
int dis(int u,int v)
{
int lc=lca(u,v);
return dep[u]+dep[v]-2*dep[lc];
}
例题 7:P3979 遥远的国度
是个可爱的小紫。
和「BZOJ3306」树 这道题很像啊,换根树剖。
搞清楚当前 \(root\) 的深度和查询的 \(u\) 的关系就行了。
int getans(int u)
{
if(root==u) return st.query(1,1,n,1,n);
if(dep[u]>=dep[root]) return st.query_tree(u);
if(dep[u]<dep[root]&&f[lca(u,root)]==u)
{
int ff=lca(u,root);
return min(st.query(1,1,n,1,id[ff]-1),st.query(1,1,n,id[ff]+siz[ff],n));
}
else return st.query_tree(u);
}
例题 8:P2146 [NOI2015] 软件包管理器
全世界的 OI 同仁都应该写一下这道题。。
浪费了我整整半天的时间,重构了一遍。
-
对于 \(install\) 操作,先
query_path(1,u)
,再upd_Path(1,u,1)
,输出 \(dep[u]\) 减去刚刚query_path(1,u)
的值。 -
对于 \(uninstall\) 操作,输出
st.query_Tree(u)
,再upd_Tree(u,0)
修改子树。
代码里面总共有 3 个 bug。
- dfs2 里的条件
void dfs2(int u,int t)
{
top[u]=t;id[u]=++cnt;
if(!son[u]) return ;
dfs2(son[u],t);
for(int i=0;i<edge[u/*这里,原本写的是i*/].size();i++)
{
int v=edge[u][i];
if(v==f[u]||v==son[u]) continue;
dfs2(v,v);
}
}
- \(pushdown\)
void pushdown(int now,int l,int r)
{
if(tr[now].lazy==-1) return ;
tr[lid].lazy=tr[rid].lazy=tr[now].lazy;
//下面的两个 r-l+1 都错了,应该是 l-mid+1 和 r-mid
tr[lid].sum=(r-l+1)*tr[now].lazy;
tr[rid].sum=(r-l+1)*tr[now].lazy;
tr[now].lazy=-1;
}
- 修改 \(pushdown\) 之后 \(pushdown\) 还是错的
void add(int now,int l,int r,int k)
{
tr[now].lazy=k;
//下面的 (r-l+1)*k 原本没有乘 k
tr[now].sum=(r-l+1)*k;
}
void pushdown(int now,int l,int r)
{
if(tr[now].lazy!=-1){
int mid=(l+r)>>1;
add(lid,l,mid,tr[now].lazy);
add(rid,mid+1,r,tr[now].lazy);
tr[now].lazy=-1;
}
}
例题 9:P4315 月下“毛景树”
整整一天!!!!
细节爆炸。
新的边权下放方法(链式前向星)
难点在于边权下放和 \(pushdown\)。
void pushdown(int now,int l,int r)
{
if(tr[now].tag!=-1)
{
tr[lid].mx=tr[rid].mx=tr[now].tag+tr[now].lazy;
tr[lid].tag=tr[rid].tag=tr[now].tag+tr[now].lazy;
tr[lid].lazy=tr[rid].lazy=0;//在区间覆盖时,子树的区间修改懒标记要清零!!
tr[now].lazy=0;
tr[now].tag=-1;
}
if(tr[now].lazy)
{
tr[lid].lazy+=tr[now].lazy;
tr[rid].lazy+=tr[now].lazy;
tr[lid].mx+=tr[now].lazy;
tr[rid].mx+=tr[now].lazy;
tr[now].lazy=0;
}
}
最难绷的是,第一遍写,调完交上去只过了第 11 个点,
第二遍写,调完交上去只有第 11 个点没过。
原来是有个数据要 change_path(2,2,5)
,由于某些神秘特性,第一次写的 \(change \ path\) 屏蔽了此操作,第二次写的却完美踩坑。
因此我加了特判:
if(c[1]=='d')
{
int u,v,w;cin>>u>>v>>w;
if(u==v) continue;//这里
st.change_path(u,v,w);
}
最终 AC 了。
#include<bits/stdc++.h>
using namespace std;
#define int long long
const int N=1e5+2;
struct node{
int to,next,w;
}edge[N<<1];
int head[N],cnt;int n,Val[N];
void add(int u,int v,int w)
{
edge[++cnt].next=head[u];
edge[cnt].to=v;
edge[cnt].w=w;
head[u]=cnt;
}
int siz[N],son[N],dep[N],f[N];
void dfs1(int u)
{
siz[u]=1;
for(int i=head[u];i;i=edge[i].next)
{
int v=edge[i].to,w=edge[i].w;
if(!siz[v])
{
dep[v]=dep[u]+1,f[v]=u;
Val[v]=w;//边权下放
dfs1(v);
siz[u]+=siz[v];
if(siz[son[u]]<siz[v]) son[u]=v;
}
}
}
int top[N],id[N],tim,nw[N];
void dfs2(int u,int t)
{
id[u]=++tim,top[u]=t,nw[tim]=u;
if(!son[u]) return ;
dfs2(son[u],t);
for(int i=head[u];i;i=edge[i].next)
{
int v=edge[i].to;
if(v==f[u]||v==son[u]) continue;
dfs2(v,v);
}
}
class wmw{
private:
struct ndoee{
int tag,lazy,mx;
}tr[N<<2];
public:
#define lid now<<1
#define rid now<<1|1
void build(int now,int l,int r)
{
tr[now].tag=-1;
if(l==r)
{
tr[now].mx=Val[nw[l]];return ;
}
int mid=(l+r)>>1;
build(lid,l,mid),build(rid,mid+1,r);
tr[now].mx=max(tr[lid].mx,tr[rid].mx);
}
void pushdown(int now,int l,int r)
{
if(tr[now].tag!=-1)
{
tr[lid].mx=tr[rid].mx=tr[now].tag+tr[now].lazy;
tr[lid].tag=tr[rid].tag=tr[now].tag+tr[now].lazy;
tr[lid].lazy=tr[rid].lazy=0;
tr[now].lazy=0;
tr[now].tag=-1;
}
if(tr[now].lazy)
{
tr[lid].lazy+=tr[now].lazy;
tr[rid].lazy+=tr[now].lazy;
tr[lid].mx+=tr[now].lazy;
tr[rid].mx+=tr[now].lazy;
tr[now].lazy=0;
}
}
void cover(int now,int l,int r,int x,int y,int k)
{
if(x<=l&&r<=y)
{
tr[now].lazy=0;
tr[now].tag=k,tr[now].mx=k;return ;
}
int mid=(l+r)>>1;
pushdown(now,l,r);
if(x<=mid) cover(lid,l,mid,x,y,k);
if(y>mid) cover(rid,mid+1,r,x,y,k);
tr[now].mx=max(tr[lid].mx,tr[rid].mx);
}
void change(int now,int l,int r,int x,int y,int k)
{
if(x<=l&&r<=y)
{
tr[now].lazy+=k,tr[now].mx+=k;return ;
}
int mid=(l+r)>>1;
pushdown(now,l,r);
if(x<=mid) change(lid,l,mid,x,y,k);
if(y>mid) change(rid,mid+1,r,x,y,k);
tr[now].mx=max(tr[lid].mx,tr[rid].mx);
}
void cover_path(int u,int v,int k)
{
while(top[u]!=top[v])
{
if(dep[top[u]]<dep[top[v]]) swap(u,v);
cover(1,1,n,id[top[u]],id[u],k);
u=f[top[u]];
}
if(dep[u]>dep[v]) swap(u,v);
cover(1,1,n,id[u]+1,id[v],k);
}
void change_path(int u,int v,int k)
{
while(top[u]!=top[v])
{
if(dep[top[u]]<dep[top[v]]) swap(u,v);
change(1,1,n,id[top[u]],id[u],k);
u=f[top[u]];
}
if(dep[u]>dep[v]) swap(u,v);
change(1,1,n,id[u]+1,id[v],k);
}
int query(int now,int l,int r,int x,int y)
{
if(x<=l&&r<=y)
{
return tr[now].mx;
}
int mid=(l+r)>>1,res=-1145141919;
pushdown(now,l,r);
if(x<=mid) res=max(res,query(lid,l,mid,x,y));
if(y>mid) res=max(res,query(rid,mid+1,r,x,y));
return res;
}
int query_path(int u,int v)
{
int res=-1145141919;
while(top[u]!=top[v])
{
if(dep[top[u]]<dep[top[v]]) swap(u,v);
res=max(res,query(1,1,n,id[top[u]],id[u]));
u=f[top[u]];
}
if(dep[u]>dep[v]) swap(u,v);
res=max(res,query(1,1,n,id[u]+1,id[v]));
return res;
}
}st;
signed main()
{
cin>>n;
for(int i=1;i<n;i++)
{
int u,v,w;
cin>>u>>v>>w;
add(u,v,w),add(v,u,w);
}
dfs1(1);
// printf("ced\n");
dfs2(1,1);
// printf("ced\n");
st.build(1,1,n);
while(191981)
{
// cout<<st.query_path(2,1)<<endl;
string c;
cin>>c;
if(c[1]=='d')
{
int u,v,w;cin>>u>>v>>w;
if(u==v) continue;//特判
st.change_path(u,v,w);
}
else if(c[2]=='v')
{
int u,v,w;
cin>>u>>v>>w;
st.cover_path(u,v,w);
}
else if(c[2]=='a')
{
int u,v;
cin>>u>>v;
u=dep[edge[u*2-1].to]<dep[edge[u*2].to]?edge[u*2].to:edge[u*2-1].to;//神奇的链式前向星边权下放
// cout<<u<<endl;
st.cover(1,1,n,id[u],id[u],v);
}
else if(c[2]=='x')
{
int u,v;cin>>u>>v;
cout<<st.query_path(u,v)<<endl;
}
else return 0;
}
}
例题 10:P3128 [USACO15DEC] Max Flow P
这道题很简单。
不需要建树,每次操作树链加,维护最大值。
最后输出 \(tr[1].mx\) 即可。
\(pushdown\) 的操作如下,比较神秘:
void pushdown(int now,int l,int r)
{
if(!tr[now].lazy) return ;
tr[lid].lazy+=tr[now].lazy;
tr[rid].lazy+=tr[now].lazy;
tr[lid].mx+=tr[now].lazy;
tr[rid].mx+=tr[now].lazy;
tr[now].lazy=0;
}
例题 11:CF343D Water Tree
看起来很板,但是某两人同时挂在这个题上。
问题在于区间覆盖上。
观察到询问中有一个操作叫
将点 \(u\) 到 \(1\) 的路径上的所有节点的权值改为 \(0\)。
在线段树的 \(pushdown\) 中我们有这样的语句:
void pushdown(int now,int l,int r)
{
if(!tr[now].lazy) return ;
int mid=(l+r)>>1;
Add(lid,l,mid,tr[now].lazy);
Add(rid,mid+1,r,tr[now].lazy);
tr[now].lazy=0;
}
可以看到,如果当前点的 \(lazy=0\),那么不会进行后面的下放操作。但是在本题中,\(0\) 是具有意义的。
意思是,执行上面说的那条操作时,节点的 \(lazy\) 就是 \(0\),不会下放,而是保留原来的值。解决的方法很简单,把 \(lazy\) 的默认值设成 \(-1\) 即可(可以参考 月下毛景树 那个题)
修改完是这样的:
void pushdown(int now,int l,int r)
{
if(tr[now].lazy==-1) return ;
int mid=(l+r)>>1;
Add(lid,l,mid,tr[now].lazy);
Add(rid,mid+1,r,tr[now].lazy);
tr[now].lazy=-1;
}
完美解决。
例题 12:CF916E Jamie and Tree
这个就很有意思了。
换根 + 树剖。
树剖部分很板,非常板,甚至不需要写树链操作的代码。
接下来就要考虑换根了:
考虑到修改是找 \(u\) 和 \(v\) 在当前根意义下的 lca,而查询是直接查原始根意义下的 lca。
什么意思呢?
看这张图,如果当前 \(root=6\),就是图三,点 \(2\) 和 \(5\) 的 lca 就是 \(3\)。
但是原始图(你剖的那棵树)中,lca 是 \(1\)。
这就是原始根意义下的 lca 和当前根意义下的 lca 的区别。
所以在修改操作中,操作的子树应是当前根意义下的 lca。
后续叙述中,我们使用 getlca()
表示查找当前根意义下的 lca,lca()
表示查找原始根意义下的 lca。
说清楚了这点,我们接着来讨论当前根分别对修改和查询操作的影响:
对于修改而言:
-
当前根与求得的当前根意义下的 lca 是同一个点,直接修改全部;
-
当前根不在查找出的 lca 子树以内,即 \(getlca(root,rt)\neq rt\)(\(rt\) 表示查找出的 lca),对修改无影响;
-
当前根在 lca 子树以内,即 \(getlca(root,rt)=rt\),需要用到容斥:
这时候 \(rt\) 的子树是整棵树中除去以 \(rt\) 的一个子节点为根,并且 \(root\) 在这个子节点的子树的子树部分,用容斥一搞就好了。
具体地,这样找这个子树:
int find(int u,int v)
{
int fx=top[u],fy=top[v];
while(fx!=fy)
{
if(dep[fx]<dep[fy]) swap(u,v),swap(fx,fy);
if(fa[fx]==v) return top[u];
u=fa[u],fx=top[u];
}
if(dep[u]>dep[v]) swap(u,v);
return son[u];
}
对于查找而言:
-
当前根与询问节点是同一个点,直接查全部;
-
当前根不在询问节点以内,即 \(lca(root,u)\neq u\),对查找无影响;
-
当前根在询问节点以内,即 \(lca(root,u)=u\),也是容斥一下就好。
整体的核心代码:
while(q--)
{
cin>>op>>u;
if(op==1)
{
root=u;
}
else if(op==2)
{
cin>>v>>w;
int rt=getlca(u,v);
if(rt==root) st.update(1,1,n,1,n,w);
else if(lca(root,rt)!=rt) st.update(1,1,n,id[rt],id[rt]+siz[rt]-1,w);
else
{
int ck=find(rt,root);
st.update(1,1,n,1,n,w),st.update(1,1,n,id[ck],id[ck]+siz[ck]-1,-w);//容斥
}
}
else
{
if(u==root) cout<<st.query(1,1,n,1,n);
else if(lca(u,root)!=u) cout<<st.query(1,1,n,id[u],id[u]+siz[u]-1);
else
{
int ck=find(u,root);
cout<<st.query(1,1,n,1,n)-st.query(1,1,n,id[ck],id[ck]+siz[ck]-1);//容斥
}
cout<<"\n";
}
}
例题 13:P9432 [NAPC-#1] rStage5 - Hard Conveyors
一道好题。
考虑最优路径一定是原路径上每个点到最近的关键点的最小值。
那我们可以预处理出所有点距离它最近的关键点的距离,拿线段树维护区间最小值。
然后原路径长度拿 BIT 维护区间和。
具体来说,我们用树形 dp 的思想来处理每个点距离它最近的关键点的距离。
-
如果 \(u\) 是关键点,那么 \(dp_u=0\);
-
否则,\(u\) 的答案只能从 \(fa_u\) 和 \(son_u\) 更新,即 \(dp_u=\min(dp_{son_u}+w_{u,son_u},dp_{fa_u}+w_{fa_u,u})\)。
那我们分两次 \(dfs\),第一次自下而上维护,第二次自上而下维护就好了。(不知到正确性,但是是对的?)
void dfsdown(int u,int f)
{
for(int i=head[u];i;i=e[i].next)
{
int v=e[i].to;
if(v==f) continue;
dfsdown(v,u);
dp[u]=min(dp[u],dp[v]+e[i].w);
}
}
void dfsup(int u,int f)
{
for(int i=head[u];i;i=e[i].next)
{
int v=e[i].to;
if(v==f) continue;
dp[v]=min(dp[v],dp[u]+e[i].w);
dfsup(v,u);
}
}
据说这题有 \(2100\) 难度,但是被我 15 分钟切了?