POJ 3321 Apple Tree 树状数组 树到数组的映射 区间求和

        我觉得是一道很不错的题目了,自己写出来的感觉真是不错。。。(虽然是看了别人的题解),总之写完之后我又回过头思考了一下,发现大概的思考路径可能是这样:

        这道题目的重点就是在于对于操作Q的处理上,也就是求和。一个很朴素的想法就是遍历子树,可是当需要大量操作的时候,这样的是不行的。然后就想到利用树状数组进行区间求和,可是题目并没有说明这棵是一个二叉树,而且也不能保证每一棵子树的编号最终都落在同一个区间里面,那么我们这时候就需要对这棵树进行重新处理,也就是重新编号,使得它满足我们需要的这一个性质,也就是在求解一棵子树上的苹果的数量时候,它的子树的号码都能够落在一个连续区间里面,同时,保存下这棵子树的子树的编号范围,要求查询的时候可以直接用树状数组来求和。

        这应该就是这道题目的一个大致的想法。至于还有别的题解说需要手动开栈,我自测了一下,在我的这种用数组模拟邻接表的建图方式下,是不需要手动开栈的。

       Tips:通过DFS来给每一个子树保存它的子树的范围,vis数组用在DFS里面表示某一个节点是否被访问过

                 first,sz,init,addeage 以及edge数组都是用来模拟邻接表建图的。

                 lb,query,update用于树状数组的操作,Apple数组用来表示这棵树上是不是有苹果。

                 low数组和high数组用来表示一个子树的子树的范围,也就是保存其子树的最小编号和最大编号。


还是先把AC的代码拿出来吧,我会选择中间的DFS进行说明的,毕竟我感觉这个题目的关键就在于此

#include<cstdio>
#include<cstring>
#include<algorithm>

using namespace std;

struct Node{
    int v,next;
    Node(int a=0,int b=0):v(a),next(b){};
}edge[100005];

inline int lb(int x){return x&-x;}

int n,m,first[100005],dep,x,y,low[100005],high[100005],num[100005],sz,apple[100005],query(int x);
bool vis[100005];
char c;
void addedge(),init(),update(int x,int val),dfs(int x);

int main(){
    while(scanf("%d",&n)!=EOF){
        init();
        for(int i=1;i<n;++i)
            scanf("%d%d",&x,&y),addedge();

        dfs(1);
        scanf("%d%*c",&m);
        for(int i=0;i<m;++i){
            scanf("%c%d%*c",&c,&x);
            if(c=='C')
                (apple[x]==1)?(update(low[x],-1),apple[x]=0):(update(low[x],1),apple[x]=1);
            else
                printf("%d\n",query(high[x])-query(low[x]-1));
        }
    }
    return 0;
}

void update(int x,int val){
    while(x<=n)num[x]+=val,x+=lb(x);
}

int query(int x){
    int sum=0;
    while(x>0)sum+=num[x],x-=lb(x);
    return sum;
}

void init(){
    sz=dep=0;
    memset(vis,0,sizeof(bool)*(n+1));
    memset(first,-1,sizeof(int)*(n+1));
    memset(num,0,sizeof(int)*(n+1));
    for(int i=1;i<=n;++i){
        apple[i]=1;
        update(i,1);  //一开始每个分支都有一个苹果
    }
}


void addedge(){
    edge[sz]=Node(y,first[x]);
    first[x]=sz++;
}

void dfs(int x){
    low[x]=++dep;
    vis[x]=true;
    for(int i=first[x];i!=-1;i=edge[i].next)
    if(!vis[edge[i].v])
        dfs(edge[i].v);
    high[x]=dep;
}


好了,接下来是对DFS的说明:
                     首先这题目本身就是一棵树,我们只需要对其重新编号就可以很轻松的求出它的某一个子树上的苹果数量,关键就在于编号以及确定其子树的区间范围。而关于这个编号,算法导论上说的很好,这儿采用的写法是类似于算导上的DFS的写法,不过也仅仅是省略了节点的颜色以及减少了时间戳的增加速度而已,对比之后可以发现,我省略掉了一行使得时间戳+1的语句。
                     同样是在那一章里面,有提过一个“括号化定理”,我这里写出来(仅仅针对这个题目):对于任意两个节点u和v来说下面三种情况仅有一种成立:
                                  ①区间【low[ u ] , high [ u ] 】 和 【 low[ v ] , high [ u ] 】完全分离,此时意味着u不是v的后代,反之亦然。
                                  ②区间【low[ u ] , high [ u ] 】 完全包含在 【 low[ v ] , high [ u ] 】内,此时u是v的后代。
                                  ③区间【low[ u ] , high [ u ] 】 完全包含 【 low[ v ] , high [ u ] 】,此时v是u的后代。

                     在此不给予证明。
                     当时知道了这三点然后结合着DFS来看的话,就会发现,我们的目的已经达成了,一棵子树K的子树的编号都落在了【low[ K ] ,high[ K ]】里面。然后就是树状数组求和的事情了。


  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值