数据结构合并算法入门(BZOJ4719 天天爱跑步题解)

一.合并算法的引入.

我们先来看一道例题.

题目:BZOJ4719.
题目大意:给定一棵 n n n个点的树和树上每个节点的 w i w_i wi,现在给出 m m m s i s_i si t i t_i ti,表示从 s i s_i si t i t_i ti会有一个人沿树上的路径走过,并且这个人每秒移动到下一个点.现在每个人都在时刻 0 0 0走出,询问每一个点 i i i,请你输出第 w i w_i wi时刻有多少人会在点 i i i.
1 ≤ n , m ≤ 3 ∗ 1 0 5 1\leq n,m\leq 3*10^5 1n,m3105.

对你绝对没有看错我就是要用天天爱跑步这道NOIP史上第一难的题引入数据结构合并算法…

首先对于这道题的一些其他分析过程请参考BZOJ4719天天爱跑步题解.

现在我们考虑如何解决子树统计的问题.显然如果子树信息并不是具有可减性的信息的话,上面那篇题解的做法就不能用了.

如果是这样的话怎么办呢?有一个比较好的办法就是利用数据结构的合并.

具体来说,我们考虑对于对于树上每一个点对信息的贡献维护一个数据结构,然后遍历整棵树,当遍历完一个点的时候把这个点维护的信息直接合并到它的父亲上,最后在退出一个点之前统计贡献即可.

不过这个时候数据结构的合并效率就成了这个做法的复杂度瓶颈,所以接下来我们要介绍几种数据结构快速合并的方法.


二.启发式合并.

启发式合并是一类非常通用的数据结构合并技巧.

首先我们考虑一个数据结构(例如平衡树),先把所有元素塞进对应的数据结构中.每次合并两个数据结构的时候,把元素少的数据结构拆开,暴力插入的元素多的数据结构中,这种方法称为启发式合并.

若原来的总贡献数为 O ( n ) O(n) O(n),数据结构插入一个元素的时间复杂度为 O ( k ) O(k) O(k),把数据结构暴力拆开来的时间复杂度为 O ( n ) O(n) O(n),那么这样子做的时间复杂度是 O ( n k log ⁡ n ) O(nk\log n) O(nklogn)的.

至于这个算法的时间复杂度怎么来的,当然是一个元素被合并一次,它所在的数据结构元素数就增大了至少一倍啊…

关于启发式合并的例子,比如并查集的按秩合并,本质上就是一种经典的启发式合并.

那么对于例题,我们维护两个map启发式合并即可.

时间复杂度 O ( m ( log ⁡ n + log ⁡ 2 m ) ) O(m(\log n+\log^2m)) O(m(logn+log2m)).

具体代码参考文末.


三.树上启发式合并dsu on tree.

上面的map启发式合并是 O ( m ( log ⁡ n + log ⁡ 2 m ) ) O(m(\log n+\log^2m)) O(m(logn+log2m))的,两个 log ⁡ \log log的时间复杂度加上map的常数看起来并不优秀,这个复杂度是否可以优化呢?

考虑维护一个计数数组直接进行启发式合并,并记录一个数组表示哪些位置有值,那么就可以做到 O ( n log ⁡ n ) O(n\log n) O(nlogn)的时间复杂度进行启发式合并了.

听上去好像没啥问题…等等空间复杂度是 O ( n 2 ) O(n^2) O(n2)的啊.

所以我们需要优化它的空间复杂度,考虑一下只记一个数组是否可行.

具体来说,我们先维护一个计数数组 c n t [ i ] cnt[i] cnt[i],然后遍历这棵树.每当我们遍历完点 k k k的所有儿子的时候,计算出当前点为根的子树对应的 c n t cnt cnt,这可以通过一遍暴力dfs整棵子树实现.为了不对它的祖先产生影响,统计完当前点的信息后应该将 c n t cnt cnt清空.

突然发现这个做法空间复杂度是降下来了,但是时间复杂度又变回了原来的 O ( n 2 ) O(n^2) O(n2).

但是我们仔细一想,这未免有些浪费,因为一个点遍历完它的所有儿子后,最后一个儿子对应的 c n t cnt cnt数组在当前点也需要用到,完全不需要清空.

为了让每次不清空的点数尽量多,显然需要最后一个遍历当前点子树点数最多的儿子,也就是它的重儿子.

而重儿子有一个性质——除了它之外的其它儿子的子树大小都只有当前点的最多一半.

考虑对于每一个点,它只有被清空一次才会有 O ( 1 ) O(1) O(1)的时间复杂度贡献.而我们将这个过程看做这个点向上被合并的过程,只有它是轻儿子的时候才会被暴力拆出来清空,此时它所在的那棵子树大小至少为原来的 2 2 2倍.

与上面启发式合并的时间复杂度证明类似的,它最多被清空 O ( log ⁡ n ) O(\log n) O(logn)次,这个做法的总时间复杂度为 O ( n log ⁡ n ) O(n\log n) O(nlogn).

对应到例题上,我们只需要维护两个计数数组dsu on tree即可.

时间复杂度 O ( n + m log ⁡ n ) O(n+m\log n) O(n+mlogn).

具体代码参考文末.


四.线段树合并.

线段树合并不同于启发式合并,它并不是每一次把小的线段树暴力拆开插入大的线段树上,而是利用线段树的结构特性直接合并.

具体来说,我们先给每个点都建立一棵的动态开点权值线段树,并加入初始的贡献.然后设计一个返回合并后节点编号的合并函数 M e r g e ( k 0 , k 1 ) Merge(k0,k1) Merge(k0,k1)表示合并 k 0 k0 k0 k 1 k1 k1为根的线段树.

对于 M e r g e ( k 0 , k 1 ) Merge(k0,k1) Merge(k0,k1),如果 k 0 k0 k0 k 1 k1 k1中有一个空节点,那么直接返回那个非空节点;否则我们将 k 0 k0 k0 k 1 k1 k1的信息合并到 k 0 k0 k0上,并分别调用 t r [ k 0 ] . s [ 0 ] = M e r g e ( t r [ k 0 ] . s [ 0 ] , t r [ k 1 ] . s [ 1 ] ) tr[k0].s[0]=Merge(tr[k0].s[0],tr[k1].s[1]) tr[k0].s[0]=Merge(tr[k0].s[0],tr[k1].s[1]) t r [ k 1 ] . s [ 1 ] = M e r g e ( t r [ k 0 ] . s [ 1 ] , t r [ k 1 ] . s [ 1 ] ) tr[k1].s[1]=Merge(tr[k0].s[1],tr[k1].s[1]) tr[k1].s[1]=Merge(tr[k0].s[1],tr[k1].s[1]).

由于一开始线段树的节点个数是 O ( n log ⁡ n ) O(n\log n) O(nlogn)的,而每次不合并的节点必然不被遍历到,被遍历到的节点必然会被合并,此时节点个数会被减少 1 1 1,而最后合并出来的线段树节点个数不可能是负数,也就是说此时被遍历到的节点数为 O ( n log ⁡ n ) O(n\log n) O(nlogn),总时空复杂度也就是 O ( n log ⁡ n ) O(n\log n) O(nlogn)的.

具体应用到例题上,我们需要先对每个点维护一个动态开点权值线段树,并将初始的贡献加入对应点上的权值线段树中,之后线段树合并即可.

时空复杂度 O ( n + m log ⁡ n ) O(n+m\log n) O(n+mlogn).

具体代码参考文末.

不过由于这个算法存在线段树这种常数较大的数据结构,线段树合并相对于dsu on tree并不好用.

那么它有什么特别之处呢?它可以支持在线查询.

当然,我们上面的合并算法并不能直接支持在线查询,而是要在合并 k 0 , k 1 k0,k1 k0,k1的时候新建一个节点 k k k合并它们的信息并返回.显然这样做的时空复杂度仍然不变.

不过线段树合并仍然无法支持修改,因为当前点的线段树与它儿子的线段树仍然是有共用节点的.


五.例题代码.

map启发式合并:

#include
using namespace std;

#define Abigail inline void
typedef long long LL;
#define mapit map::iterator

const int N=300000;

int n,m;
struct side{
  int y,next;
}e[N*2+9];
int lin[N+9],cs;

void Ins(int x,int y){e[++cs].y=y;e[cs].next=lin[x];lin[x]=cs;}
void Ins2(int x,int y){Ins(x,y);Ins(y,x);}

int w[N+9];
int st[N+9],td[N+9];
struct node{
  int fa,son,dep,siz,top;
}d[N+9];

void Dfs_ord1(int k,int fa){
  d[k].fa=fa;
  d[k].dep=d[fa].dep+1;
  d[k].siz=1;
  for (int i=lin[k];i;i=e[i].next)
    if (e[i].y^fa){
      Dfs_ord1(e[i].y,k);
      d[k].siz+=d[e[i].y].siz;
      if (d[e[i].y].siz>d[d[k].son].siz) d[k].son=e[i].y;
    }
}

void Dfs_ord2(int k,int top){
  d[k].top=top;
  if (d[k].son) Dfs_ord2(d[k].son,top);
  for (int i=lin[k];i;i=e[i].next)
    if (e[i].y^d[k].fa&&e[i].y^d[k].son) Dfs_ord2(e[i].y,e[i].y);
}

int Query_lca(int x,int y){
  for (;d[x].top^d[y].top;x=d[d[x].top].fa)
    if (d[d[x].top].dep<d[d[y].top].dep) swap(x,y);
  return d[x].dep<d[y].dep?x:y;
}

int id[2][N+9];
map<int,int>mp[2][N+9];

int Merge(int id,int x,int y){
  if (mp[id][x].size()<mp[id][y].size()) swap(x,y);
  for (mapit it=mp[id][y].begin();it!=mp[id][y].end();++it)
    mp[id][x][it->first]+=it->second;
  mp[id][y].clear();
  return x;
}

int ans[N+9];

void Dfs_ans(int k){
  for (int i=lin[k];i;i=e[i].next)
    if (e[i].y^d[k].fa){
      Dfs_ans(e[i].y);
      id[0][k]=Merge(0,id[0][k],id[0][e[i].y]);
      id[1][k]=Merge(1,id[1][k],id[1][e[i].y]);
    }
  if (w[k]<=n) ans[k]=mp[0][id[0][k]][w[k]+d[k].dep]+mp[1][id[1][k]][w[k]-d[k].dep+n];
}

void Get_ans(){
  for (int i=1;i<=n;++i) id[0][i]=id[1][i]=i; 
  for (int i=1;i<=m;++i){
  	int lca=Query_lca(st[i],td[i]),t0=d[st[i]].dep,t1=d[st[i]].dep-2*d[lca].dep+n;
  	++mp[0][st[i]][t0];
  	--mp[0][d[lca].fa][t0];
  	++mp[1][td[i]][t1];
  	--mp[1][lca][t1];
  }
  Dfs_ans(1);
}

Abigail into(){
  scanf("%d%d",&n,&m);
  for (int i=1;i<n;++i){
  	int x,y;
  	scanf("%d%d",&x,&y);
  	Ins2(x,y);
  }
  for (int i=1;i<=n;++i)
    scanf("%d",&w[i]);
  for (int i=1;i<=m;++i)
    scanf("%d%d",&st[i],&td[i]);
}

Abigail work(){
  Dfs_ord1(1,0);
  Dfs_ord2(1,1);
  Get_ans();
}

Abigail outo(){
  for (int i=1;i<=n;++i)
    printf("%d ",ans[i]);
  puts("");
}

int main(){
  into();
  work();
  outo();
  return 0;
}

dsu on tree:

#include
using namespace std;

#define Abigail inline void
typedef long long LL;

const int N=300000;

int n,m;
struct side{
  int y,next;
}e[N*2+9];
int lin[N+9],cs;

void Ins(int x,int y){e[++cs].y=y;e[cs].next=lin[x];lin[x]=cs;}
void Ins2(int x,int y){Ins(x,y);Ins(y,x);}

int w[N+9];
int st[N+9],td[N+9];
struct node{
  int fa,son,dep,siz,top;
}d[N+9];

void Dfs_ord1(int k,int fa){
  d[k].fa=fa;
  d[k].dep=d[fa].dep+1;
  d[k].siz=1;
  for (int i=lin[k];i;i=e[i].next)
    if (e[i].y^fa){
      Dfs_ord1(e[i].y,k);
      d[k].siz+=d[e[i].y].siz;
      if (d[e[i].y].siz>d[d[k].son].siz) d[k].son=e[i].y;
    }
}

void Dfs_ord2(int k,int top){
  d[k].top=top;
  if (d[k].son) Dfs_ord2(d[k].son,top);
  for (int i=lin[k];i;i=e[i].next)
    if (e[i].y^d[k].fa&&e[i].y^d[k].son) Dfs_ord2(e[i].y,e[i].y);
}

int Query_lca(int x,int y){
  for (;d[x].top^d[y].top;x=d[d[x].top].fa)
    if (d[d[x].top].dep<d[d[y].top].dep) swap(x,y);
  return d[x].dep<d[y].dep?x:y;
}

vector<int>a[2][N+9],v[2][N+9];

int Insert(int id,int k,int t,int x){a[id][k].push_back(t);v[id][k].push_back(x);}

struct Table{
  int cnt[N*2+9],vis[N*2+9],p[N*2+9],cp;
  void Clear(){for (;cp;--cp) cnt[p[cp]]=vis[p[cp]]=0;}
  void Insert(int x,int v){if (!vis[x]) vis[x]=1,p[++cp]=x;cnt[x]+=v;}
  void Erase(int x,int v){cnt[x]-=v;}
  int Query(int x){return cnt[x];} 
}tab[2];
int ans[N+9];

void Get_tab(int k){
  int siz=a[0][k].size();
  for (int i=0;i<siz;++i) tab[0].Insert(a[0][k][i],v[0][k][i]);
  siz=a[1][k].size();
  for (int i=0;i<siz;++i) tab[1].Insert(a[1][k][i],v[1][k][i]);
}

void Dfs_tab(int k){
  Get_tab(k);
  for (int i=lin[k];i;i=e[i].next)
    if (e[i].y^d[k].fa) Dfs_tab(e[i].y);
}

void Dfs_ans(int k){
  tab[0].Clear();tab[1].Clear();
  for (int i=lin[k];i;i=e[i].next)
    if (e[i].y^d[k].fa&&e[i].y^d[k].son) Dfs_ans(e[i].y);
  if (d[k].son) Dfs_ans(d[k].son);
  Get_tab(k);
  for (int i=lin[k];i;i=e[i].next)
    if (e[i].y^d[k].fa&&e[i].y^d[k].son) Dfs_tab(e[i].y);
  if (w[k]<=n) ans[k]=tab[0].Query(w[k]+d[k].dep)+tab[1].Query(w[k]-d[k].dep+n);
}

void Get_ans(){
  for (int i=1;i<=m;++i){
  	int lca=Query_lca(st[i],td[i]),t0=d[st[i]].dep,t1=d[st[i]].dep-2*d[lca].dep+n;
  	Insert(0,st[i],t0,1);
  	Insert(0,d[lca].fa,t0,-1);
  	Insert(1,td[i],t1,1);
  	Insert(1,lca,t1,-1);
  }
  Dfs_ans(1);
}

Abigail into(){
  scanf("%d%d",&n,&m);
  for (int i=1;i<n;++i){
  	int x,y;
  	scanf("%d%d",&x,&y);
  	Ins2(x,y);
  }
  for (int i=1;i<=n;++i)
    scanf("%d",&w[i]);
  for (int i=1;i<=m;++i)
    scanf("%d%d",&st[i],&td[i]);
}

Abigail work(){
  Dfs_ord1(1,0);
  Dfs_ord2(1,1);
  Get_ans();
}

Abigail outo(){
  for (int i=1;i<=n;++i)
    printf("%d ",ans[i]);
  puts("");
}

int main(){
  into();
  work();
  outo();
  return 0;
}

线段树合并:

#include
using namespace std;

#define Abigail inline void
typedef long long LL;

const int N=300000,C=80;

int n,m;
struct side{
  int y,next;
}e[N*2+9];
int lin[N+9],cs;

void Ins(int x,int y){e[++cs].y=y;e[cs].next=lin[x];lin[x]=cs;}
void Ins2(int x,int y){Ins(x,y);Ins(y,x);}

int w[N+9];
int st[N+9],td[N+9];
struct node{
  int fa,son,dep,siz,top;
}d[N+9];

void Dfs_ord1(int k,int fa){
  d[k].fa=fa;
  d[k].dep=d[fa].dep+1;
  d[k].siz=1;
  for (int i=lin[k];i;i=e[i].next)
    if (e[i].y^fa){
      Dfs_ord1(e[i].y,k);
      d[k].siz+=d[e[i].y].siz;
      if (d[e[i].y].siz>d[d[k].son].siz) d[k].son=e[i].y;
    }
}

void Dfs_ord2(int k,int top){
  d[k].top=top;
  if (d[k].son) Dfs_ord2(d[k].son,top);
  for (int i=lin[k];i;i=e[i].next)
    if (e[i].y^d[k].fa&&e[i].y^d[k].son) Dfs_ord2(e[i].y,e[i].y);
}

int Query_lca(int x,int y){
  for (;d[x].top^d[y].top;x=d[d[x].top].fa)
    if (d[d[x].top].dep<d[d[y].top].dep) swap(x,y);
  return d[x].dep<d[y].dep?x:y;
}

struct tree{
  int v,s[2];
}tr[N*C+9];
int cn,rot[2][N+9];

void Change_add(int p,int v,int l,int r,int &k){
  if (!k) k=++cn;
  if (l==r) {tr[k].v+=v;return;}
  int mid=l+r>>1;
  p<=mid?Change_add(p,v,l,mid,tr[k].s[0]):Change_add(p,v,mid+1,r,tr[k].s[1]);
}

int Query(int p,int l,int r,int k){
  if (!k) return 0;
  if (l==r) return tr[k].v;
  int mid=l+r>>1;
  return p<=mid?Query(p,l,mid,tr[k].s[0]):Query(p,mid+1,r,tr[k].s[1]);
}

int Merge(int k0,int k1){
  if (!k0||!k1) return k0+k1;
  tr[k0].s[0]=Merge(tr[k0].s[0],tr[k1].s[0]);
  tr[k0].s[1]=Merge(tr[k0].s[1],tr[k1].s[1]);
  tr[k0].v+=tr[k1].v;
  return k0;
}

int ans[N+9];

void Dfs_ans(int k){
  for (int i=lin[k];i;i=e[i].next)
    if (e[i].y^d[k].fa){
      Dfs_ans(e[i].y);
      rot[0][k]=Merge(rot[0][k],rot[0][e[i].y]);
      rot[1][k]=Merge(rot[1][k],rot[1][e[i].y]);
    }
  if (w[k]<=n) ans[k]=Query(w[k]+d[k].dep,1,n<<1,rot[0][k])+Query(w[k]-d[k].dep+n,1,n<<1,rot[1][k]);
}

void Get_ans(){
  for (int i=1;i<=m;++i){
  	int lca=Query_lca(st[i],td[i]),t0=d[st[i]].dep,t1=d[st[i]].dep-2*d[lca].dep+n;
  	Change_add(t0,1,1,n<<1,rot[0][st[i]]);
  	Change_add(t0,-1,1,n<<1,rot[0][d[lca].fa]);
  	Change_add(t1,1,1,n<<1,rot[1][td[i]]);
  	Change_add(t1,-1,1,n<<1,rot[1][lca]);
  }
  Dfs_ans(1);
}

Abigail into(){
  scanf("%d%d",&n,&m);
  for (int i=1;i<n;++i){
  	int x,y;
  	scanf("%d%d",&x,&y);
  	Ins2(x,y);
  }
  for (int i=1;i<=n;++i)
    scanf("%d",&w[i]);
  for (int i=1;i<=m;++i)
    scanf("%d%d",&st[i],&td[i]);
}

Abigail work(){
  Dfs_ord1(1,0);
  Dfs_ord2(1,1);
  Get_ans();
}

Abigail outo(){
  for (int i=1;i<=n;++i)
    printf("%d ",ans[i]);
  puts("");
}

int main(){
  into();
  work();
  outo();
  return 0;
}

你可能感兴趣的:(算法入门)