zoukankan      html  css  js  c++  java
  • 与图论的邂逅02:树链剖分

      其实本人学树剖时还完全没到学树剖的水平,但本着在图论这一块的兴趣就看了看,结果发现树剖还挺简单?

      树剖是什么呢?看看它的全称:“树链剖分”。顾名思义,就是把一棵树解剖成一个个链子咯。然后在每条链上维护需要的信息,比如最小值、最大值、权值和什么的。这棵树的红线就是剖出来的链条:特别地,一个点也算一条链.

      但是问题来了,假如树上有两个点,我怎么知道他们俩是不是在同一条链子上呢?所以我们还需要用一个东西来给链子上的所有节点染色,这样就可以回答刚才的问题了。怎么染呢?我们想一个问题,如果我要查树上两个点之间节点的权值的最大值,怎么查?这里就有两种情况:

        1.这两个点在同一条链上,这样的话就可以直接查出链上两点的区间中的最小值了。

        2.这两个点不在同一条链上。我们就可以让两个点不断往上跳,直到跳到他们属于同一条链为止。而树剖的关键之一就是这个跳法。如果朴(bao)素(li)一点,怎么跳?那当然是一个点一个点地往上跳,那么时间复杂度就是O(N),对于m次查询就是O(NM)。那我还要树剖有个屁用。。。所以要换一种优雅点的。现在我们不是把树都剖成链了吗?为什么不一条链一条链地跳呢?我们可以首先记录下当前节点u所在的链的顶端top[u],若要跳出这条链,就让u=fa[top[u]](不是top[u])。就像这样不断地跳,直到两个点的top相同为止。

      以上就是树剖的基本思路。那么我们为什么要树剖这种东西呢?“有时可以用来暴力。”——学校一个大佬。树剖确实是暴力,只不过非常地优雅。如果一道题需要查询树上两点的区间中的某些信息,我们可以直接查这个区间,而不是枚举区间的每个点。所以我们需要记录下树上的每一个区间,用线段树来维护,而这些区间就是上面所说的链条了。这样一来,每次查询的复杂度就降为O(logN)了。当然这是最理想的情况。而为了让我们的树剖的复杂度尽量接近这个等级,我们就需要想办法把树剖成logN条链子。所以这里引入一个概念:重链剖分。

      所谓重链,给人的感觉就是这条链特别重。也就是这条链的节点数特别多了。而重链剖分的过程就是:对于当前节点u,找出以它的子节点为根的子树中节点最多的一棵子树,假如这颗子树的根为v,那么就给这条链加上节点v,并递归下去,对v也如法炮制。这样构造出的链就很接近logN个。而u的这个子节点v有个定义,叫做u的“重儿子”。

      那么,怎么让一条链所表示的区间内的所有节点都是连续的整数呢?想想刚才的操作,我们在建链时是沿着链递归下去的,如果我们这时记下每个节点的时间戳的话,是不是这条链上的时间戳就是一串连续的整数了呢?这样就解决了刚才的问题。所以,我们在剖树时,需要两次DFS,第一次找出每个节点的重儿子,第二次沿着重儿子走下去,记下每个点被遍历到的时间戳:

    void dfs_getson(int u){//第一次DFS
        size[u] = 1;
        for(int i = head[u]; ~i; i = e[i].next){
            int v = e[i].to;
            if(v == fa[u]) continue;//注意不能往回走
            fa[v] = u;
            dep[v] = dep[u] + 1;//计算深度,等会要讲.
            dfs_getson(v);
            size[u] += size[v];
            if(size[v] > size[son[u]]) son[u] = v;//记录u的重儿子.
        }
    }
    void dfs_rewrite(int u, int tp){//第二次DFS
        top[u] = tp, dfn[u] = ++tot, id[tot] = u;//id为dfn的反函数,由于链上的区间由dfn组成,为了知道链上某个点的编号我们记录下id.top为u所在链的顶端
        if(son[u]) dfs_rewrite(son[u], tp);//优先往重儿子走.
        for(int i = head[u]; ~i; i = e[i].next){
            int v = e[i].to;
            if(v != fa[u] && v != son[u]) dfs_rewrite(v, v);//往不是重儿子的儿子(轻儿子)走
        }
    }

      像这样,一棵树就被我们剖成了链。不过这些链现在还没有什么用,我们需要用线段树来维护它们。所以首先是构造。这里用区间最大值为例,其它的类似:

    void build(int d, int l, int r){
        t[d].l = l, t[d].r = r;
        if(l == r){ t[d].mmax = val[id[l]]; return; }//要注意这里的lr都是dfn值
        int mid = l + r >> 1;
        build(d << 1, l, mid);//构造左儿子
        build(d << 1 | 1, mid + 1, r);//构造右儿子
        t[d].mmax = max(t[d << 1].mmax, t[d << 1 | 1].mmax);
    }
    //main函数中
        build(1, 1, tot); 

      然后是单点查询:

    int getmax_vertex(int d, int x){
        if(t[d].l == t[d].r) return t[d].mmax;
        int mid = t[d].l + t[d].r >> 1;
        if(x <= mid) return getmax_vertex(d << 1, x);
        else return getmax_vertex(d << 1 | 1, x);
    }
    //main函数中
        ans = getmax_vertex(1, dfn[u]); 

      单点修改:

    void change_vertex(int d, int x, int w){
        if(t[d].l == t[d].r){ t[d].mmax = w; return; }
        int mid = t[d].l + t[d].r >> 1;
        if(x <= mid) change_vertex(d << 1, x, w);
        else change_vertex(d << 1 | 1, x, w);
        t[d].mmax = max(t[d << 1].mmax, t[d << 1 | 1].mmax); 
    }
    //main函数中
        change_vertex(1, dfn[u], w); 

      需要注意的是对区间的操作。其实上面已经提到了,若区间的两个端点u,v在同一条链上,那么直接操作就可以了。而不在一条链上时,只需要跳到同一条链上即可。那么谁跳呢?假如我们让top深度小的点跳,深度只能越跳越小,永远小于另一个点,直到跳到根节点也什么也做不了(可以画个图来理解)。所以我们应该选择top深度大的开始跳,边跳边对跳过的链操作,最后跳到同一条链时再操作一次。

      于是就有了区间查询(延迟标记就懒得写了):

    int getmax_xtoy(int d, int l, int r){
        if(l <= t[d].l && t[d].r <= r) return t[d].mmax;
        if(t[d].f) down(d);
        int mid = t[d].l + t[d].r >> 1, ans = 0;
        if(l <= mid) ans = max(ans, getmax_xtoy(d << 1, l, r));
        if(r > mid) ans = max(ans, getmax_xtoy(d << 1 | 1, l, r));
        return ans;
    }
    //main函数中
        int ans = 0;
        while(top[u] != top[v]){
            if(dep[top[u]] > dep[top[v]]) swap(u, v);//从深度大的开始跳
            ans = max(ans, getmax_xtoy(1, dfn[top[v]], dfn[v]));
            v = fa[top[v]];//注意要调到顶端的父亲去
        }
        if(dep[u] > dep[v]) swap(u, v);
        ans = max(ans, getmax_xtoy(1, dfn[u], dfn[v]));

      以及区间修改:

    void change_xtoy(int d, int l, int r, int w){
        if(l <= t[d].l && t[d].r <= r){ t[d].mmax = w; t[d].f = w; return; }
        if(t[d].f) down(d);
        int mid = t[d].l + t[d].r >> 1;
        if(l <= mid) change_xtoy(d << 1, l, r, w);
        if(r > mid) change_xtoy(d << 1 | 1, l, r, w);
        t[d].mmax = max(t[d << 1].mmax, t[d << 1 | 1].mmax);
    }
    //main函数中
        while(top[u] != top[v]){
            if(dep[top[u]] > dep[top[v]]) swap(u, v);
            change_xtoy(1, dfn[top[v]], dfn[v], w);
            v = fa[top[v]];
        } 
        if(dep[u] > dep[v]) swap(u, v);
        change_xtoy(1, dfn[u], dfn[v]);

      没错,树链剖分就是个码量惊人的东西。


      现在我们可以愉快地刷例题了。

    P3384 【模板】树链剖分

    题目描述

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

    操作1: 格式: 1 x y z 表示将树从x到y结点最短路径上所有节点的值都加上z

    操作2: 格式: 2 x y 表示求树从x到y结点最短路径上所有节点的值之和

    操作3: 格式: 3 x z 表示将以x为根节点的子树内所有节点值都加上z

    操作4: 格式: 4 x 表示求以x为根节点的子树内所有节点值之和

    输入输出格式

    输入格式:

    第一行包含4个正整数N、M、R、P,分别表示树的结点个数、操作个数、根节点序号和取模数(即所有的输出结果均对此取模)。

    接下来一行包含N个非负整数,分别依次表示各个节点上初始的数值。

    接下来N-1行每行包含两个整数x、y,表示点x和点y之间连有一条边(保证无环且连通)

    接下来M行每行包含若干个正整数,每行表示一个操作,格式如下:

    操作1: 1 x y z

    操作2: 2 x y

    操作3: 3 x z

    操作4: 4 x

    输出格式:

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

      这不是模板题。这题涉及了区间修改、区间查询,以及——子树修改和查询???这就是这题的难点了。子树并不只是简单的一条链,这怎么办呢?实际上子树的东西还简单地可怜。想想,我们在对区间操作时的对象等同于一些连续的dfn值,而子树呢?子树的所有点的dfn值不也是连续的一串吗?所以我们在第二次搜索时不仅记录下节点x的dfn值,也记下以x为根的子树的dfn最大值:

    void dfs_rewrite(int u, int tp){
        top[u] = tp, dfn[u] = ++tot, id[tot] = u;
        if(son[u]) dfs_rewrite(son[u], tp);
        for(int i = head[u]; ~i; i = e[i].next){
            int v = e[i].to;
            if(v != fa[u] && v != son[u]) dfs_rewrite(v, v); 
        }
        cnt[v] = tot;//递归完子树后的tot就是dfn最大值 
    }

      而对子树的操作也就可以写得出来了:

    inline void getsum_sontree(){//查询子树和 
        int u;
        scanf("%d", &u);
        ans = getsum(1, dfn[u], cnt[u]);
    }
    inline void change_sontree(){//修改子树值
        int u, w;
        scanf("%d %d", &u, &w);
        change(1, dfn[u], cnt[u], w);
    }

      最后放上代码(我也不知道用不用long long):

    #include <string.h>
    #include <stdio.h>
    #define maxn 500010
    #define maxm 500010
    
    struct graph{
        struct edge{
            long long to, next;
            edge(){}
            edge(const long long &_to, const long long &_next){
                to = _to;
                next = _next;
            }
        }e[maxn << 1];
        long long head[maxn], k;
        inline void init(){
            memset(head, -1, sizeof head);
            k = 0;
        }
        inline void add(const long long &u, const long long &v){
            e[k] = edge(v, head[u]);
            head[u] = k++;
        }
    }g;
    
    struct node{
        long long l, r, zuo, you;
        long long c, f;
    }t[maxn << 2];
    
    long long fa[maxn], dep[maxn], size[maxn], son[maxn];
    long long dfn[maxn], id[maxn], top[maxn], cnt[maxn], tot;
    long long n, m, root, p, val[maxn], len;
    
    inline void swap(long long &x, long long &y){long long t = x; x = y; y = t;}
    
    void dfs_getson(long long u){
        size[u] = 1;
        for(long long i = g.head[u]; ~i; i = g.e[i].next){
            long long v = g.e[i].to;
            if(v == fa[u]) continue;
            fa[v] = u;
            dep[v] = dep[u] + 1;
            dfs_getson(v);
            size[u] += size[v];
            if(size[v] > size[son[u]]) son[u] = v;
        }
    }
    
    void dfs_rewrite(long long u, long long tp){
        top[u] = tp;
        dfn[u] = ++tot;
        id[tot] = u;
        if(son[u]) dfs_rewrite(son[u], tp);
        for(long long i = g.head[u]; ~i; i = g.e[i].next){
            long long v = g.e[i].to;
            if(v != son[u] && v != fa[u]) dfs_rewrite(v, v);
        }
        cnt[u] = tot;
    }
    
    void build(long long d, long long l, long long r){
        t[d].l = l, t[d].r = r;
        if(l == r){
            t[d].c = val[id[l]];
            return;
        }
        long long mid = l + r >> 1;
        build(d << 1, l, mid);
        build(d << 1 | 1, mid + 1, r);
        t[d].c = t[d << 1].c + t[d << 1 | 1].c;
    }
    
    inline void down(long long d){
        t[d << 1].f += t[d].f;
        t[d << 1 | 1].f += t[d].f;
        t[d << 1].c += t[d].f * (t[d << 1].r - t[d << 1].l + 1);
        t[d << 1 | 1].c += t[d].f * (t[d << 1 | 1].r - t[d << 1 | 1].l + 1);
        t[d].f = 0;
    }
    
    void change(long long d, long long l, long long r, long long x){
        if(l <= t[d].l && t[d].r <= r){
            t[d].c += (t[d].r - t[d].l + 1) * x;
            t[d].f += x;
            return;
        }
        if(t[d].f) down(d);
        long long mid = t[d].l + t[d].r >> 1;
        if(l <= mid) change(d << 1, l, r, x);
        if(r > mid) change(d << 1 | 1, l, r, x);
        t[d].c = t[d << 1].c + t[d << 1 | 1].c;
    }
    
    long long getsum(long long d, long long l, long long r){
        if(l <= t[d].l && t[d].r <= r) return t[d].c;
        if(t[d].f) down(d);
        long long mid = t[d].l + t[d].r >> 1;
        long long ans = 0;
        if(l <= mid) ans =  (ans + getsum(d << 1, l, r)) % p;
        if(r > mid) ans = (ans + getsum(d << 1 | 1, l, r)) % p;
        return ans;
    }
    
    inline void change_xtoy(){
        long long x, y, z;
        scanf("%lld%lld%lld",&x, &y, &z);
        while(top[x] != top[y])
        {
            if(dep[top[x]] > dep[top[y]]) swap(x, y);
            change(1, dfn[top[y]], dfn[y], z);
            y = fa[top[y]];
        }
        if(dep[x] > dep[y]) swap(x, y);
        change(1, dfn[x], dfn[y], z); 
    }
    
    inline void getson_xtoy(){
        long long x, y;
        scanf("%lld%lld", &x, &y);
        long long ans = 0;
        while(top[x] != top[y]){
            if(dep[top[x]] > dep[top[y]]) swap(x, y);
            ans = (ans + getsum(1, dfn[top[y]], dfn[y])) % p;
            y = fa[top[y]];
        }
        if(dep[x] > dep[y]) swap(x, y);
        ans += getsum(1, dfn[x], dfn[y]);
        printf("%lld\n", ans % p);
    }
    
    inline void change_sontree(){
        long long x, y;
        scanf("%lld%lld", &x, &y);
        change(1, dfn[x], cnt[x], y);
    }
    
    inline void getsum_sontree(){
        long long x;
        scanf("%lld", &x);
        printf("%lld\n", getsum(1, dfn[x], cnt[x]) % p);
    }
    
    int main(){
        g.init();
        scanf("%lld%lld%lld%lld", &n, &m, &root, &p);
        for(long long i = 1; i <= n; i++) scanf("%lld", &val[i]);
        for(long long i = 1; i < n; i++){
            long long u, v;
            scanf("%lld%lld", &u, &v);
            g.add(u, v);
            g.add(v, u);
        }
        dfs_getson(root);
        dfs_rewrite(root, root);
        build(1, 1, tot);
        for(long long i = 1; i <= m; i++){
            long long op;
            scanf("%lld", &op);
            if(op == 1) change_xtoy();
            if(op == 2) getson_xtoy();
            if(op == 3) change_sontree();
            if(op == 4) getsum_sontree();
        }
        return 0;
    }

      注释应该就不需要了吧,自我感觉代码还是能被看懂的(溜~)


    P2146 [NOI2015]软件包管理器

    题目描述

    Linux用户和OSX用户一定对软件包管理器不会陌生。通过软件包管理器,你可以通过一行命令安装某一个软件包,然后软件包管理器会帮助你从软件源下载软件包,同时自动解决所有的依赖(即下载安装这个软件包的安装所依赖的其它软件包),完成所有的配置。Debian/Ubuntu使用的apt-get,Fedora/CentOS使用的yum,以及OSX下可用的homebrew都是优秀的软件包管理器。

    你决定设计你自己的软件包管理器。不可避免地,你要解决软件包之间的依赖问题。如果软件包A依赖软件包B,那么安装软件包A以前,必须先安装软件包B。同时,如果想要卸载软件包B,则必须卸载软件包A。现在你已经获得了所有的软件包之间的依赖关系。而且,由于你之前的工作,除0号软件包以外,在你的管理器当中的软件包都会依赖一个且仅一个软件包,而0号软件包不依赖任何一个软件包。依赖关系不存在环(若有m(m≥2)个软件包A1,A2,A3,⋯,Am,其中A1依赖A2,A2依赖A3,A3依赖A4,……,A[m-1]依赖Am,而Am依赖A1,则称这m个软件包的依赖关系构成环),当然也不会有一个软件包依赖自己。

    现在你要为你的软件包管理器写一个依赖解决程序。根据反馈,用户希望在安装和卸载某个软件包时,快速地知道这个操作实际上会改变多少个软件包的安装状态(即安装操作会安装多少个未安装的软件包,或卸载操作会卸载多少个已安装的软件包),你的任务就是实现这个部分。注意,安装一个已安装的软件包,或卸载一个未安装的软件包,都不会改变任何软件包的安装状态,即在此情况下,改变安装状态的软件包数为0。

    输入输出格式

    输入格式:

     

    从文件manager.in中读入数据。

    输入文件的第1行包含1个整数n,表示软件包的总数。软件包从0开始编号。

    随后一行包含n−1个整数,相邻整数之间用单个空格隔开,分别表示1,2,3,⋯,n−2,n−1号软件包依赖的软件包的编号。

    接下来一行包含1个整数q,表示询问的总数。之后q行,每行1个询问。询问分为两种:

    install x:表示安装软件包x

    uninstall x:表示卸载软件包x

    你需要维护每个软件包的安装状态,一开始所有的软件包都处于未安装状态。

    对于每个操作,你需要输出这步操作会改变多少个软件包的安装状态,随后应用这个操作(即改变你维护的安装状态)。

     

    输出格式:

     

    输出到文件manager.out中。

    输出文件包括q行。

    输出文件的第i行输出1个整数,为第i步操作中改变安装状态的软件包数。

      这题有点像上面那道模板题。这题其实已经暗示我们很多了。首先,0号软件包不依赖任何软件包——这不就是说0为根节点吗?其次,依赖关系——这不就是让我们把u向着依赖它的v连一条边吗?于是我们就建好了一颗树。如果我们要安装软件包A,由题意得,我们需要将u到根节点路上所有为安装的软件包都安装上;而删除u呢?又由题意得,我们需要删掉所有依赖u的软件包,而依赖那些软件包的软件包也得被删掉。这不就是上一题写过的对子树的操作吗?所以我们只需要再用线段树维护一下区间上已安装的软件包个数,这个题就也被切掉了。

    #include <iostream>
    #include <cstring>
    #include <cstdio>
    #define maxn 100010
    #define maxm 100010
    using namespace std;
    
    struct edge{
        int to, next;
        edge(){}
        edge(const int &_to, const int &_next){
            to = _to;
            next = _next;
        }
    }e[maxn << 1];
    int head[maxn], k;
    
    struct node{
        int l, r, c, f;
    }t[maxn << 2];
    
    int size[maxn], fa[maxn], dep[maxn], son[maxn];
    int dfn[maxn], id[maxn], top[maxn], cnt[maxn], tot;
    int n, m;
    
    inline void add(const int &u, const int &v){
        e[k] = edge(v, head[u]);
        head[u] = k++;
    }
    
    void dfs_getson(int u){
        size[u] = 1;
        for(int i = head[u]; ~i; i = e[i].next){
            int v = e[i].to;
            if(v == fa[u]) continue;
            fa[v] = u, dep[v] = dep[u] + 1;
            dfs_getson(v);
            size[u] += size[v];
            if(size[v] > size[son[u]]) son[u] = v;
        }
    }
    
    inline void dfs_rewrite(int u, int tp){
        top[u] = tp, dfn[u] = ++tot, id[tot] = u;
        if(son[u]) dfs_rewrite(son[u], tp);
        for(int i = head[u]; ~i; i = e[i].next){
            int v = e[i].to;
            if(v != fa[u] && v != son[u]) dfs_rewrite(v, v);
        }
        cnt[u] = tot;
    }
    
    void build(int d, int l, int r){
        t[d].l = l, t[d].r = r;
        if(l == r) return;
        int mid = l + r >> 1;
        build(d << 1, l, mid), build(d << 1 | 1, mid + 1, r);
    }
    
    inline void down(int d){
        if(t[d].f == 2){
            t[d << 1].c = t[d << 1].r - t[d << 1].l + 1;
            t[d << 1 | 1].c = t[d << 1 | 1].r - t[d << 1 | 1].l + 1;
            t[d << 1].f = t[d << 1 | 1].f = t[d].f;
        }else if(t[d].f == 1){
            t[d << 1].c = t[d << 1 | 1].c = 0;
            t[d << 1].f = t[d << 1 | 1].f = t[d].f;
        }
        t[d].f = 0;
    }
    
    int change(int d, const int &l, const int &r, const int &op){
        if(l <= t[d].l && t[d].r <= r){
            int ans = t[d].c;
            if(op == 2) t[d].c = t[d].r - t[d].l + 1;
            else t[d].c = 0;
            t[d].f = op;
            return ans;
        }
        if(op) down(d);
        int mid = t[d].l + t[d].r >> 1, ans = 0;
        if(l <= mid) ans += change(d << 1, l, r, op);
        if(r > mid) ans += change(d << 1 | 1, l, r, op);
        t[d].c = t[d << 1].c + t[d << 1 | 1].c;
        return ans;
    }
    
    inline void change_path();
    inline void change_sontree();
    
    int main(){
        memset(head, -1, sizeof head);
        scanf("%d", &n);
        for(int i = 2; i <= n; i++){
            int v;
            scanf("%d", &v);v++;
            add(i, v), add(v, i);
        }
        dfs_getson(1);
        dfs_rewrite(1, 1);
        build(1, 1, tot);
        
        scanf("%d", &m);
        while(m--){
            string op;
            cin >> op;
            if(op == "install") change_path();
            else change_sontree();
        }
        return 0;
    }
    
    inline void change_path(){
        int u, ans;
        scanf("%d", &u);u++;
        ans = dep[u] - dep[1] + 1;
        while(top[u] != 1){
            ans -= change(1, dfn[top[u]], dfn[u], 2);//被删掉的个数就等于节点总数减被安装了的软件包个数
            u = fa[top[u]];
        }
        ans -= change(1, 1, dfn[u], 2);
        printf("%d\n", ans);
    }
    
    inline void change_sontree(){
        int u, ans;
        scanf("%d", &u);u++;
        ans = change(1, dfn[u], cnt[u], 1);
        printf("%d\n", ans);
    }

      这里的节点编号要加1。注意这不是习惯!为什么呢?首先,我们的son数组一开始都是0,若不给节点编号加1的话,第二次DFS时就可能出错。


    P2486 [SDOI2011]染色

      这是一道很好的树剖题(至少不是模板)。很直接地,我们会想到用线段树记下区间内的颜色段的数量。于是我们递归下去,如果到了所要改的区间,我们就把区间的颜色段数量改成1就好了。但回溯时,若左儿子的右端点颜色和右儿子的左端点颜色一样,怎么判断呢?于是我们需要再记下两个信息:区间左端点的颜色和右端点的颜色。若左儿子的右端点颜色和右儿子的左端点颜色一样,将两个儿子的颜色段数加起来后再减1即

    可。就是代码有点难调,我的那个BUG代码还没调好(果然太弱了)。


    P2590 [ZJOI2008]树的统计

    题目描述

    一棵树上有n个节点,编号分别为1到n,每个节点都有一个权值w。

    我们将以下面的形式来要求你对这棵树完成一些操作:

    I. CHANGE u t : 把结点u的权值改为t

    II. QMAX u v: 询问从点u到点v的路径上的节点的最大权值

    III. QSUM u v: 询问从点u到点v的路径上的节点的权值和

    注意:从点u到点v的路径上的节点包括u和v本身

    输入输出格式

    输入格式:

    输入文件的第一行为一个整数n,表示节点的个数。

    接下来n – 1行,每行2个整数a和b,表示节点a和节点b之间有一条边相连。

    接下来一行n个整数,第i个整数wi表示节点i的权值。

    接下来1行,为一个整数q,表示操作的总数。

    接下来q行,每行一个操作,以“CHANGE u t”或者“QMAX u v”或者“QSUM u v”的形式给出。

    输出格式:

    对于每个“QMAX”或者“QSUM”的操作,每行输出一个整数表示要求输出的结果。

      真正的模板题。如果你觉得你写树剖还不熟练,可以拿这题练(shui)练(jing)手(yan)。话说树剖的代码真的难调。


    P4114 Qtree1

    题目描述

    给定一棵n个节点的树,有两个操作:

    • CHANGE i ti 把第i条边的边权变成ti

    • QUERY a b 输出从a到b的路径中最大的边权,当a=b的时候,输出0

    输入输出格式

    输入格式:

    第一行输入一个n,表示节点个数

    第二行到第n行每行输入三个数,ui,vi,wi,分别表示 ui,vi有一条边,边权是wi

    第n+1行开始,一共有不定数量行,每一行分别有以下三种可能

    CHANGE,QUERY同题意所述

    DONE表示输入结束

    输出格式:

    对于每个QUERY操作,输出一个数,表示a b之间边权最大值

      这道题的技巧在很多树剖题都会用到~我们树剖时,计算的都是点权对不对?可是这题就不同些,给的是边权。这时怎么办呢?就用到了一种技巧,叫.......我也不知道叫什么。就是把边权转化为点权。怎么转呢?假如有边(u,v),如果我们把边权存在深度大的那个节点上去,也就是在节点x存下x连向父亲节点的边的权值。这样有什么用呢?想想,除了根节点,每个节点都唯一地只有一个父亲对吧,所以这样存不就把n-1条边的权值存在n-1个点上了吗?而那个没存的就是根节点了。于是,计算边权的题就可以转化为计算点权的题了。不过这样做还需要注意一个事项,由于每个点存的都是连向父亲边的边权,所以我们在计算树上两点之间的信息时,这两点的lca是不可用的(我不会告诉你我因为这个写爆过)



      例题就讲到这里吧。。。

      其实树剖还可以做lca。现有树上两点u,v,若求他们的lca,就树剖后一直跳链,直到top相同为止。此时深度小的那个就是lca了~也就是说现在我们有三种求lca的方法了:树上倍增、Tarjan和树剖(还有向上标记)。我们来对比一下。其实三种算法的思路都是一样,一直往上跳即可。而树上倍增时跳的是2的k次方,虽然很大,但还是没有树剖跳一跳链跳得彻底。而树剖跳的时候,如果链多了,还是会费时的。但Tarjan就异常强大了,在遍历图的过程中遇到了可以回答的询问就能得出答案,你可以理解为现在遍历到了u,而询问里有求lca(u,v)并且v已经遍历过,此时lca就是v在并查集的生成树里的祖先,也就是直接把u,v直接拽到了lca的位置去,跳都不跳了。但毕竟是离线算法,费空间......再对比一下时间复杂度,树上倍增为O((N+M)logN),树剖为(N+MlogN),Tarjan为O(N+M),Tarjan最优。而空间复杂度呢?树上倍增法为O(NlogN),树剖为O(N),Tarjan为O(N+M)(询问与并查集),树剖最优。综上,个人认为树剖还是最好的求lca的算法(虽然有点小题大做并且暴力得一批)。所以你们可以尝试做一下lca的题:

    P3379 【模板】最近公共祖先(LCA)

  • 相关阅读:
    Exception handling 异常处理的本质
    一个人运气不好怎么办?做什么事能够马上改变运气?
    autoreleasing on a thread
    Tagged Pointer
    Objective-C 引用计数原理
    oc引用计数原理-引用计数相关变化
    黑箱中的 retain 和 release
    黑幕背后的Autorelease
    自动释放池的前世今生 ---- 深入解析 autoreleasepool
    Exceptions and Errors on iOS
  • 原文地址:https://www.cnblogs.com/akura/p/10692600.html
Copyright © 2011-2022 走看看