”树链剖分+线段树“详解

“在一棵树上进行路径的修改、求极值、求和”乍一看只要线段树就能轻松解决,实际上,仅凭线段树是不能搞定它的。我们需要用到一种貌似高级的复杂算法——树链剖分。

    树链,就是树上的路径。剖分,就是把路径分类为重链轻链
    记siz[v]表示以v为根的子树的节点数,dep[v]表示v的深度(根深度为1),top[v]表示v所在的链的顶端节点,fa[v]表示v的父亲,son[v]表示与v在同一重链上的v的儿子节点(姑且称为重儿子),w[v]表示v与其父亲节点的连边(姑且称为v的父边)在线段树中的位置。只要把这些东西求出来,就能用logn的时间完成原问题中的操作。

    重儿子:siz[u]为v的子节点中siz值最大的,那么u就是v的重儿子。
    轻儿子:v的其它子节点。
    重边:点v与其重儿子的连边。
    轻边:点v与其轻儿子的连边。
    重链:由重边连成的路径。
    轻链:轻边。

    剖分后的树有如下性质:
    性质1:如果(v,u)为轻边,则siz[u] * 2 < siz[v];
    性质2:从根到某一点的路径上轻链、重链的个数都不大于logn。
   

    算法实现:
    我们可以用两个dfs来求出fa、dep、siz、son、top、w。
    dfs_1:把fa、dep、siz、son求出来,比较简单,略过。
    dfs_2:⒈对于v,当son[v]存在(即v不是叶子节点)时,显然有top[son[v]] = top[v]。线段树中,v的重边应当在v的父边的后面,记w[son[v]] = totw+1,totw表示最后加入的一条边在线段树中的位置。此时,为了使一条重链各边在线段树中连续分布,应当进行dfs_2(son[v]);
           ⒉对于v的各个轻儿子u,显然有top[u] = u,并且w[u] = totw+1,进行dfs_2过程。
           这就求出了top和w。
    将树中各边的权值在线段树中更新,建链和建线段树的过程就完成了。

    修改操作:例如将u到v的路径上每条边的权值都加上某值x。
    一般人需要先求LCA,然后慢慢修改u、v到公共祖先的边。而高手就不需要了。
    记f1 = top[u],f2 = top[v]。
    当f1 <> f2时:不妨设dep[f1] >= dep[f2],那么就更新u到f1的父边的权值(logn),并使u = fa[f1]。
    当f1 = f2时:u与v在同一条重链上,若u与v不是同一点,就更新u到v路径上的边的权值(logn),否则修改完成;
    重复上述过程,直到修改完成。

    求和、求极值操作:类似修改操作,但是不更新边权,而是对其求和、求极值。

    就这样,原问题就解决了。鉴于鄙人语言表达能力有限,咱画图来看看:

 


    如右图所示,较粗的为重边,较细的为轻边。节点编号旁边有个红色点的表明该节点是其所在链的顶端节点。边旁的蓝色数字表示该边在线段树中的位置。图中1-4-9-13-14为一条重链。

    当要修改11到10的路径时。
    第一次迭代:u = 11,v = 10,f1 = 2,f2 = 10。此时dep[f1] < dep[f2],因此修改线段树中的5号点,v = 4, f2 = 1;
    第二次迭代:dep[f1] > dep[f2],修改线段树中10--11号点。u = 2,f1 = 2;
    第三次迭代:dep[f1] > dep[f2],修改线段树中9号点。u = 1,f1 = 1;

    第四次迭代:f1 = f2且u = v,修改结束。









SPOJ Problem Set (classical)

375. Query on a tree

Problem code: QTREE

You are given a tree (an acyclic undirected connected graph) with N nodes, and edges numbered 1, 2, 3...N-1.

We will ask you to perfrom some instructions of the following form:

  • CHANGE i ti : change the cost of the i-th edge to ti
    or
  • QUERY a b : ask for the maximum edge cost on the path from node a to node b

Input

The first line of input contains an integer t, the number of test cases (t <= 20). t test cases follow.

For each test case:

  • In the first line there is an integer N (N <= 10000),
  • In the next N-1 lines, the i-th line describes the i-th edge: a line with three integers a b c denotes an edge between ab of cost c (c <= 1000000),
  • The next lines contain instructions "CHANGE i ti" or "QUERY a b",
  • The end of each test case is signified by the string "DONE".

There is one blank line between successive tests.

Output

For each "QUERY" operation, write one integer representing its result.

Example

Input:
1

3
1 2 1
2 3 2
QUERY 1 2
CHANGE 1 3
QUERY 1 2
DONE

Output:
1
3


题意:一棵包含N 个结点的树,每条边都有一个权值,要求模拟两种操作:(1)改变某条边的权值,(2)询问U,V 之间的路径中权值最大的边。

思路:对树进行轻重边路径剖分。对于询问操作,我们可以分别处理两个点到其最近公共祖先的路径。路径可以分解成最多O(log N)条轻边和O(log N)条重路径,那么只需考虑如何维护这两种对象。对于轻边,我们直接处理即可。而对于重路径,我们只需用线段树来维护。

#include<stdio.h>
#include<string.h>
#include<algorithm>
#define N 10005
#define inf 0x3fffffff
using namespace std;
struct node
{
    int u,v,w,next;
}bian[N*2];
struct line
{
    int x,y,w;
}ed[N];
int e,id;
//********树链部分**************
//siz[u]u的子节点个数
//top[u]u所在链顶点
//father[u]表示u的父节点
//son[u]与u在同重链上的儿子节点
//ti[u]表示u与其父亲节点的连边,在线段树中的位置
int sz[N],dep[N],son[N],father[N],head[N],ti[N],top[N];
struct p
{
    int x,y,w;
}a[N*3];
int max(int a,int b)
{
    return a>b?a:b;
}
void add(int u,int v,int w)
{
    bian[e].u=u;
    bian[e].v=v;
    bian[e].w=w;
    bian[e].next=head[u];
    head[u]=e++;
}
void dfs1(int u,int fa)
{
    int i,v;
    sz[u]=1;  dep[u]=dep[fa]+1;  son[u]=0;  father[u]=fa;
    for(i=head[u];i!=-1;i=bian[i].next)
    {
        v=bian[i].v;
        if(v==fa)  continue;
        dfs1(v,u);
        sz[u]+=sz[v];
        if(sz[son[u]]<sz[v])
            son[u]=v;
    }
}
void dfs2(int u,int fa)
{
    int i,v;
    ti[u]=id++;
    top[u]=fa;
    if(son[u]!=0)
        dfs2(son[u],fa);//标记重边
    for(i=head[u];i!=-1;i=bian[i].next)
    {//标记轻边
        v=bian[i].v;
        if(v==father[u]||v==son[u])
            continue;
        dfs2(v,v);//该链的顶点就是改点
    }
}
void build(int t,int x,int y)
{
    a[t].x=x; a[t].y=y; a[t].w=-inf;
    if(x==y)
       return;
    int mid=(x+y)>>1,temp=t<<1;
    build(temp,x,mid);
    build(temp+1,mid+1,y);
}
void update(int t ,int x,int w)
{
    if(a[t].x==x&&a[t].y==x)
    {
        a[t].w=w;
        return ;
    }
    int mid=(a[t].x+a[t].y)>>1,temp=t<<1;
    if(x<=mid)
        update(temp,x,w);
    else
        update(temp+1,x,w);
    a[t].w=max(a[temp].w,a[temp+1].w);
}
int query(int x,int y,int t)
{
    if(a[t].x==x&&a[t].y==y)
        return a[t].w;
    int mid=(a[t].x+a[t].y)>>1,temp=t<<1;
    if(y<=mid)
        return query(x,y,temp);
    else if(x>mid)
        return query(x,y,temp+1);
    else
        return max(query(x,mid,temp),query(mid+1,y,temp+1));
}
int lca(int x,int y)
{
    int ans=-inf;
    while(top[x]!=top[y])
    {
        if(dep[top[x]]<dep[top[y]])
            swap(x,y);
        //x到所在链的顶点所有边的最大值
        ans=max(ans,query(ti[top[x]],ti[x],1));
        x=father[top[x]];//x所在链的顶点的父节点,转到另一条链上
    }
    if(dep[x]>dep[y])
        swap(x,y);
    if(x!=y)//ti[x]指的是x与其父亲的边,所以+1
        ans=max(ans,query(ti[x]+1,ti[y],1));
    return ans;
}
int main()
{
    int t,i,n,x,y;
    char str[100];
    scanf("%d",&t);
    while(t--)
    {
        scanf("%d",&n);
        e=0;
        memset(head,-1,sizeof(head));
        for(i=1;i<n;i++)
        {
            scanf("%d%d%d",&ed[i].x,&ed[i].y,&ed[i].w);
            add(ed[i].x,ed[i].y,ed[i].w);
            add(ed[i].y,ed[i].x,ed[i].w);
        }
        dep[1]=0;  id=1; sz[0]=0;
        dfs1(1,1);
        dfs2(1,1);
        //for(i=1;i<=n;i++)
            //printf("i=%d sz=%d top=%d father=%d son=%d ti=%d\n",i,sz[i],top[i],father[i],son[i],ti[i]);
        build(1,2,n);
        for(i=1;i<n;i++)
        {
            if(dep[ed[i].x]<dep[ed[i].y])
                swap(ed[i].x,ed[i].y);
            update(1,ti[ed[i].x],ed[i].w);
        }
        while(scanf("%s",str),strcmp(str,"DONE")!=0)
        {
           if(str[0]=='Q')
           {
               scanf("%d%d",&x,&y);
               printf("%d\n",lca(x,y));
           }
           else
           {
               scanf("%d%d",&x,&y);
               update(1,ti[ed[x].x],y);
           }
        }
        printf("\n");
    }
    return 0;
}


评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值