POJ3321 Apple Tree


题意:给一棵有根树,各节点编号为1到N,其中根的编号为1,各个节点上可能有1个苹果或者没有苹果。现在需要支持两种操作:

(1)改变一个节点的“苹果情况”,即:如果原来有则吃掉它,如果没有则长出一个。

(2)询问以某个节点为根的子树上一共有多少苹果。

操作数量为M。1 <= N,M <= 100000。

 

         由于此题出现于“树状数组练习题”,所以我就仔细思考如何使用树状数组……

         树状数组只能维护区间和,如何把一个子树的和转化为区间和呢?也就是如何把子树转化为区间呢?于是我们就想到了后根遍历序列。(其实前根遍历也可以,没有什么区别)后根遍历序列当中,一个节点的所有子孙以及他自己是连续出现的。那么对于一个节点,他对应的区间是哪一块呢?这个区间的结束位置肯定是他自己对应的位置(后根嘛),而起始位置可以先递归到他的孩子里面,然后让孩子返回自己的子树的起始位置,然后他把所有孩子的起始位置取最小即可。

         然后就是标准的树状数组问题了。对于后根序列维护和查询区间和,这个就不说了……

         还有一个比较麻烦的问题。可以想见,我们在进行各种操作的时候经常会遇到把“苹果树上编号”(以后称为num)与“后根序列中位置”,也就是“树状数组中编号”(以后称为id)相互转化的需要。(具体来说;维护后根序列即是id到num的映射,之后查询的时候给出的是num,需要转化为id之后再去树状数组里查询)因此我们要维护一个双向的映射,从num映射到id以及从id映射到num。

         然后这就是我们的大致思路:输入之后,从1号节点后序遍历整棵树。遍历过程:对于每个节点,先递归遍历所有孩子,然后把后根序列curID++,维护自己id到num和num到id的映射,并记录自己的minID(子树区间起始位置),然后返回值为自己的minID。然后,对于每个操作,先把输入的num转成id,然后去维护或者查询树状数组即可。


#include <iostream>
#include <cstring>
#include <cstdlib>
#include <cstdio>
#include <vector>
#define INF 1000007

using namespace std;

int N,M;
vector<int> G[100007];	//邻接表 

void AddEdge(int a, int b)
{
	G[a].push_back(b);
	G[b].push_back(a);
}

int minID[100007]={};	//[id]
int n2ID[100007]={};	//[1 ~ N]
int ID2n[100007]={};	//[1 ~ N]

/* postRoot: 后根遍历, 建立苹果树num到树状数组id的双向映射,
 * 并维护苹果树中各节点minID 
 */ 

bool vis[100007]={};
int tempID = 0;
int postRoot(int x)		//后根遍历, x: num
{
	vis[x] = true;
	int cMinID = INF;
	for(int i = 0; i < G[x].size(); i++)
	{
		if(!vis[G[x][i]])
		{
			cMinID = min(cMinID, postRoot(G[x][i]));	//维护minID 
		}
	}
	
	cMinID = min(cMinID, ++tempID);
	n2ID[x] = tempID;		//维护双向映射的信息 
	ID2n[tempID] = x;		//维护双向映射的信息 
	minID[tempID] = cMinID;	//记录inID 
	return cMinID;
}

int sumv[100007]={};		//树状数组 

void add(int x, int a)		//x: id
{
	for(; x <= N; x += (x & (-x)) )
	{
		sumv[x] += a;
	}
}

int psum(int x)				//x: id
{
	int res = 0;
	for(; x > 0; x -= (x & (-x)) )
	{
		res += sumv[x];
	}
	return res;
}

bool exist[100007];		//[num]

int main()
{
	scanf("%d",&N);
	for(int i = 0; i < N-1; i++)
	{
		int a,b;
		scanf("%d%d", &a, &b);
		AddEdge(a,b);
	}
	postRoot(1);			//后根遍历 
	for(int i = 1; i <= N; i++)
	{
		exist[i] = true;	//初始时各处都有苹果 
		add(i, 1);		//初始时各处都有苹果 
	}
	
	scanf("%d",&M);
	for(int i = 0; i < M; i++)
	{
		char cmd;
		int n;
		while(scanf("%c", &cmd), cmd == '\n');	//滤掉回车 
		scanf("%d", &n);
		if(cmd == 'Q')
		{
			int nID = n2ID[n];
			int ans = psum(nID) - psum(minID[nID]-1);	//求[minID[nID], nID]区间的和 
			printf("%d\n", ans);
		}
		else if(cmd == 'C')
		{
			if(exist[n]) 	//已有苹果,则吃掉并维护信息 
			{
				exist[n] = false;
				add(n2ID[n], -1); 
			}
			else		//木有苹果,则长出并维护信息 
			{
				exist[n] = true;
				add(n2ID[n], 1);
			}
		}
	}
	return 0;
}


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值