POJ-3321-Apple Tree,线段树,树状数组。

来源:互联网 发布:淘宝搜索什么有惊喜 编辑:程序博客网 时间:2024/05/17 07:28

具体题目是,POJ-3321,这里不给了;

数据先给了n;代表了一共有n个编号(对于n个结点);

大致题意是给一颗根节点为编号1的树,以及接下来的n-1条边(x,y) 根据discuss区描述这条边就是x to y,不用考虑y to x;(这不是重点)

再是m个操作(点修改and这个点的子树的结点个数查询);

31 21 33Q 1C 2Q 1
样例:根节点1的两个子树是2,3;所以Q (1) 返回3(1,2,3) 当C(2)后,就只有两个了 返回2

首先不管怎么样,我们先得根据边建树(图);

用邻接表

vector<int> tree[N];

scanf("%d%d",&x,&y);

tree[x].push_back(y);

这样子就行了

我刚拿这个题目并不会使用线段树和树状数组,所以我直接暴力模拟感觉也能过

思路是并查集思想,用cnt【i】来存放编号i结点的子树的结点个数

怎么求cnt[i]呢?当然是DFS(后根遍历)一遍cnt[i]=sum of (cnt[子树]);然后顺便把 fa【i】连接上就可以找到祖先了;

修改一个点后,向上更新祖先的值,这样要是树比较平衡的话更新的复杂度是O log(n)

查询是O(1);

但是TLE;很显然这棵树比较极端;

所以我百度了题解发现都是用树状数组或是线段树来写的。

首先你们得知道这两个数据结构,这里给两个我看懂的博客;

线段树             

树状数组 

当然虽说这是两个基础数据结构,但是对菜鸡(我)也不好懂。

然后我看完这两种数据结构感觉有点想法了,然后回过头来看题目,完全不知道怎么用 有没有?这树的编号和区间有半毛钱关系啊!!!;

所以我有仔细的研究了一番别人的题解;

震惊!!!他们竟然给树的结点重新编号

然而我不理解呀!!为什么可以可以这样编号,把树的结构给线性化了,而且刚好可以套上树状数组啊!

好吧!先不吐槽了!先看一下他们是怎么编号的;重点!!注意 L数组,R数组!!;其他的Tree是上面的树,C数组是树状数组要用的空间

这是我模仿写的代码

#include<cstdio>#include<iostream>#include<cstring>#include<queue>#include<cmath>#include<vector>#define lbt(x) x&-x#define mem(a) memset(a, 0, sizeof(a))using namespace std;const int N=1e5+100;vector<int> tree[N];       //int L[N],R[N];         //L是该节点在新编号下的左边界也是他本身的新编号,比如结点1 L【1】=1,R【1】=n;int C[N];          //bool apple[N];       //int n,m,key;void add(int x,int val){  修改点    while(x<=n){        C[x]+=val;        x+=lbt(x);    }}int read(int x){    求1-x的和;    int sum=0;    while(x>0){        sum+=C[x];        x-=lbt(x);    }    return sum;}void dfs(int i){  这才是关键    L[i]=key;    for(int j=0;j<tree[i].size();++j){        key++;        dfs(tree[i][j]);    }    R[i]=key;}void init(){    mem(L);mem(R);mem(C);    for(int i=0;i<N;i++) tree[i].clear();}int main(){    //freopen("in.txt","r",stdin);    cin>>n;    init();    for(int i=1;i<n;i++){        int x,y;scanf("%d%d",&x,&y);        tree[x].push_back(y);    }    for(int i=1;i<=n;i++){        apple[i]=true;        add(i,1);    }    key=1;dfs(1);cin>>m;    for(int i=1;i<=m;i++){        char op[2];int x;scanf("%s%d",op,&x);        if(op[0]=='Q') printf("%d\n",read(R[x])-read(L[x]-1));        else{            if(apple[x]==true){apple[x]=false;add(L[x],-1);}            else{apple[x]=true;add(L[x],1);}     } }

大家仔细揣摩一番,不知道看懂了没有,反正刚开始我是一脸MB;

其中 L是该节点在新编号下的左边界也是他本身的新编号,比如结点1   L【1】=1,R【1】=n,也就是根节点是新编号的数组的第一个结点。控制着(1,n)的和;

正好跟1 的子树就是整棵树一样;

那这是为什么呢?;

这是因为这里在重新编号的时候充分考虑到DFS的特点,

把!树形!对应关系转换成一个!线性!的先序遍历序列


这样做的原因有两点

1:这样遍历能把一颗颗子树在用线性的关系隔开,如图三颗子树隔开了;

2:刚好符合树状数组的求解;一颗树从根到最后一个叶子结点,就是区间最右端-区间最左端

到此这个问题告诉我重新地理解了DFS--线性化;并加强了对两种数据结构的理解;

最后给上用线段树模板ac的代码;
#include<cstdio>#include<iostream>#include<cstring>#include<queue>#include<cmath>#include<vector>#define mem(a) memset(a, 0, sizeof(a))#define ls l,m,rt<<1#define rs m+1,r,rt<<1|1using namespace std;const int N=1e5+100;vector< vector<int> > tree(N);int L[N],R[N],A[N];bool apple[N];int Sum[N<<2];int n,m,key;void init(){    mem(L);mem(R);    for(int i=1;i<=n;i++){        apple[i]=true;        A[i]=1;    }}void PushUp(int rt){Sum[rt]=Sum[rt<<1]+Sum[rt<<1|1];}//Build函数建树void Build(int l,int r,int rt){ //l,r表示当前节点区间,rt表示当前节点编号    if(l==r) {//若到达叶节点        Sum[rt]=A[l];//储存数组值        return;    }    int m=(l+r)>>1;    //左右递归    Build(l,m,rt<<1);    Build(m+1,r,rt<<1|1);    //更新信息    PushUp(rt);}void Update(int L,int C,int l,int r,int rt){//l,r表示当前节点区间,rt表示当前节点编号    if(l==r){//到叶节点,修改        Sum[rt]+=C;        return;    }    int m=(l+r)>>1;    //根据条件判断往左子树调用还是往右    if(L <= m) Update(L,C,l,m,rt<<1);    else       Update(L,C,m+1,r,rt<<1|1);    PushUp(rt);//子节点更新了,所以本节点也需要更新信息}int Query(int L,int R,int l,int r,int rt){//L,R表示操作区间,l,r表示当前节点区间,rt表示当前节点编号    if(L <= l && r <= R){        //在区间内,直接返回        return Sum[rt];    }    int m=(l+r)>>1;    //累计答案    int ANS=0;    if(L <= m) ANS+=Query(L,R,l,m,rt<<1);    if(R >  m) ANS+=Query(L,R,m+1,r,rt<<1|1);    return ANS;}void dfs(int i){    L[i]=key;    for(int j=0;j<tree[i].size();++j){        key++;        dfs(tree[i][j]);    }    R[i]=key;}int main(){    //freopen("in.txt","r",stdin);    cin>>n;    init();    for(int i=1;i<n;i++){        int x,y;scanf("%d%d",&x,&y);        tree[x].push_back(y);    }    key=1;dfs(1);Build(1,n,1);cin>>m;    for(int i=1;i<=m;i++){        char op[2];int x;scanf("%s%d",op,&x);        if(op[0]=='Q') printf("%d\n",Query(L[x],R[x],1,n,1));        else{            if(apple[x]==true){apple[x]=false;Update(L[x],-1,1,n,1);}            else{apple[x]=true;Update(L[x],1,1,n,1);}        }    }}

vector< vector<int> > tree(N);
注意这里我改成了这样建树,从1.8s到了1s 具体涉及到vector的使用效率问题

看这个文章就明白了

原创粉丝点击