树上那些事——树上问题 II (重链剖分,树上启发式合并 静态链分治)

树链剖分,简称树剖。听起来是个很高级的东西。但其实它只是个比较基础的处理树上问题的工具。(人话:大多数题难点不在树链剖分上)
树剖有几种,下文介绍常用的重链剖分及其衍生的树上启发式合并算法。

重链剖分

首先要知道树剖用干什么。在 树上那些事——树上问题 I 中的 DFN 序部分,我们介绍了一种比较复杂的解决树上路径加,树上子树和问题的方法。

现在考虑:我们想要同时维护树上路径加,树上路径和。那就很难受了——毕竟不是谁都可以快速捏出一个维护一堆线段树的树上差分方法,而且题目还不一定只让我们维护加法。

这样的困难引导着我们想要找出一种更普适的方法:能不能真的把线段树放在树的路径上呢?

只要几乎无所不能的线段树被放在树上,一切都会好起来的。

于是,树链剖分方法被发明了出来。树剖通过一种特殊的 DFN 标记法,把树拆成了一条条链且没有破坏 DFN 序的良好性质。树剖完成的树由有限条数量的链组成,这让我们可以把线段树放在链上。同时,树剖还保证了树上的任意一点到树根经过的树链条数有限,便于我们在可以接受的时间复杂度下处理两点间路径的问题。

剖分规则

想要让一条链上的 DFN 连续,就必须用 DFS 一直沿一条路经标记下去。问题是:一个点会有许多个儿子,应沿着谁向下标记呢?随便选一个就可以吗?
人们提出了一个很天才的规则:去子树大小最大的那个儿子!

这有什么?我让它去子树更小的儿子不可以吗?

事实上,这保证了树上的任意一点到树根经过的树链条数有限,这个我们在下面会证明。注意到一但一条链被线段树接管,对这条链上任意区间(对应链上任意两点)的操作都是 log 级别。因此,让链更少一定是更好的。

人们将节点 \(u\) 儿子中子树最大的儿子称为 \(u\) 的重儿子,其它儿子为 \(u\) 的轻儿子。注意到重儿子可能会有多个儿子节点满足定义,这时从中任选一个当重儿子即可。重儿子仅能有一个。

由轻重儿子的定义,我们引出下列定义:

  • 由父亲节点引向重儿子的边称作重边。

  • 由父亲节点引向轻儿子的边称作轻边。

  • 各重边组成的链称作重链。

  • 各轻边组成的链称作轻链。

对于一棵进行过树剖的树,有如下性质:

  • 各重链必然不相交。

    证明:考虑相交时的状态,即一个父节点引出了两条重链,也就是一个父节点有两个重儿子,显然不符合定义。

  • 各重链总是在叶子结点结束。
    证明:由于每个树上的非叶子结点都要没有一个重儿子,只有叶子节点没有重儿子,因此重链总在叶子节点结束,不会在一个父节点还有儿子时结束。

  • 各点都在重链上
    证明:每个树上的非叶子结点都有一个重儿子,由上一个命题的证明显然。

  • 树上的任意一点到树根经过的树链条数不超过 log 量级。

    证明:由于重链之间由轻链连接,因此我们尝试证明:对于树上的任意节点,其到根节经过的轻链数不超过 log 量级。考虑何时两重链之间会被一轻链连接。

    对于轻链连接的两节点,我们将深度更小的称为父节点,深度更大的称为子节点。若父子节点之间以一轻边相连,则一定意味着父节点重儿子子树的大小大于等于当前子树的大小。因此,父节点子树的大小至少是当前节点子树大小的两倍。由于整棵树的大小为 \(n\),因此走过轻边的数量必不超过 \(\log n\) 条。于是原命题得证。

性质说完了,来整体感受一下树剖吧!下例给出了一棵树被树剖后可能的结果,其中加粗的边为重边,点内的编号为原始结点编号,点外# 符号引出的编号为点的 DFN 值。

(png)

可以发现,重链上的 DFN 连续,且子树内所有点的 DFN 在一个区间上。

下面说说树剖的代码实现。

预处理

仔细想想,在树链剖分中,我们需要记录一个节点的哪些信息?

首先是重儿子 int son、DFN 值 int dfn,这两个是显然且必要的。

既然要算重儿子,那么每个点子树的大小 int siz 也是有必要记录的。

上文我们提到了使用树剖时要从原位置沿着重链向上走,因为一个重链内可以用线段树很快的维护,我们就没必要在一个重链内走了,直接到重链的头上,再去下一个重链即可。因此还要记录这个节点从属的重链头的编号 int top,及每个点的父结点 int fa

还需要每个点的深度 int dep,这可以帮助我们在两个点一起向上走的过程中判断谁先谁后。

差不多了,虽然不同的树剖题维护的东西不尽相同,但这六样应该是必须的。

有点多?那我们封装在一个结构体中。

struct treed{
  int dfn;
  int son;
  int siz;
  int fa;
  int dep;
  int top;
}td[N];

怎么算这些值呢? DFS 就足够了,但能用一遍 DFS 算出所有东西吗?

不可能,原因是我们要保证重链 DFN 连续,就要在 DFS 前知道每个点的重儿子,但重儿子的计算又需要子树的大小。于是我们分开计算:第一遍 DFS 处理完成除了 DFN ,重链头编号之外的所有值,第二遍 DFS 处理剩下两者即可。

code

Show me the code
void dfs1(int u,int fa){
  td[u].fa=fa;
  td[u].dep=td[fa].dep+1;
  td[u].siz=1;
  int maxsub=0;// 这里不建议设成负数
  for(int i=0;i<edge[u].size();i++){
    int v=edge[u][i];
    if(v==fa)continue;
    dfs1(v,u);
    td[u].siz+=td[v].siz;
    if(td[v].siz>maxsub){
      maxsub=td[v].siz;
      td[u].son=v;
    }
  }
  return ;
}
int idx=0;// 如果有多测,这个也要清空!
void dfs2(int u,int top){// 递归时带上链头
  td[u].dfn=++idx;
  td[u].top=top;
  num[idx]=nw[u];
  if(!td[u].son)return ;// 对叶子节点的判断
  dfs2(td[u].son,top);// 先去找重儿子
  for(int i=0;i<edge[u].size();i++){
    int v=edge[u][i];
    if(v==td[u].fa||v==td[u].hs)continue;
    dfs2(v,v);
  }
}

这两遍 DFS 就是树链剖分的预处理,时间复杂度 \(O(n)\)

路径操作

由于我们让链上的 DFN 连续,因此对树上两点之间的操作,找到两点间的路径对应的多个 DFN 区间是必须的。

对于任意两点,我们以其从属的重链作为判断标准。显然如果两点在同一条重链上,我们只需在一条链上操作即可。

若两点不在同一条链上,此时我们让 链头深度更大的节点 去到下一条链,同时对整条链应用操作。注意这里比较两节点自身深度是错误的,原因是显然的。

去到下一条链的过程,就是把当前节点赋值为 节点从属重链的 头节点的 父节点,即 td[ td[u].top ].fa。这也是上文中要记录各节点的父节点的原因。

上述过程可用 while 循环结构实现,这里给出两种代码实现。

code 1

Show me the code
type procedure(int u,int v,...){
  while(td[u].top!=td[v].top){
    if(td[td[u].top].dep<=td[td[v].top].dep)swap(u,v);
    operator...
    x=td[td[u].top].fa;
  }
  if(td[u].dep>td[v].dep)swap(u,v);
  one last operator...  
      
  return ans;
}

该实现使用 swap 减少码量,更简洁,但会改变 \(u,v\) 意义。

code 2

Show me the code
type procedure(int u,int v){
  while(td[u].top!=td[v].top){
    if(td[td[u].top].dep<=td[td[v].top].dep){
      operation for v side...
      v=td[td[v].top].fa;
    }
    else{
      operation for u side...
      u=td[td[u].top].fa;
    }
  }
  if(td[u].dep>td[v].dep){
    operation from v to u...
  }
  else{
  	operation from u to v...
  }
  merge u side and v side...  
  
  return ans;
}

该实现在代码中使用 if else 结构判断深度,并在不改变 \(u,v\) 意义的情况下完成操作。这一过程书写较麻烦,但是题目若要求树上路经合并结果,这是唯一选择。这种类题我们下面讲。

关于线段树.就是对 DFN 序建树,再实现好区间上的一些操体,之后在上面树剖的路径操作中调用实现好的线段树即可。

来看些例题吧!

问题时间!

P3384 【模板】重链剖分/树链剖分

已知一棵包含 \(N\) 个结点的树(连通且无环),每个节点上包含一个数值,需要支持以下操作:

  • 1 x y z,表示将树从 \(x\)\(y\) 结点最短路径上所有节点的值都加上 \(z\)

  • 2 x y,表示求树从 \(x\)\(y\) 结点最短路径上所有节点的值之和。

  • 3 x z,表示将以 \(x\) 为根节点的子树内所有节点值都加上 \(z\)

  • 4 x 表示求以 \(x\) 为根节点的子树内所有节点值之和。

输出包含若干行,分别依次表示每个操作 \(2\) 或操作 \(4\) 所得的结果(\(P\) 取模

最基础的模板啦,实现路径加.路径和即可。但是要注意原问题中有对取模的要求,细心模即可。

直接看代码吧!

code

Show me the code
#define psb push_back
#define mkp make_pair
#define rep(i,a,b) for( int i=(a); i<=(b); ++i)
#define per(i,a,b) for( int i=(a); i>=(b); --i)
#define rd read()
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
ll read(){
  ll x=0,f=1;
  char c=getchar();
  while(c>'9'||c<'0'){if(c=='-') f=-1;c=getchar();}
  while(c>='0'&&c<='9'){x=(x<<3)+(x<<1)+(c^48);c=getchar();}
  return x*f;
}
const int N=1e5+5;
int n,m,r,pi;int nw[N];int num[N];
struct treed{
  int dfn;int hs;int siz;int fa;int dep;int top;
}td[N];
vector<int> edge[N];
void dfs1(int u,int fa){
  td[u].fa=fa;
  td[u].dep=td[fa].dep+1;
  td[u].siz=1;
  int maxsub=-1;
  for(int i=0;i<edge[u].size();i++){
    int v=edge[u][i];
    if(v==fa)continue;
    dfs1(v,u);
    td[u].siz+=td[v].siz;
    if(td[v].siz>maxsub){
      maxsub=td[v].siz;
      td[u].hs=v;
    }
  }
  return ;
}
int idx=0;
void dfs2(int u,int top){
  td[u].dfn=++idx;
  td[u].top=top;
  num[idx]=nw[u];
  if(!td[u].hs)return ;
  dfs2(td[u].hs,top);
  for(int i=0;i<edge[u].size();i++){
    int v=edge[u][i];
    if(v==td[u].fa||v==td[u].hs)continue;
    dfs2(v,v);
  }
}
struct segt{
  int l,r;int v;int lzt;
}t[N*4];
void pushup(int u){
  int ls=u*2;
  int rs=u*2+1;
  t[u].v=(t[ls].v+t[rs].v)%pi;
  return ;
}
void build(int p,int l,int r){
  t[p].l=l;t[p].r=r;t[p].lzt=0;
  if(l==r){
    t[p].v=num[l]%pi;
    return ;
  }
  int mid=l+r>>1;
  build(p*2,l,mid);
  build(p*2+1,mid+1,r);
  pushup(p);
}
void pushdown(int p){
  if(t[p].lzt==0)return ;
  int k=t[p].lzt;
  t[p].lzt=0;

  int ls=p*2;
  int rs=p*2+1;
  t[ls].v+=(t[ls].r-t[ls].l+1)*k;
  t[ls].v%=pi;t[ls].lzt+=k;
  t[rs].v+=(t[rs].r-t[rs].l+1)*k;
  t[rs].v%=pi;t[rs].lzt+=k;  
    
  return ;
}
void add(int p,int l,int r,int k){
  if(l<=t[p].l&&t[p].r<=r){
    t[p].v+=(t[p].r-t[p].l+1)*k;
    t[p].v%=pi;
    t[p].lzt+=k;
    return ;
  }
  pushdown(p);
  int mid=t[p].l+t[p].r>>1;
  if(l<=mid)add(p*2,l,r,k);
  if(mid<r)add(p*2+1,l,r,k);
  pushup(p);
  return ;
}
ll query(int p,int l,int r){
  if(l<=t[p].l&&t[p].r<=r)return t[p].v;
  pushdown(p);
  int mid=t[p].l+t[p].r>>1;
  ll sub=0;
  if(l<=mid){sub+=query(p*2,l,r);sub%=pi;}
  if(mid<r){sub+=query(p*2+1,l,r);sub%=pi;}
  return sub;
}
void cadd(int x,int y,int k){
  k%=pi;
  while(td[x].top!=td[y].top){
    if(td[td[x].top].dep<=td[td[y].top].dep)swap(x,y);
    add(1,td[td[x].top].dfn,td[x].dfn,k);
    x=td[td[x].top].fa;
  }
  if(td[x].dep>td[y].dep)swap(x,y);
  add(1,td[x].dfn,td[y].dfn,k);
  return ;
}
ll cquery(int x,int y){
  ll sub=0;
  while(td[x].top!=td[y].top){
    if(td[td[x].top].dep<=td[td[y].top].dep)swap(x,y);
    sub+=query(1,td[td[x].top].dfn,td[x].dfn);
    sub%=pi;
    x=td[td[x].top].fa;
  }
  if(td[x].dep>td[y].dep)swap(x,y);
  sub+=query(1,td[x].dfn,td[y].dfn);
  sub%=pi;
  return sub;
}
int main(){
  
  cin>>n>>m>>r>>pi;
  for(int i=1;i<=n;i++)cin>>nw[i];
  for(int i=1;i<n;i++){
    int u,v;cin>>u>>v;
    edge[u].push_back(v);
    edge[v].push_back(u);
  }
  dfs1(r,0);dfs2(r,r);build(1,1,n);
  for(int i=1;i<=m;i++){
    int op;cin>>op;
    if(op==1){
      int x,y,z;cin>>x>>y>>z;
      cadd(x,y,z);
    }
    if(op==2){
      int x,y;cin>>x>>y;
      cout<<cquery(x,y)%pi<<'\n';
    }
    if(op==3){
      int x,k;cin>>x>>k;
      add(1,td[x].dfn,td[x].dfn+td[x].siz-1,k);
    }
    if(op==4){
      int x;cin>>x;
      cout<<query(1,td[x].dfn,td[x].dfn+td[x].siz-1)%pi<<'\n';
    }
  }

  return 0;
}

上述代码的时间复杂度为 \(O( n \log^2 n )\),此为一般树链剖分的基础时间复杂度。

P3379 【模板】最近公共祖先

树剖也可以找 LCA ,他的时间复杂度为 \(O(n)\) 预处理,\(O(\log n)\) 查询,且常数比倍增要小,但码量也多了不少。(其实没多多少,听到树剖的心理作用啦)

具体地,我们在路经查询中不做其它操作,仅是往上跳。等两点跳到相同树链上时,比较两点深度即得 LCA 。

code ,比倍增快了不少。

Show me the code
const int N=1e6+5;
int n,m,r,pi;int nw[N];int num[N];
struct treed{
  int dfn;int hs;int siz;int fa;int dep;int top;
}td[N];
vector<int> edge[N];
void dfs1(int u,int fa){
  td[u].fa=fa;
  td[u].dep=td[fa].dep+1;
  td[u].siz=1;
  int maxsub=-1;
  for(int i=0;i<edge[u].size();i++){
    int v=edge[u][i];
    if(v==fa)continue;
    dfs1(v,u);
    td[u].siz+=td[v].siz;
    if(td[v].siz>maxsub){
      maxsub=td[v].siz;
      td[u].hs=v;
    }
  }
  return ;
}
int idx=0;
void dfs2(int u,int top){
  td[u].dfn=++idx;
  td[u].top=top;
  num[idx]=nw[u];
  if(!td[u].hs)return ;
  dfs2(td[u].hs,top);
  for(int i=0;i<edge[u].size();i++){
    int v=edge[u][i];
    if(v==td[u].fa||v==td[u].hs)continue;
    dfs2(v,v);
  }
}
ll lca(int u,int v){
  while(td[u].top!=td[v].top){
    if(td[td[u].top].dep<=td[td[v].top].dep)swap(u,v);
    u=td[td[u].top].fa;
  }
  if(td[u].dep<td[v].dep)return u;
  else return v;
}
int main(){
  
  cin>>n>>m>>r;
  for(int i=1;i<n;i++){
    int u,v;cin>>u>>v;
    edge[u].push_back(v);
    edge[v].push_back(u);
  }
  dfs1(r,0);
  dfs2(r,r);
  for(int i=1;i<=m;i++){
    int u,v;cin>>u>>v;
    cout<<lca(u,v)<<'\n';
  }

  
  return 0;
}

P1505 [国家集训队] 旅游

给定一棵 \(n\) 个节点的树,边带权,编号 \(0 \sim n-1\),需要支持五种操作:

  • C i w 将输入的第 \(i\) 条边权值改为 \(w\)
  • N u v\(u,v\) 节点之间的边权都变为相反数;
  • SUM u v 询问 \(u,v\) 节点之间边权和;
  • MAX u v 询问 \(u,v\) 节点之间边权最大值;
  • MIN u v 询问 \(u,v\) 节点之间边权最小值。

保证任意时刻所有边的权值都在 \([-1000,1000]\) 内。

线段树码量题。

该题中,由于权值在边上,要把权值放在点上才能用树剖维护,那我们考虑让点表示边权。显然一个点只可以代表上方边的边权,因为如果代表下方边,对于一个有多个儿子的父节点是不好办的。

因此,除了正常的树链剖分,还要注意修改时不要改两点的 LCA ,分讨一下即可。什么?你说你不会线段树上的这些操作?

code

Show me the code
#define psb push_back
#define mkp make_pair
#define rep(i,a,b) for( int i=(a); i<=(b); ++i)
#define per(i,a,b) for( int i=(a); i>=(b); --i)
#define rd read()
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
ll read(){
  ll x=0,f=1;
  char c=getchar();
  while(c>'9'||c<'0'){if(c=='-') f=-1;c=getchar();}
  while(c>='0'&&c<='9'){x=(x<<3)+(x<<1)+(c^48);c=getchar();}
  return x*f;
}
const int N=2e5+5;
int n,m;
struct e{
  int v;
  int w;
  int eid;
};
vector<e> edge[N];
int nw[N];
int reff[N];
struct treed{
  int fa;int top;int son;int siz;int dep;int dfn;
}td[N];
int num[N];
void dfs1(int u,int fa){
  td[u].dep=td[fa].dep+1;
  td[u].fa=fa;
  td[u].siz=1;
  int hson=0,hsonv=-1;
  for(int i=0;i<edge[u].size();i++){
    int v=edge[u][i].v;
    int w=edge[u][i].w;
    if(v==fa)continue;
    nw[v]=w;
    reff[edge[u][i].eid]=v;
    dfs1(v,u);
    td[u].siz+=td[v].siz;
    if(hsonv<td[v].siz){
      hsonv=td[v].siz;
      hson=v;
    }
  }
  td[u].son=hson;
  return ;
}
int idx=0;
void dfs2(int u,int top){
  idx++;
  td[u].top=top;
  td[u].dfn=idx;
  num[idx]=nw[u];
  if(!td[u].son)return;
  dfs2(td[u].son,top);
  for(int i=0;i<edge[u].size();i++){
    int v=edge[u][i].v;
    int w=edge[u][i].w;
    if(v==td[u].fa||v==td[u].son)continue;
    dfs2(v,v);
  }
}
struct seg{
  int l,r;
  int pv,nv;
  bool lzt;
  int maxv;
  int minv;
}t[N<<2];
void pushup(int p){
  int ls=p*2;
  int rs=p*2+1;
  t[p].nv=t[ls].nv+t[rs].nv;
  t[p].pv=t[ls].pv+t[rs].pv;
  t[p].maxv=max(t[ls].maxv,t[rs].maxv);
  t[p].minv=min(t[ls].minv,t[rs].minv);
  return ;
}
void build(int p,int l,int r){
  t[p].l=l;
  t[p].r=r;
  t[p].pv=t[p].nv=t[p].maxv=t[p].minv=t[p].lzt=0;
  if(l==r){
    if(num[l]>=0){
      t[p].pv=num[l];
      t[p].nv=0;
    }
    else{
      t[p].pv=0;
      t[p].nv=-num[l];
    }
    t[p].maxv=num[l];
    t[p].minv=num[l];
    return ;
  }
  int mid=l+r>>1;
  build(p*2,l,mid);
  build(p*2+1,mid+1,r);
  pushup(p);

  return ;
}
void pushdown(int p){
  if(t[p].lzt==0)return ;
  t[p].lzt=0;
  int ls=p*2,rs=p*2+1;
  t[ls].lzt^=1;
  t[rs].lzt^=1;
  swap(t[ls].maxv,t[ls].minv);
  t[ls].maxv*=-1;t[ls].minv*=-1;
  swap(t[ls].pv,t[ls].nv);
  swap(t[rs].maxv,t[rs].minv);
  t[rs].maxv*=-1;t[rs].minv*=-1;
  swap(t[rs].pv,t[rs].nv);

  return ;
}
void add(int p,int pos,int k){
  if(t[p].l==t[p].r&&t[p].l==pos){
    if(k>=0){
      t[p].pv=k;
      t[p].nv=0;
    }
    else{
      t[p].nv=-k;
      t[p].pv=0;
    }
    t[p].maxv=k;
    t[p].minv=k;
    return ;
  }
  int mid=t[p].l+t[p].r>>1;
  pushdown(p);
  if(pos<=mid)add(p*2,pos,k);
  if(mid<pos)add(p*2+1,pos,k);
  pushup(p);

  return ;
}
void rev(int p,int l,int r){
  if(l<=t[p].l&&t[p].r<=r){
    t[p].lzt^=1;
    swap(t[p].maxv,t[p].minv);
    t[p].maxv*=-1;t[p].minv*=-1;
    swap(t[p].pv,t[p].nv);
    return ;
  }
  int mid=t[p].l+t[p].r>>1;
  pushdown(p);
  if(l<=mid)rev(p*2,l,r);
  if(mid<r)rev(p*2+1,l,r);
  pushup(p);

  return ;
}
int querys(int p,int l,int r){
  if(l<=t[p].l&&t[p].r<=r){
    return t[p].pv-t[p].nv;
  }
  int mid=t[p].l+t[p].r>>1;
  pushdown(p);
  int sub=0;
  if(l<=mid)sub+=querys(p*2,l,r);
  if(mid<r)sub+=querys(p*2+1,l,r);

  return sub;
}
int qmax(int p,int l,int r){
  if(l<=t[p].l&&t[p].r<=r){
    return t[p].maxv;
  }
  int mid=t[p].l+t[p].r>>1;
  pushdown(p);
  int sub=INT_MIN;
  if(l<=mid)sub=max(sub,qmax(p*2,l,r));
  if(mid<r)sub=max(sub,qmax(p*2+1,l,r));

  return sub;
}
int qmin(int p,int l,int r){
  if(l<=t[p].l&&t[p].r<=r){
    return t[p].minv;
  }
  int mid=t[p].l+t[p].r>>1;
  pushdown(p);
  int sub=INT_MAX;
  if(l<=mid)sub=min(sub,qmin(p*2,l,r));
  if(mid<r)sub=min(sub,qmin(p*2+1,l,r));

  return sub;
}
int csum(int u,int v){
  int res=0;
  while(td[u].top!=td[v].top){
    if(td[td[u].top].dep<td[td[v].top].dep)swap(u,v);
    res+=querys(1,td[td[u].top].dfn,td[u].dfn);
    u=td[td[u].top].fa;
  }
  if(v==u)return res;
  else if(td[u].dep<td[v].dep)res+=querys(1,td[td[u].son].dfn,td[v].dfn);
  else res+=querys(1,td[td[v].son].dfn,td[u].dfn);
  return res;
}
void crev(int u,int v){
  while(td[u].top!=td[v].top){
    if(td[td[u].top].dep<td[td[v].top].dep)swap(u,v);
    rev(1,td[td[u].top].dfn,td[u].dfn);
    u=td[td[u].top].fa;
  }
  if(v==u)return ;
  else if(td[u].dep<td[v].dep)rev(1,td[td[u].son].dfn,td[v].dfn);
  else rev(1,td[td[v].son].dfn,td[u].dfn);
  return ;
}
int cmax(int u,int v){
  int res=INT_MIN;
  while(td[u].top!=td[v].top){
    if(td[td[u].top].dep<td[td[v].top].dep)swap(u,v);
    res=max(res,qmax(1,td[td[u].top].dfn,td[u].dfn));
    u=td[td[u].top].fa;
  }
  if(v==u)return res;
  else if(td[u].dep<td[v].dep)res=max(res,qmax(1,td[td[u].son].dfn,td[v].dfn));
  else res=max(res,qmax(1,td[td[v].son].dfn,td[u].dfn));
  return res;
}
int cmin(int u,int v){
  int res=INT_MAX;
  while(td[u].top!=td[v].top){
    if(td[td[u].top].dep<td[td[v].top].dep)swap(u,v);
    res=min(res,qmin(1,td[td[u].top].dfn,td[u].dfn));
    u=td[td[u].top].fa;
  }
  if(v==u)return res;
  else if(td[u].dep<td[v].dep)res=min(res,qmin(1,td[td[u].son].dfn,td[v].dfn));
  else res=min(res,qmin(1,td[td[v].son].dfn,td[u].dfn));
  return res;
}
int main(){
  
  cin>>n;
  for(int i=1;i<n;i++){
    int u,v,w;
    cin>>u>>v>>w;
    u++;
    v++;
    edge[u].push_back(e{v,w,i});
    edge[v].push_back(e{u,w,i});
  }
  dfs1(1,0);
  dfs2(1,1);
  build(1,1,n);
  cin>>m;
  for(int i=1;i<=m;i++){
    string s;
    cin>>s;
    if(s=="C"){
      int c,w;
      cin>>c>>w;
      add(1,td[reff[c]].dfn,w);
    }
    if(s=="N"){
      int u,v;
      cin>>u>>v;
      u++;v++;
      crev(u,v);
    }
    if(s=="SUM"){
      int u,v;
      cin>>u>>v;u++;v++;
      cout<<csum(u,v)<<'\n';
    }
    if(s=="MAX"){
      int u,v;
      cin>>u>>v;u++;v++;
      cout<<cmax(u,v)<<'\n';
    }
    if(s=="MIN"){
      int u,v;
      cin>>u>>v;u++;v++;
      cout<<cmin(u,v)<<'\n';
    }
  }

  return 0;
}

当然,我觉得你会有一个更直接的思路就是把边化点点化边。你可能会问这是人写的东西?你还别说我真写了一份,(所以我不是人)

code

以下代码人类是看不到的哦~






































































































































P2486 SDOI2011 染色

给定一棵 \(n\) 个节点的无根树,共有 \(m\) 个操作,操作分为两种:

  1. 将节点 \(a\) 到节点 \(b\) 的路径上的所有点(包括 \(a\)\(b\))都染成颜色 \(c\)
  2. 询问节点 \(a\) 到节点 \(b\) 的路径上的颜色段数量。

颜色段的定义是极长的连续相同颜色被认为是一段。例如 112221 由三段组成:112221

码量+细节题,其中一个细节是测试数据全是大样例通过保龄给精神攻击。

这题即我们上文所说的要求树上合并结果的一类题。还记得小白逛公园吗?这题的线段树部分与这题很像,记录一下区间的左右颜色及区间内相同颜色段数量,合并时根据左右端点颜色,更所区间内相同颜色数量就好了。一般我们将这种操作写成重载运算符放在结构体里。

现在考虑扩展到树上,由于我们在线段树上截下的小块有左右之分,因此要仔细的考虑方向和运算顺序。

首先要知道重载运算符是有顺序区别的,加号后的元素即为传参传入的那个元素。

来到树上,由于 \(u,v\) 与其 LCA 各对应着一条路径,我们让两个空块对应 \(u,v\) 侧路径.并让 \(u\) 侧块总是接在新加入的块之左,让 \(v\) 侧块总是接在新加入的块之右。来到 LCA 后,计算 \(u+v\),即两块合并为答案。

但这里有细节,我们在 让 \(u\) 侧的块总是接在新加入的块之左 的过程,并不是简单的改变加法顺序。由于我们从树上重链提取路径的过程总是由 DFN 小值向 DFN 大值,对应着提取的区间,深度小的点在左,深度大的点在右,而 \(u\) 侧块是深度更大的那一侧,因此不能直接合并在提取区间深度小的左侧。合并前要把整个区间反转,即交换左右端点颜色,显然这样并不会影响块内部的结果,此时合并才正确。

感谢讨论区朋友们给的对拍!如果你也被交一万次代码都是零分整红温了卡了,可以试试对拍。

看看代码吧!

Show me the code
#define psb push_back
#define mkp make_pair
#define rep(i,a,b) for( int i=(a); i<=(b); ++i)
#define per(i,a,b) for( int i=(a); i>=(b); --i)
#define rd read()
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
#define int long long
ll read(){
  ll x=0,f=1;
  char c=getchar();
  while(c>'9'||c<'0'){if(c=='-') f=-1;c=getchar();}
  while(c>='0'&&c<='9'){x=(x<<3)+(x<<1)+(c^48);c=getchar();}
  return x*f;
}
const int N=1e5+5;
int n,m,r,pi;
int nw[N];
int num[N];
struct treed{
  int dfn;
  int hs;
  int siz;
  int fa;
  int dep;
  int top;
}td[N];
vector<int> edge[N];
void dfs1(int u,int fa){
  td[u].fa=fa;
  td[u].dep=td[fa].dep+1;
  td[u].siz=1;
  int maxsub=-1;
  for(int i=0;i<edge[u].size();i++){
    int v=edge[u][i];
    if(v==fa)continue;
    dfs1(v,u);
    td[u].siz+=td[v].siz;
    if(td[v].siz>maxsub){
      maxsub=td[v].siz;
      td[u].hs=v;
    }
  }
  return ;
}
int idx=0;
void dfs2(int u,int top){
  td[u].dfn=++idx;
  td[u].top=top;
  num[idx]=nw[u];
  if(!td[u].hs)return ;
  dfs2(td[u].hs,top);
  for(int i=0;i<edge[u].size();i++){
    int v=edge[u][i];
    if(v==td[u].fa||v==td[u].hs)continue;
    dfs2(v,v);
  }
}
struct segt{
  int l,r;
  int lc,rc;
  int v;
  int lzt;
  segt operator+(const segt &x) const{
    if(l==r&&l==0)return x;
    if(x.l==x.r&&x.l==0)return *this;
    segt res;
    res.l=l;
    res.r=x.r;
    res.lc=lc;
    res.rc=x.rc;
    res.lzt=0;
    if(rc==x.lc){
      res.v=x.v+v-1;
    }
    else{
      res.v=x.v+v;
    }
    return res;
  }
}t[N*4];
void build(int p,int l,int r){
  t[p].l=l;
  t[p].r=r;
  t[p].lzt=0;
  if(l==r){
    t[p].lc=num[l];
    t[p].rc=num[l];
    t[p].v=1;
    return ;
  }
  int mid=l+r>>1;
  build(p*2,l,mid);
  build(p*2+1,mid+1,r);
  t[p]=t[p*2]+t[p*2+1];
  return;
}
void pushdown(int p){
  if(t[p].lzt==0)return ;
  int k=t[p].lzt;
  t[p].lzt=0;

  int ls=p*2;
  int rs=p*2+1;
  t[ls].v=1;t[ls].lc=k;t[ls].rc=k;t[ls].lzt=k;
  t[rs].v=1;t[rs].lc=k;t[rs].rc=k;t[rs].lzt=k;

  return ;
}
void add(int p,int l,int r,int k){
  if(l<=t[p].l&&t[p].r<=r){
    t[p].v=1;
    t[p].lc=k;
    t[p].rc=k;
    t[p].lzt=k;
    return ;
  }
  pushdown(p);
  int mid=t[p].l+t[p].r>>1;
  if(l<=mid){
    add(p*2,l,r,k);
  }
  if(mid<r){
    add(p*2+1,l,r,k);
  }
  t[p]=t[p*2]+t[p*2+1];
  return ;
}
segt query(int p,int l,int r){
  if(l<=t[p].l&&t[p].r<=r){
    return t[p];
  }
  pushdown(p);
  int mid=t[p].l+t[p].r>>1;
  if(l<=mid&&mid<r){
    return query(p*2,l,r) + query(p*2+1,l,r);
  }
  else if(l<=mid){
    return query(p*2,l,r);
  }
  else if(mid<r){
    return query(p*2+1,l,r);
  }
}
void cadd(int x,int y,int k){
  while(td[x].top!=td[y].top){
    if(td[td[x].top].dep<=td[td[y].top].dep)swap(x,y);
    add(1,td[td[x].top].dfn,td[x].dfn,k);
    x=td[td[x].top].fa;
  }
  if(td[x].dep>td[y].dep)swap(x,y);
  add(1,td[x].dfn,td[y].dfn,k);
  return ;
}
segt cquery(int x,int y){
  ll sub=0;
  segt lx,ly;
  lx.l=lx.r=0;
  ly.l=ly.r=0;
  while(td[x].top!=td[y].top){
    if(td[td[x].top].dep<=td[td[y].top].dep){
    	segt a=query(1,td[td[y].top].dfn,td[y].dfn);
    	swap(a.lc,a.rc);
      ly=ly+a;
      y=td[td[y].top].fa;
    }
    else{
      lx=query(1,td[td[x].top].dfn,td[x].dfn)+lx;
      x=td[td[x].top].fa;
    }
  }
  if(td[x].dep>td[y].dep){
    lx=query(1,td[y].dfn,td[x].dfn)+lx;
  }
  else{
  	segt a=query(1,td[x].dfn,td[y].dfn);
  	swap(a.lc,a.rc);
    ly=ly+a;
  }
  return ly+lx;
}
signed main(){
  
  cin>>n>>m;
  for(int i=1;i<=n;i++){
    cin>>nw[i];
  }
  for(int i=1;i<n;i++){
    int u,v;
    cin>>u>>v;
    edge[u].push_back(v);
    edge[v].push_back(u);
  }
  dfs1(1,0);
  dfs2(1,1);
  build(1,1,n);
  for(int i=1;i<=m;i++){
    char op;
    cin>>op;
    if(op=='C'){
      int x,y,z;
      cin>>x>>y>>z;
      cadd(x,y,z);
    }
    if(op=='Q'){
      int x,y;
      cin>>x>>y;
      cout<<cquery(x,y).v<<'\n';
    }
  }

  return 0;
}

P7735 NOI2021 轻重边

小 W 有一棵 \(n\) 个结点的树,树上的每一条边可能是轻边或者重边。接下来你需要对树进行 \(m\) 次操作,在所有操作开始前,树上所有边都是轻边。操作有以下两种:

  1. 给定两个点 \(a\)\(b\),首先对于 \(a\)\(b\) 路径上的所有点 \(x\)(包含 \(a\)\(b\)),你要将与 \(x\) 相连的所有边变为轻边。然后再将 \(a\)\(b\) 路径上包含的所有边变为重边。
  2. 给定两个点 \(a\)\(b\),你需要计算当前 \(a\)\(b\) 的路径上一共包含多少条重边。

更像思维题。

首先给的题意非常的树剖,但是对边的修改不只有路径上的边而是邻接的所有边,这让我们无法朴素的用点代边和边换点,点换边,这怎么办?

注意到点只有两种状态,这里用个小 trick :用点的状态表示边。

我们上每个点带上权值,点权表示:这个点最近的一次被修改的编号。初始时设编号为 \(0\)

此时有很好的性质,而一条边是轻边当且仅当:其两端的两个点编号不同,或有一点编号为 \(0\);一条边是重边当且仅当:两端编号都不是 \(0\),且两端编号相等。

此时问题就变成上一个树上合并区间结果类题了!

code

Show me the code
#define psb push_back
#define mkp make_pair
#define rep(i,a,b) for( int i=(a); i<=(b); ++i)
#define per(i,a,b) for( int i=(a); i>=(b); --i)
#define rd read()
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
#define int long long
ll read(){
  ll x=0,f=1;
  char c=getchar();
  while(c>'9'||c<'0'){if(c=='-') f=-1;c=getchar();}
  while(c>='0'&&c<='9'){x=(x<<3)+(x<<1)+(c^48);c=getchar();}
  return x*f;
}
const int N=1e5+5;
int n,m,r,pi;
int nw[N];
int num[N];
struct treed{
  int dfn;
  int hs;
  int siz;
  int fa;
  int dep;
  int top;
}td[N];
vector<int> edge[N];
void dfs1(int u,int fa){
  td[u].fa=fa;
  td[u].dep=td[fa].dep+1;
  td[u].siz=1;
  int maxsub=-1;
  for(int i=0;i<edge[u].size();i++){
    int v=edge[u][i];
    if(v==fa)continue;
    dfs1(v,u);
    td[u].siz+=td[v].siz;
    if(td[v].siz>maxsub){
      maxsub=td[v].siz;
      td[u].hs=v;
    }
  }
  return ;
}
int idx=0;
void dfs2(int u,int top){
  td[u].dfn=++idx;
  td[u].top=top;
  num[idx]=nw[u];
  if(!td[u].hs)return ;
  dfs2(td[u].hs,top);
  for(int i=0;i<edge[u].size();i++){
    int v=edge[u][i];
    if(v==td[u].fa||v==td[u].hs)continue;
    dfs2(v,v);
  }
}
struct segt{
  int l,r;
  int lc,rc;
  int v;
  int lzt;
  segt operator+(const segt &x) const{
    if(l==r&&l==0)return x;
    if(x.l==x.r&&x.l==0)return *this;
    segt res;
    res.l=l;
    res.r=x.r;
    res.lc=lc;
    res.rc=x.rc;
    res.lzt=0;
    if(rc==x.lc&&rc!=0){
      res.v=x.v+v+1;
    }
    else{
      res.v=x.v+v;
    }
    return res;
  }
}t[N*4];
void build(int p,int l,int r){
  t[p].l=l;
  t[p].r=r;
  t[p].lzt=0;
  if(l==r){
    t[p].lc=num[l];
    t[p].rc=num[l];
    t[p].v=0;
    return ;
  }
  int mid=l+r>>1;
  build(p*2,l,mid);
  build(p*2+1,mid+1,r);
  t[p]=t[p*2]+t[p*2+1];
  return;
}
void pushdown(int p){
  if(t[p].lzt==0)return ;
  int k=t[p].lzt;
  t[p].lzt=0;

  int ls=p*2;
  int rs=p*2+1;
  t[ls].v=(t[ls].r-t[ls].l);t[ls].lc=k;t[ls].rc=k;t[ls].lzt=k;
  t[rs].v=(t[rs].r-t[rs].l);t[rs].lc=k;t[rs].rc=k;t[rs].lzt=k;

  return ;
}
void add(int p,int l,int r,int k){
  if(l<=t[p].l&&t[p].r<=r){
    t[p].v=(t[p].r-t[p].l);
    t[p].lc=k;
    t[p].rc=k;
    t[p].lzt=k;
    return ;
  }
  pushdown(p);
  int mid=t[p].l+t[p].r>>1;
  if(l<=mid){
    add(p*2,l,r,k);
  }
  if(mid<r){
    add(p*2+1,l,r,k);
  }
  t[p]=t[p*2]+t[p*2+1];
  return ;
}
segt query(int p,int l,int r){
  if(l<=t[p].l&&t[p].r<=r){
    return t[p];
  }
  pushdown(p);
  int mid=t[p].l+t[p].r>>1;
  if(l<=mid&&mid<r){
    return query(p*2,l,r) + query(p*2+1,l,r);
  }
  else if(l<=mid){
    return query(p*2,l,r);
  }
  else if(mid<r){
    return query(p*2+1,l,r);
  }
}
void cadd(int x,int y,int k){
  while(td[x].top!=td[y].top){
    if(td[td[x].top].dep<=td[td[y].top].dep)swap(x,y);
    add(1,td[td[x].top].dfn,td[x].dfn,k);
    x=td[td[x].top].fa;
  }
  if(td[x].dep>td[y].dep)swap(x,y);
  add(1,td[x].dfn,td[y].dfn,k);
  return ;
}
segt cquery(int x,int y){
  ll sub=0;
  segt lx,ly;
  lx.l=lx.r=0;
  ly.l=ly.r=0;
  while(td[x].top!=td[y].top){
    if(td[td[x].top].dep<=td[td[y].top].dep){
    	segt a=query(1,td[td[y].top].dfn,td[y].dfn);
    	swap(a.lc,a.rc);
      ly=ly+a;
      y=td[td[y].top].fa;
    }
    else{
      lx=query(1,td[td[x].top].dfn,td[x].dfn)+lx;
      x=td[td[x].top].fa;
    }
  }
  if(td[x].dep>td[y].dep){
    lx=query(1,td[y].dfn,td[x].dfn)+lx;
  }
  else{
  	segt a=query(1,td[x].dfn,td[y].dfn);
  	swap(a.lc,a.rc);
    ly=ly+a;
  }
  return ly+lx;
}
signed main(){
  
  int T;
  cin>>T;
  while(T--){
    memset(nw,0,sizeof nw);
    memset(num,0,sizeof num);
    memset(edge,0,sizeof edge);
    memset(td,0,sizeof td);
    memset(t,0,sizeof t);
    cin>>n>>m;
    idx=0;
    for(int i=1;i<=n;i++){
      nw[i]=0;
    }
    for(int i=1;i<n;i++){
      int u,v;
      u=rd;v=rd;
      edge[u].push_back(v);
      edge[v].push_back(u);
    }
    dfs1(1,0);
    dfs2(1,1);
    build(1,1,n);
    for(int i=1;i<=m;i++){
      int op;
      op=rd;
      if(op==1){
        int x,y;
        x=rd;y=rd;
        cadd(x,y,i);
      }
      if(op==2){
        int x,y;
        x=rd;y=rd;
        cout<<cquery(x,y).v<<'\n';
      }
    }
  }


  return 0;
}

P3250 [HNOI2016] 网络

给定一棵包含 \(n\) 个节点的树,处理 \(m\) 个事件,每个事件是以下三种类型之一:

  1. 添加请求:在节点 \(a\)\(b\) 的路径上添加一个重要度为 \(v\) 的请求
  2. 删除请求:删除之前添加的某个请求
  3. 查询请求:当节点 \(x\) 故障时,查询所有不经过 \(x\) 的现存请求中的重要度最大值

输入格式

  • 第一行两个整数 \(n,m\) 表示树的节点数和事件数
  • 接下来 \(n-1\) 行,每行两个整数 \(u,v\) 表示树的一条边
  • 接下来 \(m\) 行描述事件:
    • 0 a b v:添加重要度为 \(v\) 的请求
    • 1 t:删除第 \(t\) 个事件添加的请求
    • 2 x:查询节点 \(x\) 故障时的结果

输出格式

  • 对于每个查询事件,输出一个整数表示答案。如果没有满足条件的请求,输出 \(-1\)

数据范围

  • \(2 \le n \le 10^5\)
  • \(1 \le m \le 2 \times 10^5\)
  • 所有重要度 \(v \le 10^9\)

更像思维题。

首先考虑如果没有操作二,这样是好做的,我们定义此时线段树上的值为不受区间内点的影响的事件中,重要度最大的。由于是不受区间上的点影响的请求,因此线段树的修改操作变成对路径以外的点集操作。由于我们已经把树放到了一个 DFN 区间上,而链上是一个连续的区间,因此对一个连续的区间之外的区间整体操作是简单的。

接下来尝试放上操作二。首先注意到题目中撤销和查询操作并不是同时进行的,因此我们完全可以先把撤销操作存放在某些节点上,等到查询需要时在进行处理。

考虑双堆存操作,一堆中存所有与此段点中无关的传输操作,不计结束操作。另一堆中存放结束的操作,两堆均以重要度降序排序。这样,在询问到一区间时,只需将两堆顶比较,若相同则同时弹出该元素,至堆空或两堆栈顶元素不同即为此区间上的答案,这样就实现了对传输操作取消的懒处理。

现在考虑如何在线段树上维护双堆。注意到如果我们仍将双堆懒处理,懒标记下传的时间复杂度过大,但我们只需单点查询,考虑标记永久化,在查询时带上各段的答案,取最值即可。

复杂度分析,考虑势能法,传输操作最多被应用到 \(O ( \log n )\) 个线段树上节点,对每个传输操作处理一次,单次处理 \(O( \log n )\)。树链操作时间复杂度 \(O ( \log n )\),传输操作 \(O ( n )\) 次。综上可得时间复杂度为 \(O ( n \log ^3 n )\)。其它操作时间复杂度均低于此,不计。

code:

Show me the code
#define psb push_back
#define mkp make_pair
#define rep(i,a,b) for( int i=(a); i<=(b); ++i)
#define per(i,a,b) for( int i=(a); i>=(b); --i)
#define rd read()
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
ll read(){
  ll x=0,f=1;
  char c=getchar();
  while(c>'9'||c<'0'){if(c=='-') f=-1;c=getchar();}
  while(c>='0'&&c<='9'){x=(x<<3)+(x<<1)+(c^48);c=getchar();}
  return x*f;
}
const int N=1e5+1145;
int n,m;
vector<int> edge[N]; 
struct treed{
  int top;
  int fa;
  int dfn;
  int son;
  int siz;
  int dep;
}td[N];
void dfs1(int u,int fa){
  td[u].fa=fa;
  td[u].dep=td[fa].dep+1;
  td[u].siz=1;
  int hson=0,hsonv=0;
  for(int i=0;i<edge[u].size();i++){
    int v=edge[u][i];
    if(v==fa)continue;
    dfs1(v,u);
    td[u].siz+=td[v].siz;
    if(td[v].siz>hsonv){
      hsonv=td[v].siz;
      hson=v;
    }
  }
  td[u].son=hson;
  return ;
}
int idx=0;
void dfs2(int u,int top){
  idx++;
  td[u].dfn=idx;
  td[u].top=top;
  if(!td[u].son)return ;
  dfs2(td[u].son,top);
  for(int i=0;i<edge[u].size();i++){
    int v=edge[u][i];
    if(v==td[u].fa||v==td[u].son)continue;
    dfs2(v,v);
  }
  return ;
}
struct pq{
  int v;
  int qid;
  bool operator<(const pq &x) const{
    return x.v>v;
  }
  bool operator==(const pq &x) const{
    if(v==x.v&&qid==x.qid)return 1;
    else return 0;
  }
};
pq mem[2*N];
struct seg{
  int l,r;
  bool operator<(const seg &x)const{
    return x.l<l;
  }
};
struct segt{
  int l,r;
  priority_queue<pq> a,d;
}t[N*4];
void build(int p,int l,int r){
  t[p].l=l;t[p].r=r;
  if(l==r)return ;
  int mid=l+r>>1;
  build(p*2,l,mid);
  build(p*2+1,mid+1,r);
  return ;
}
void addp(int p,int l,int r,int id,int imp){
  if(l<=t[p].l&&t[p].r<=r){
    t[p].a.push(pq{imp,id});
    return ;
  }
  int mid=t[p].l+t[p].r>>1;
  if(l<=mid)addp(p*2,l,r,id,imp);
  if(mid<r)addp(p*2+1,l,r,id,imp);
  return ;
}
void addn(int p,int l,int r,int id,int imp){
  if(l<=t[p].l&&t[p].r<=r){
    t[p].d.push(pq{imp,id});
    return ;
  }
  int mid=t[p].l+t[p].r>>1;
  if(l<=mid)addn(p*2,l,r,id,imp);
  if(mid<r)addn(p*2+1,l,r,id,imp);
  return ;
}
pq query(int p,int k){
  pq ans;
  while(t[p].a.size()!=0){
    if(t[p].d.size()&&t[p].a.top()==t[p].d.top()){
      t[p].a.pop();
      t[p].d.pop();
     }
     else break;
  }
  if(t[p].a.size()==0)ans=pq{INT_MIN,0};
  else ans=t[p].a.top();

  if(t[p].l==t[p].r&&t[p].l==k)return ans;
  int mid=t[p].l+t[p].r>>1;
  if(k<=mid)return max(query(p*2,k),ans);
  if(mid<k)return max(query(p*2+1,k),ans);
}

vector<seg> oper[N*2];
void provide(int u,int v,int id,ll imp){
  priority_queue<seg> q;
  while(td[u].top!=td[v].top){
    if(td[td[u].top].dep<td[td[v].top].dep)swap(u,v);
    q.push(seg{td[td[u].top].dfn,td[u].dfn});
    u=td[td[u].top].fa;
  }
  if(td[u].dep>td[v].dep)swap(u,v);
  q.push(seg{td[u].dfn,td[v].dfn});
  vector<seg> ol;
  int l=1;
  while(q.size()){
    seg vv=q.top();
    q.pop();
    if(vv.l==l){l=vv.r+1;continue;}
    ol.push_back(seg{l,vv.l-1});
    l=vv.r+1;
  }
  if(l<=idx)ol.push_back(seg{l,idx});
  oper[id]=ol;
  mem[id].qid=id;
  mem[id].v=imp;
  for(auto i:ol){
    int lll=i.l,rr=i.r;
    addp(1,lll,rr,id,imp);
  }  
  return ;
}
void decline(int k){
  vector<seg> ol=oper[k];
  for(auto i:ol){
    int l=i.l,r=i.r;
    addn(1,l,r,mem[k].qid,mem[k].v);
  }  
  return ;
}
int main(){
  
  cin>>n>>m;
  for(int i=1;i<n;i++){
    int u,v;
    cin>>u>>v;
    edge[u].push_back(v);edge[v].push_back(u);
  } 
  dfs1(1,0);
  dfs2(1,1);
  build(1,1,idx);
  for(int i=1;i<=m;i++){
    int op;
    cin>>op;
    if(op==0){
      int a,b,v;cin>>a>>b>>v;
      provide(a,b,i,v);
    }
    if(op==1){
      int t;cin>>t;
      decline(t);
    }
    if(op==2){
      int x;cin>>x;
      pq ans=query(1,td[x].dfn);
      if(ans.v==INT_MIN)cout<< -1<<'\n';
      else cout<<ans.v<<'\n';
    }
  }
  
  return 0;
}

P3979 遥远的国度

给定一棵 \(n\) 个节点的树,支持三种操作:

  1. 修改根节点:将树的根设为指定节点
  2. 路径修改:将两个节点路径上的所有节点权值设为 \(v\)
  3. 子树查询:查询以指定节点为根的子树中的最小权值

输入格式

  • 第一行 \(n,m\) 表示节点数和操作数
  • 接下来 \(n-1\) 行每行两个整数表示树边
  • 一行 \(n\) 个整数表示各节点初始权值
  • 一个整数表示初始根节点
  • 接下来 \(m\) 行操作:
    • 1 id:将根改为 \(id\)
    • 2 x y v:将 \(x\)\(y\) 路径上的节点权值改为 \(v\)
    • 3 id:查询以 \(id\) 为根的子树最小权值

输出格式

  • 对每个查询操作输出一行答案

数据范围

  • \(1 \leq n,m \leq 10^5\)
  • \(0 < val_i < 2^{31}\)

在一般树链剖分操作上引入了换根,换根势必会改变各点对应的子树,但对路径无影响。显然我们只能对原树进行 DFS 操作常数次。现在想想,在根改变的情况下,对一个节点子树的 DFN 区间会有什么影响?

考虑对一个节点,找出当前的根在哪一个子树上,这一过程好做,把各子树代表的 DFN 区间找出。这样,二分根的 DFN 可找到对应的区间,也就找到了对应的子树。显然,除这一子树外的其它子树都是当前根下该点的子树。对应在 DFN 上相当于取出了根所在子树的那段区间,在线段树上区间查询另两段区间的答案再合并即可。

code:

Show me the code
#define psb push_back
#define mkp make_pair
#define rep(i,a,b) for( int i=(a); i<=(b); ++i)
#define per(i,a,b) for( int i=(a); i>=(b); --i)
#define rd read()
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
ll read(){
  ll x=0,f=1;
  char c=getchar();
  while(c>'9'||c<'0'){if(c=='-') f=-1;c=getchar();}
  while(c>='0'&&c<='9'){x=(x<<3)+(x<<1)+(c^48);c=getchar();}
  return x*f;
}
#define int long long
const int N=1e5+5;
int n,m;
int nw[N];
int capt;
vector<int> edge[N];
struct treed{
  int top;int fa;int dfn;int son;int siz;int dep;
}td[N];
int rev[N];
vector<int> lft[N],rgt[N];
void dfs1(int u,int fa){
  td[u].fa=fa;
  td[u].dep=td[fa].dep+1;
  td[u].siz=1;
  int hson=0,hsonv=0;
  for(int i=0;i<edge[u].size();i++){
    int v=edge[u][i];
    if(v==fa)continue;
    dfs1(v,u);
    td[u].siz+=td[v].siz;
    if(td[v].siz>hsonv){
      hsonv=td[v].siz;
      hson=v;
    }
  }
  td[u].son=hson;
  return ;
}
int idx=0;
void dfs2(int u,int top){
  idx++;
  td[u].dfn=idx;
  rev[idx]=u;
  td[u].top=top;
  if(!td[u].son)return ;
  dfs2(td[u].son,top);
  lft[u].push_back(td[td[u].son].dfn);
  rgt[u].push_back(td[td[u].son].dfn+td[td[u].son].siz-1);
  for(int i=0;i<edge[u].size();i++){
    int v=edge[u][i];
    if(v==td[u].fa||v==td[u].son)continue;
    dfs2(v,v);
    lft[u].push_back(td[v].dfn);
    rgt[u].push_back(td[v].dfn+td[v].siz-1);
  }
  return ;
}
struct segt{
  int l,r;
  int lzt;
  int minv;
}t[N*4];
void build(int p,int l,int r){
  t[p].l=l;t[p].r=r;t[p].lzt=INT_MIN;
  if(l==r){
    t[p].minv=nw[rev[l]];
    return ;
  }
  int mid=l+r>>1;
  build(p*2,l,mid);
  build(p*2+1,mid+1,r);
  t[p].minv=min(t[p*2].minv,t[p*2+1].minv);
  return ;
}
void pushdown(int p){
  if(t[p].lzt==INT_MIN)return ;
  int k=t[p].lzt;t[p].lzt=INT_MIN;
  int ls=p*2,rs=p*2+1;
  t[ls].lzt=k;t[ls].minv=k;
  t[rs].lzt=k;t[rs].minv=k;
  t[p].minv=min(t[ls].minv,t[rs].minv);
  return ;
}
void cover(int p,int l,int r,int k){
  if(l<=t[p].l&&t[p].r<=r){
    t[p].minv=k;
    t[p].lzt=k;
    return ;
  }
  pushdown(p);
  int mid=t[p].l+t[p].r>>1;
  if(l<=mid)cover(p*2,l,r,k);
  if(mid<r)cover(p*2+1,l,r,k);
  t[p].minv=min(t[p*2].minv,t[p*2+1].minv);
  return ;
}
int query(int p,int l,int r){
  if(l<=t[p].l&&t[p].r<=r)return t[p].minv;
  pushdown(p);
  int mid=t[p].l+t[p].r>>1;
  int sub=INT_MAX;
  if(l<=mid)sub=min(sub,query(p*2,l,r));
  if(mid<r)sub=min(sub,query(p*2+1,l,r));
  return sub;
}
void cadd(int u,int v,int k){
  while(td[u].top!=td[v].top){
    if(td[td[u].top].dep<td[td[v].top].dep)swap(u,v);
    cover(1,td[td[u].top].dfn,td[u].dfn,k);
    u=td[td[u].top].fa;
  }
  if(td[u].dfn>td[v].dfn)swap(u,v);
  cover(1,td[u].dfn,td[v].dfn,k);
  return ;
}
int cquery(int u){
  if(capt==u){
    if(td[capt].dfn==1)return query(1,1,idx);
    else return min(query(1,1,td[capt].dfn-1),query(1,td[capt].dfn,idx));
  }
  auto wp=lower_bound(rgt[u].begin(),rgt[u].end(),td[capt].dfn);
  if(wp==rgt[u].end()||td[capt].dfn<lft[u][0])return query(1,td[u].dfn,td[u].dfn+td[u].siz-1);
  int id=wp-rgt[u].begin();
  int l=lft[u][id],r=rgt[u][id];
  int res=INT_MAX;
  for(int i=0;i<lft[u].size();i++){
    int rl=lft[u][i];
    int rr=rgt[u][i];
    if(i==0&&rl>1)res=min(query(1,1,rl-1),res);
    if(i==lft[u].size()-1&&rr<idx)res=min(query(1,rr+1,idx),res);
    if(rl==l&&rr==r)continue;
    res=min(res,query(1,rl,rr));
  }
  return res;
}
signed main(){
  
  cin>>n>>m;
  for(int i=1;i<n;i++){
    int u,v;
    cin>>u>>v;
    edge[u].push_back(v);
    edge[v].push_back(u);
  }  
  for(int i=1;i<=n;i++){
    cin>>nw[i];
  }
  cin>>capt;
  dfs1(capt,0);
  dfs2(capt,capt);
  build(1,1,n);
  for(int i=1;i<=m;i++){
    int op;cin>>op;
    if(op==1){
      cin>>capt;
    }
    if(op==2){
      int u,v,w;cin>>u>>v>>w;
      cadd(u,v,w);
    }
    if(op==3){
      int id;cin>>id;
      cout<<cquery(id)<<'\n';
    }
  }
  
  return 0;
}

静态链分治,树上启发式合并

dsu on tree \(\Leftrightarrow\) 树上启发式合并 \(\Leftrightarrow\) 静态链分治

考虑这样的问题:给你一棵有根树,让你求解树上各结点的子树内的某些值,不带修改。

暴力肯定不行了,但是要知道一个子树内的答案可能会对其祖先有贡献,我们可以用子树上的结果来求解父亲乃至祖先的答案。你可能会说这不就是树上 DP 吗,但若你的答案需要保存很多的内容才能计算出来呢?一个简单的例子,若树上结点有颜色,问子树内最多的颜色是哪一种,这不是只保存最多的颜色种类和结点数量就可以计算出的。如果把每个节点为根的子树内各颜色的点的数量都保留下来,空间上显然是不够的。

此时可以应用树上启发式合并,该算法通过有规则的按排节点的求解与保留答案的顺序,在可接受的时间与空间内解决问题。

规则

我们稍作让步:可以保留子树上的答案,但对于每个根,只能保留一棵子树上的答案,这让空间问题得到解决,但如果子树的选择没有规则,与暴力依然没有区别。

还记得上文对重链剖分很关键的重儿子吗?这里我们设定一个规则:只保留重儿子子树内的答案。

具体地,我们先对原树进行树链剖分,计算一棵子树答案时先递归计算子树根各轻儿子的答案,再计算重儿子的答案,并保留重儿子子树内的答案,舍弃各轻儿子的答案。之后再单独暴力遍历各轻儿子,在重儿子基础上计算总子树答案。该算法的时间复杂度就变为了 \(O( n \log n )\)

这是什么原理?接下来我们证明该时间复杂度的正确性。

考虑何时会有 "遍历整个子树" 操作。其一是 DFS 访问到时要进行一次.其二是可能因为此树树从属于祖先上的某个轻儿子。现在我们要知道一点到根最多有多少个轻儿子。这个我们在上文证过是 \(O ( \log n )\) 的,故这个子树的根会被遍历 \(O ( \log n )\) 次,推广一下即每个点只会被遍历 \(O ( \log n )\) 次.则总计算次数为 \(O ( n \log n )\),复杂度得证。

通过精细地考虑遍历次序,我们成功做到了优雅的暴力!

问题时间!

CF208E Blood Cousins

给定一个森林(多棵树),处理关于亲属关系的查询:

定义

  • \(k\) 级祖先:\(a\)\(b\)\(k\) 级祖先,当 \(a\)\(b\) 父节点的 \((k-1)\) 级祖先(1 级祖先即父亲)
  • \(p\) 级表亲:存在 \(z\) 同时是 \(a\)\(b\)\(p\) 级祖先

问题
对于每个查询 \((v, p)\),求 \(v\) 有多少个 \(p\) 级表亲(不含自己)

输入格式

  • 第一行 \(n\) 表示人数
  • 第二行 \(n\) 个数,\(r_i\) 表示 \(i\) 的父亲(\(0\) 表示根节点)
  • 第三行 \(m\) 表示查询数
  • 接下来 \(m\) 行,每行 \(v\ p\) 表示查询

输出格式
一行 \(m\) 个整数,空格分隔每个查询的答案

数据范围

  • \(1 \leq n, m \leq 10^5\)
  • 保证输入构成合法森林

直接从一个点开始寻找它的表亲然是不好做的,题目中也提到了祖先,显然 \(p\) 级表亲即此点 \(p\) 级祖先的子树中深度为 \(p\) 的各点(除了它自己)。于是带我们将操作离线,将查找一个点 \(p\) 级表亲的任务托付给这个结点的 \(p\) 级祖先即可。此时问题变为:对于树上每一结点,求出以它为根的各深度的结点个数,朴素链分治就可以。

这里要补充一点,我们在链分治的时候一般直接保留相对于树根的关系,显然的如果只保留点与当前计算到的根的相对关系的话是不好转移的。

code

Show me the code
#define psb push_back
#define mkp make_pair
#define rep(i,a,b) for( int i=(a); i<=(b); ++i)
#define per(i,a,b) for( int i=(a); i>=(b); --i)
#define rd read()
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
ll read(){
  ll x=0,f=1;
  char c=getchar();
  while(c>'9'||c<'0'){if(c=='-') f=-1;c=getchar();}
  while(c>='0'&&c<='9'){x=(x<<3)+(x<<1)+(c^48);c=getchar();}
  return x*f;
}
const int N=2e5+5;
const int lgr=25;
vector<int> edge[N];
int dep[N];int fa[N][30];
void bfs(int r){
  memset(dep,0x3f,sizeof dep);
  queue<int> q;
  q.push(r);
  dep[r]=1;dep[0]=0;
  fa[0][0]=0;fa[r][0]=0;
  while(q.size()){
    int u=q.front();q.pop();
    for(int i=0;i<edge[u].size();i++){
      int v=edge[u][i];
      if(dep[v]>dep[u]+1){
        dep[v]=dep[u]+1;
        fa[v][0]=u;
        q.push(v);
        for(int j=1;j<=lgr;j++){
          fa[v][j]=fa[fa[v][j-1]][j-1];
        }
      }
    }
  }
  return ;
}
int p_anc(int u,int p){
  if(p==0)return u;
  for(int i=lgr;i>=0;i--){
    if(p-(dep[u]-dep[fa[u][i]])>=0){
      p-=dep[u]-dep[fa[u][i]];
      u=fa[u][i];
      if(p==0)break;
    }
  }
  return u;
}
struct treed{
  int fa;int siz;int son;int dep;
}td[N];
struct work{
  int qid;int k;
};
vector<work> req[N];
int ans[N];
int deepl[N];
void dfs1(int u,int fa){
  td[u].fa=fa;
  td[u].siz=1;
  td[u].dep=td[fa].dep+1;
  int hson=0,hsonv=0;
  for(int i=0;i<edge[u].size();i++){
    int v=edge[u][i];
    if(v==fa)continue;
    dfs1(v,u);
    td[u].siz+=td[v].siz;
    if(td[v].siz>hsonv){
      hsonv=td[v].siz;
      hson=v;
    }
  }
  td[u].son=hson;
  return ;
}
int curson=0;
void calc(int u,int op){
  deepl[td[u].dep]+=op;
  for(int i=0;i<edge[u].size();i++){
    int v=edge[u][i];
    if(v==td[u].fa||v==curson)continue;
    calc(v,op);
  }
  return ;
}
void dfs2(int u,bool hson){
  for(int i=0;i<edge[u].size();i++){
    int v=edge[u][i];
    if(v==td[u].fa||v==td[u].son)continue;
    dfs2(v,0);
  }
  if(!td[u].son){
  	deepl[td[u].dep]++;
  	if(hson==0)calc(u,-1);
  	return;
	}
  dfs2(td[u].son,1);
  curson=td[u].son;
  calc(u,1);
  curson=0;
  for(int i=0;i<req[u].size();i++){
    int id=req[u][i].qid;
    int k=req[u][i].k;
    ans[id]=deepl[td[u].dep+k]-1;
  }
  if(hson==0)calc(u,-1);
  return ;
}
int main(){
  
  int n;
  cin>>n;
  for(int i=1;i<=n;i++){
    int r;r=rd;
    if(r==0){
      edge[0].push_back(i);
      edge[i].push_back(0);
      continue;
    }
    edge[r].push_back(i);
    edge[i].push_back(r);
  }  
  bfs(0);
  dfs1(0,0);
  int m;
  cin>>m;
  for(int i=1;i<=m;i++){
    int u,k;u=rd;k=rd;
    int anc=p_anc(u,k);
    if(anc==0){ans[i]=0;continue;}
    req[anc].push_back(work{i,k});
  }
  for(int i=1;i<=n;i++){
    if(td[i].fa==0)dfs2(i,0);
  }
  for(int i=1;i<=m;i++)cout<<ans[i]<<' ';

  return 0;
}

CF600E

给定一棵以顶点 \(1\) 为根的树,每个顶点被染上某种颜色。

对于顶点 \(v\) 的子树,如果颜色 \(c\) 在该子树中出现次数不少于其他任何颜色,则称 \(c\) 为该子树的支配颜色。注意一个子树可能有多个支配颜色。

顶点 \(v\) 的子树包含 \(v\) 本身以及所有满足 \(v\) 在其到根节点路径上的顶点。

你的任务是为每个顶点 \(v\) 计算其子树中所有支配颜色的编号之和。

在链分治时维护一棵权值线段树就好啦,当然也可以写线段树合并,只是常数较大。数不大,没必要动态开点。

好像没必要用权值线段树?但是也能过就是了,下面是权值线段树的代码。

code

Show me the code
#define psb push_back
#define mkp make_pair
#define rep(i,a,b) for( int i=(a); i<=(b); ++i)
#define per(i,a,b) for( int i=(a); i>=(b); --i)
#define rd read()
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
#define int long long
ll read(){
  ll x=0,f=1;
  char c=getchar();
  while(c>'9'||c<'0'){if(c=='-') f=-1;c=getchar();}
  while(c>='0'&&c<='9'){x=(x<<3)+(x<<1)+(c^48);c=getchar();}
  return x*f;
}
const int N=1e5+5;
int n;
vector<int> edge[N];
int nw[N];
struct treed{
  int dfn;int son;int siz;int fa;int dep;
}td[N];
int idx=0;
void dfs1(int u,int fa){
  td[u].fa=fa;
  td[u].dep=td[fa].dep+1;
  td[u].siz=1;
  int hsonv=0,hson=0;
  for(int i=0;i<edge[u].size();i++){
    int v=edge[u][i];
    if(v==fa)continue;
    dfs1(v,u);
    td[u].siz+=td[v].siz;
    if(td[v].siz>hsonv){
      hsonv=td[v].siz;
      hson=v;
    }
  }
  return td[u].son=hson,void();
}
struct seg{
  int l,r;
  int masterv;
  int v;
  bool cl;
}t[N*4];
int ansl[N];
void pushup(int p){
  int ls=p*2,rs=p*2+1;
  if(t[ls].masterv==t[rs].masterv&&t[ls].masterv!=0){
    t[p].v=t[ls].v+t[rs].v;
    t[p].masterv=t[ls].masterv;
    return ;
  }
  if(t[ls].masterv<t[rs].masterv)swap(ls,rs);
  t[p].v=t[ls].v;
  t[p].masterv=t[ls].masterv;
  return ;
}
void build(int p,int l,int r){
  t[p].l=l;t[p].r=r;
  if(l==r){
    t[p].masterv=0;
    t[p].v=l;
    return ;
  }
  int mid=l+r>>1;
  build(p*2,l,mid);
  build(p*2+1,mid+1,r);
  return pushup(p),void();
}
void pushdown(int p){
  if(!t[p].cl)return ;
  int ls=p*2,rs=p*2+1;
  t[ls].cl=1;t[ls].masterv=0;
  t[rs].cl=1;t[rs].masterv=0;
  t[p].cl=0;
  return ;
}
void add(int p,int k){
  if(t[p].l==t[p].r&&t[p].l==k){
    t[p].masterv++;return ;    
  }
  int mid=t[p].l+t[p].r>>1;
  pushdown(p);
  if(k<=mid)add(p*2,k);
  if(mid<k)add(p*2+1,k);
  pushup(p);
}
void c(){t[1].cl=1;return;}
void pa(int u){
  add(1,nw[u]);
  for(int i=0;i<edge[u].size();i++){
    int v=edge[u][i];
    if(v==td[u].fa)continue;
    pa(v);
  }
  return ;
}
void dsu(int u,bool heavy){
  for(int i=0;i<edge[u].size();i++){
    int v=edge[u][i];
    if(v==td[u].son||v==td[u].fa)continue;
    dsu(v,0);
  }
  if(!td[u].son){
  	if(heavy)add(1,nw[u]);
  	ansl[u]=nw[u];return;
	} 
  dsu(td[u].son,1);
  add(1,nw[u]);
  for(int i=0;i<edge[u].size();i++){
    int v=edge[u][i];
    if(v==td[u].son||v==td[u].fa)continue;
    pa(v);
  }
  ansl[u]=t[1].v;
  if(!heavy)c();
  return ;
}
signed main(){
  
  cin>>n;
  for(int i=1;i<=n;i++)cin>>nw[i];
  for(int i=1;i<n;i++){
    int u,v;
    cin>>u>>v;
    edge[u].push_back(v);
    edge[v].push_back(u);
  }  
  build(1,1,n);
  dfs1(1,0);
  dsu(1,1);
  for(int i=1;i<=n;i++){
    cout<<ansl[i]<<' ';
  }
  
  return 0;
}

P4149 [IOI 2011] Race

给一棵树,每条边有权。求一条简单路径,权值和等于 \(k\),且边的数量最小。
如果不存在这样的路径,输出 \(-1\)

对于 \(100\%\) 的数据,保证 \(1\leq n\leq 2\times10^5\)\(0\leq k,w_i\leq 10^6\)\(0\leq u_i,v_i<n\)

可用 mop 维护从当前的根走到子树内的点,给定权值和的最小边长。在访问各轻儿子的过程中,类似于前缀和地记录 DFS 到各点的权值和与边长,用 k 减去权值和,去既有的 map 中更新答案并把此时的权值和与边长放在一个小 map 中。遍历完成后合并两 map 。所有轻儿子访问完后,视根的轻重情况保留成舍弃 map 即可。时间复杂度应该是 \(O(n \log ^3 n)\) 的。

但是这样是错的。

因为维护的不是相对于总根的值,为此,我们先对原树求一个各点到根的路径前缀和与各点的深度,map 中维护的应为:路径两点前缀和对应的深度之和的最小值。之后,类似于 lca 求树上两点之间路径的从 map 中找就可以了。

如果维护的不是相对于总根的值,继承子树状态时是会超时的。

code:

Show me the code

CF570D Tree Pequests

Roman 种了一棵包含 \(n\) 个顶点的树,每个顶点上有一个小写英文字母。顶点 \(1\) 是树的根,其余 \(n-1\) 个顶点各有一个父节点。顶点 \(i\) 的父节点是 \(p_i\)(保证 \(p_i < i\))。

顶点的深度定义为从根到该顶点的路径上的节点数(根的深度为 \(1\))。

若顶点 \(u\) 可以通过不断向父节点移动到达顶点 \(v\),则称 \(u\)\(v\) 的子树中(特别地,\(v\) 自身也属于其子树)。

给定 \(m\) 个查询,每个查询给出 \(v_i\)\(h_i\)。对于每个查询,判断在 \(v_i\) 的子树中、深度为 \(h_i\) 的所有顶点上的字母,是否能通过重新排列构成一个回文串(必须使用所有字母)。

输出 \(m\) 行,每行对应一个查询的结果。若能构成回文串输出 "Yes",否则输出 "No"。

还是把操作离线,之后考虑记录 \(f[i][j]\) 表示深度为 \(i\) ,字符 \(j\) 的出现次数,这是方便复用的,于是链分治即可。进一步的可以考虑状态压缩,将各字符出现的奇偶性压在 int 里,异或即可快速合并两深度的状态。

code

Show me the code


CF 1009 F Dominant Indices

你说得对,但是形式化题面好难写啊,之后就偷懒不写了~

思路类似 CF 600E。

code

Show me the code


CF3751D Tree and queries

思路类似 CF 600E。

code

Show me the code


CF246E Blood Cousins Return

因为点权是字符串不是很好办,那么直接把每个深度上的点扔到 map 里就行了.时间复杂度 \(O(n\log ^2 n)\)

code

Show me the code


CF741D

类似于 CF 570 D ,不过要边权转点权。然后这题在维护路径,我们依然可以将各路径上字母出现的次数状态压缩在一个 int 中,依然是先遍历子树,计算再合并 map 。计算时可以这样:我们在查找儿子子时得到了一个路径状态,合法状态为进制上只有一个 1 或全为 0。全为 0 和枚举让哪一位为1的组成的合法状态,与当前状态异成即得在大 map 里要的状态,去大 map 里查即可。这样会多出来一个 \(23\) 的常数,进似于 \(O(\log n)\),但是是与把状态扔到 mop 里同阶的,然后总复杂度应该还是 \(O(n\log ^3 n)\)

code

Show me the code


posted @ 2025-03-23 22:17  hm2ns  阅读(62)  评论(0)    收藏  举报