【hdu6547】Tree(树链剖分, 线段树区间根号)

problem

【hdu6547】Tree(树链剖分, 线段树区间根号)

algorihtm

1、树链剖分

什么是树链剖分(重链剖分)?

  • 树链,就是树上的路径。剖分,就是把路径分类为重链和轻链。
  • 对于树上的一个点,与其相连的边中,连向的节点子树大小最大(重儿子) 的边叫做重边,其他的边叫轻边。重边连成的边叫做重链。
  • 下图中黑色加粗的为重链。
    【hdu6547】Tree(树链剖分, 线段树区间根号)

树链剖分(重链剖分)如何实现?

  • 重链剖分的实现是由两次dfs来实现的,第一次dfs处理出每个点的重儿子son[],子树大小size[],深度d[]及父节点f[]。 而第二遍dfs则是处理出每个点所在重链的链头top[]
  • 具体实现很简单,回溯时直接比较当前子节点和重儿子子树大小关系来更新重儿子。找完重儿子后再递归一遍,每次维护当前链顶,如果是轻边就更换当前节点为链顶即可。

树链剖分(重链剖分)有什么性质?

  • 性质1:所有重链互不相交,即每个点只属于一条重链
  • 性质2:一个点到根节点的路径上经过的边中轻边条数,重边条数都不超过log条

性质2的证明: 因为最坏情况就是这个点到根路径上经过的边都是轻边,那么每走一条轻边到达这个点的父节点就代表这个父节点至少还有一个与当前子树同样大的子树,也就是说每走一条轻边走到的点的子树大小就要*2,因此最多只能走log次。这也是为什么要选重儿子而不是随便一个儿子的原因。

树链剖分(重链剖分)有什么用?
重链剖分能保证划分出的每条链上的节点 DFS 序连续,因此可以方便地用一些维护序列的数据结构(如线段树)来维护树上路径的信息:

  • 修改 树上两点之间的路径上 所有点的值
  • 查询 树上两点之间的路径上 节点权值的 和/极值/其它

什么是DFS序?

  • 每个节点在dfs深度优先遍历中的进出栈的时间序列。
  • 我们发现,一个点的进出栈的时间点之间的时间段就是它的子树在栈中的所有时间。
  • 也就是说,子树的dfs序(时间戳)肯定大于根节点的进栈时间小于根节点的出栈时间,这就成了一个区间问题。 所以我们就把一个树上的问题“拍”到了一个线性的数据结构上面。

什么是时间戳?

  • 时间戳的概念是:按照深度优先遍历的过程,按每个节点第一次被访问的顺序,依次给予这些节点1−N的标记,这个标记就是时间戳。
  • 时间戳是DFS序的反向映射:
    id[i]=x,i是时间,x是节点,id是DFS序。 即时间对应的点(tarjan)
    dfn[x]=i,i是时间,x是节点,dfn是时间戳。即节点对应的时间(树剖)
  • 网上很多讲解说树链剖分是用的DFS序,严格地说,这个说法是不太严谨的,事实上它是通过维护时间戳来实现树转连续区间的
    【hdu6547】Tree(树链剖分, 线段树区间根号)
    【hdu6547】Tree(树链剖分, 线段树区间根号)

2、线段树区间根号

  • 对于原来的两个数 a 和 b ( a>b ) ,开根后变成 √a 和 √b ,它们的差从 a−b 变成了 √a−√b
  • 考虑维护一个区间最大值MAX和区间最小值MIN,如果MAX-√MAX==MIN-√MIN, 即此时区间内所有数的开根后相差都相同,区间开根操作就可以转换成区间减法操作了

solution

//题意:给定一颗树,q次操作。每次可以询问树上任意两点之间最短路上点权和,或者将树上任意两点之间最短路上每个点权开根。
//思路:修改/查询 树上两点之间的路径上 所有点的值,树链剖分模板。区间开根可以用线段树维护。
#include<bits/stdc++.h>
using namespace std;
typedef long long LL;
const int maxn = 1e5+10;

//题目
int n, q;
LL a[maxn];//树上各节点的值
vector<int>G[maxn];

//树链剖分
int son[maxn], dad[maxn], siz[maxn], dep[maxn];
void dfs1(int x, int fa){//处理出重儿子,父亲,深度,子树大小
    son[x] = 0;  dad[x] = fa;
    siz[x] = 1;  dep[x] = dep[fa]+1;
	for(int to : G[x]){
		if(to==fa)continue;
		dfs1(to,x);
		siz[x] += siz[to];
		if(siz[to]>siz[son[x]])son[x]=to;
	}
}
int top[maxn], pos[maxn], tot=0;//所在链链顶,时间戳编号
LL v[maxn];//映射到序列后的值, 线段树依照此序列建树
void dfs2(int x, int fa, int k){//处理出链顶, 时间戳, 映射后的值
    top[x] = k;
    pos[x] = ++tot;
    v[tot] = a[x];
    if(son[x])dfs2(son[x], x, k);//重链往下去不用更新链顶
    for(int to : G[x]){
        if(to==fa)continue;
        if(to==son[x])continue;
        dfs2(to, x, to);//更新to为链顶
    }
}

//线段树
#define lch p<<1
#define rch p<<1|1
struct node{
    int l, r;
    LL mx, mi, sum, lazy;//维护区间加,区间max,min
}sgt[maxn<<2];
void pushdown(int p){
    sgt[lch].lazy += sgt[p].lazy;
    sgt[rch].lazy += sgt[p].lazy;
    sgt[lch].sum += (sgt[lch].r-sgt[lch].l+1)*sgt[p].lazy;
    sgt[rch].sum += (sgt[rch].r-sgt[rch].l+1)*sgt[p].lazy;
    sgt[lch].mx += sgt[p].lazy;
    sgt[lch].mi += sgt[p].lazy;
    sgt[rch].mx += sgt[p].lazy;
    sgt[rch].mi += sgt[p].lazy;
    sgt[p].lazy = 0;
}
void pushup(int p){
    sgt[p].sum = sgt[lch].sum+sgt[rch].sum;
    sgt[p].mx = max(sgt[lch].mx, sgt[rch].mx);
    sgt[p].mi = min(sgt[lch].mi, sgt[rch].mi);
}
void build(int p, int l, int r){
    sgt[p].l = l;  sgt[p].r = r;
    if(l==r){
        sgt[p].mx = sgt[p].mi = sgt[p].sum = v[l];
        return ;
    }
    int mid = l+r>>1;
    build(lch, l, mid);
    build(rch, mid+1, r);
    pushup(p);
}
LL querysum(int p, int l, int r){
    if(sgt[p].l==l && sgt[p].r==r)return sgt[p].sum;
    if(sgt[p].lazy)pushdown(p);
    int mid = (sgt[p].l+sgt[p].r)>>1;
    if(r<=mid)return querysum(lch, l, r);
    else if(l>mid)return querysum(rch, l, r);
    else return querysum(lch, l, mid)+querysum(rch, mid+1,r);
}
void update(int p, int l, int r){
    if(sgt[p].l==l && sgt[p].r==r){
        LL a = sqrt(sgt[p].mx), b = sqrt(sgt[p].mi);
        if(sgt[p].mx==sgt[p].mi || sgt[p].mx-a==sgt[p].mi-b){//开根后减少的值相同
            LL v= sgt[p].mx-a;//开根后会减少那么多
            sgt[p].sum -= (r-l+1)*v;
            sgt[p].mx -= v;
            sgt[p].mi -= v;
            sgt[p].lazy -= v;
            return ;
        }
    }
    if(sgt[p].lazy)pushdown(p);
    int mid = (sgt[p].l+sgt[p].r)>>1;
    if(r <= mid)update(lch, l, r);
    else if(l > mid)update(rch, l, r);
    else update(lch, l, mid), update(rch, mid+1, r);
    pushup(p);
}

//题目
LL calsum(int u, int v){//计算[u,v]路径和
    LL res = 0;
    while(top[u]!=top[v]){//不在同一条链上 每次链顶深度较大的点往上爬
        if(dep[top[u]]<dep[top[v]])swap(u,v);
        res += querysum(1,pos[top[u]], pos[u]); //sum[pos[top[u]], pos[u]]
        u = dad[top[u]]; //进入新的链
    }
    if(dep[u]>dep[v])swap(u,v);//进入同一条链上时再求一遍区间和 深度小的点在前面
    res += querysum(1, pos[u], pos[v]);
    return res;
}
void updatework(int u, int v){
    while(top[u]!=top[v]){
        if(dep[top[u]]<dep[top[v]])swap(u,v);
        update(1,pos[top[u]], pos[u]);
        u = dad[top[u]]; 
    }
    if(dep[u]>dep[v])swap(u,v);
    update(1,pos[u], pos[v]);
}

int main(){
    ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
    cin>>n>>q;
    for(int i = 1; i <= n; i++)cin>>a[i];
    for(int i = 1; i < n; i++){
        int u, v;  cin>>u>>v;
        G[u].push_back(v);
        G[v].push_back(u);
    }
    dfs1(1,0);
    dfs2(1,0,1);
    build(1,1,n);
    //for(int i = 1 ; i<= n; i++)cout<<top[i]<<" "; cout<<"\n"; return 0;
    for(int i = 1; i <= q; i++){
        int op, u, v;  cin>>op>>u>>v;
        if(op==0)updatework(u,v);
        else cout<<calsum(u,v)<<"\n";
    }
    return 0;
}
上一篇:android项目的文件介绍


下一篇:第一周学习日志