POJ - 3321
目录
题意:
给出一个苹果树,每个节点一开始都有苹果。有两种操作:
C X,如果X点有苹果,则拿掉,如果没有,则新长出一个
Q X,查询X点与它的所有后代分支一共有几个苹果
分析:
给我边的信息,用dfs为这棵树编号,并记录每个结点的管辖范围(即线段),查询该结点子树的和其实就是查询这个线段区间的和。即,就可以用线段树或树状数组求解了。
这题用不能用cin 关闭同步流也不行,必须用scanf
一、用树状数组求解
分析:
1、输入条件中告诉我们有n个节点 n-1个邻接关系,首先用邻接表(用vector数组)存储边的信息。
2、因为不是标准的二叉树,然后通过dfs搜索的方式为每一个结点编号,至此我的理解就是我们根据题目中边的信息构建出了一棵有编号的树。另外在编号的过程中,给每个结点赋一个左值和右值(其中左值的意思就是该结点的编号,右值就是子树中的最大编号),用左值和右值表示这个结点的管辖范围(就类似于线段树中的一条线段)。
下面图片内容转载自 链接
上图也就是DFS搜索的时候做标记的过程,这样新的编号为1~6的节点所管辖的范围分别就是[1,6] [2,4] [3,3] [4,4] [5,6] [6,6],其中左边的是左值,右边的是右值,节点1的区间是[1,6],正好这棵子树有6个节点,其他也一样。
3、所以我们求该节点子树中的苹果数时,就用到树状数组的sum()函数,式子为sum(rig[x])-sum(lef[x]-1)意思为求出每一个节点从1~左值的和 和 1~右值的和 他们的差就是这个节点的子树的所有的和(即这棵子树苹果数目)
看到两篇非常好的题解
#include<iostream>
#include<stdio.h>
#include<cstring>
#include<algorithm>
#include<string>
#include<vector>
using namespace std;
const int maxn=1e5+10;
int kase;
int n,m;
int c[maxn];
int lef[maxn],rig[maxn];
int a[maxn];//表示每个结点的值,相当于树状数组中的叶子节点数组
vector<vector<int> > v(maxn);
void dfs(int x)
{
lef[x]=kase;//为左值编号,就是该节点编号
int len=v[x].size();
for(int i=0;i<len;i++)
{
kase++;
dfs(v[x][i]);//递归地为结点x的孩子结点编号
}
/*x的孩子结点都编号完毕之后,设置x结点的右值*/
rig[x]=kase;
}
int lowbit(int x){return x&(-x);}
void add(int x,int num)
{
for(int i=x;i<=n;i+=lowbit(i))
c[i]+=num;
}
int sum(int x)
{
int ans=0;
for(int i=x;i>0;i-=lowbit(i))
ans+=c[i];
return ans;
}
int main()
{
// freopen("in.txt","r",stdin);
while(~scanf("%d",&n))
{
int x;
int e1,e2;
char ch[3];
memset(c,0,sizeof(c));
memset(lef,0,sizeof(lef));
memset(rig,0,sizeof(rig));
memset(a,0,sizeof(a));
for(int i=0;i<maxn;i++)
v[i].clear();
for(int i=1;i<=n;i++)
{
a[i]=1;
add(i,1);//题目要求初始都为1
}
for(int i=1;i<n;i++)
{
scanf("%d%d",&e1,&e2);
v[e1].push_back(e2);//保存边的信息
}
kase=1;
dfs(1);
scanf("%d",&m);
for(int i=0;i<m;i++)
{
scanf("%s%d",ch,&x);
if(ch[0]=='Q')
{
printf("%d\n",sum(rig[x])-sum(lef[x]-1));
}
else
{
if(a[x]==1)
add(lef[x],-1);///这里必须用lef[x],因为x不一定就等于lef[x]
else
add(lef[x],1);
a[x]=!a[x];
}
}
}
return 0;
}
/*
测试样例:
5
1 2
1 3
3 5
3 4
10
Q 1
C 2
Q 1
Q 2
C 2
Q 1
C 5
Q 5
C 5
Q 5
*/
二、用线段树求解
用线段树做其实也挺简单的 虽然让我纠结很久吧。 需要写两个函数,单点修改和区间查询 因为是单点修改 所以就不用写懒惰标记了,这个地方也是纠结很久才想通。
看到一篇文章也是用线段树做的但是他保存边信息的方法是链式向前星法 而我用的是vector数组。我看到一些题解说用vector建图会卡STL,用这个方法就不会,希望以后注意一下这一点。 文章链接。
#include<iostream>
#include<stdio.h>
#include<cstring>
#include<algorithm>
#include<string>
#include<vector>
using namespace std;
const int maxn=1e6;
int kase;
int n,m;
int lef[maxn],rig[maxn];
int a[maxn];
int sum[maxn];
vector<vector<int> > v(maxn);
void dfs(int x)
{
lef[x]=kase;
int len=v[x].size();
for(int i=0;i<len;i++)
{
kase++;
dfs(v[x][i]);//递归地为结点x的孩子结点编号
}
/*x的孩子结点都编号完毕之后,设置x结点的右值*/
rig[x]=kase;
}
void pushup(int k)
{
sum[k]=sum[2*k]+sum[2*k+1];
}
void build(int l,int r,int k)
{
if(l==r)
{
sum[k]=1;
return;
}
int mid=(l+r)/2;
build(l,mid,2*k);
build(mid+1,r,2*k+1);
pushup(k);
}
void updateone(int l,int r,int k,int p,int add)
{
if(l==r)
{
sum[k]+=add;
return;
}
int mid=(l+r)/2;
if(p<=mid) updateone(l,mid,2*k,p,add);
else updateone(mid+1,r,2*k+1,p,add);
pushup(k);
}
int query(int L,int R,int l,int r,int k)//L,R表示要查询的区间,l,r就表示当前的区间
{
if(l>=L&&r<=R)
return sum[k];
int ans=0;
int mid=(l+r)/2;
if(mid>=L) ans+=query(L,R,l,mid,2*k);
if(mid<R) ans+=query(L,R,mid+1,r,2*k+1);
pushup(k);
return ans;
}
int main()
{
// freopen("in.txt","r",stdin);
while(~scanf("%d",&n))
{
int x;
int e1,e2;
char ch[3];
memset(lef,0,sizeof(lef));
memset(rig,0,sizeof(rig));
memset(a,0,sizeof(a));
memset(sum,0,sizeof(sum));
for(int i=0;i<maxn;i++)
v[i].clear();
for(int i=1;i<=n;i++)
a[i]=1;
for(int i=1;i<n;i++)
{
scanf("%d%d",&e1,&e2);
v[e1].push_back(e2);//保存边的信息
}
kase=1;
dfs(1);
build(1,n,1);
scanf("%d",&m);
for(int i=0;i<m;i++)
{
scanf("%s%d",ch,&x);
if(ch[0]=='Q')
{
printf("%d\n",query(lef[x],rig[x],1,n,1));
}
else
{
if(a[x]==1)
updateone(1,n,1,lef[x],-1);
else
updateone(1,n,1,lef[x],1);
a[x]=!a[x];
}
}
}
return 0;
}