poj3321---Apple Tree

 题目大意:

给你一颗苹果树,树的主干设为1,每一个分支设为一个数,一直到N,代表这颗苹果树。每个分支上面只能最多有一个苹果,也就是一个枝子上面不可能有两个苹果,另外注意一点,不要把苹果树想象成二叉树,苹果树每个节点可以分出很多叉,应该是多叉树。

 

输入是叉之间的关系,

1 2

1 3

就是主干上面两个叉分别是2 和3.

 

下面是两种操作,Q 和C

C   j  的意思是如果 j 这个枝子上面有苹果就摘下来,如果没有,那么就会长出新的一个

Q  j  就是问 j 这个叉上面的苹果总数。

 

这个题很明显是线段树,其实我很多时候就用树状数组解决,但是,这个数组怎么来定?????算法很容易搞定,但是初始化是问题。

先来说一下树状数组:

 

先看一个例题:
数列操作:
给定一个初始值都为0的序列,动态地修改一些位置上的数字,加上一个数,减去一个数,或者乘上一个数,然后动态地提出问题,问题的形式是求出一段数字的和.

若要维护的序列范围是0..5,先构造下面的一棵线段树:

 

 

 

可以看出,这棵树的构造用二分便可以实现.复杂度是2*N.
每个结点用数组a来表示该结点所表示范围内的数据之和.
修改一个位置上数字的值,就是修改一个叶子结点的值,而当程序由叶子结点返回根节点的同时顺便修改掉路径上的结点的a数组的值. 
对于询问的回答,可以直接查找i..j范围内的值,遇到分叉时就兵分两路,最后在合起来.也可以先找出0..i-1的值和0..j的值,两个值减一减就行了.后者的实际操作次数比前者小一些.
这样修改与维护的复杂度是logN.询问的复杂度也是logN,对于M次询问,复杂度是MlogN.
然而不难发现,线段树的编程复杂度大,空间复杂度大,时间效率也不高.

更好的解决办法就是用树状数组!

下图中的C数组就是树状数组,a数组是原数组

 

 

 

 

对于序列a,我们设一个数组C定义C[i] = a[i-2^k+1] + a[i-2^k+2] + … + a[i],k为i在二进制下末尾0的个数。
2^k的计算可以这样:2^k = i & (i^(i-1)) 

inline int Lowbit(int x){
       // 求x最低位1的权
       return x & (x^(x-1));
}
定义C[i] = a[i-2^k+1]+a[i-2^k+2]+...+a[i];
若要修改 a[k]的值,则C数组的修改过程如下:(n为C数组的大小)
void Change(int k, int delta){
       while( k<=n ){
              C[k] += delta;
              k += LowBit(k);
       }
}

求a中1..k元素的和
int Getsum(int k){
       int ret = 0;
       while( k>0 ){
              ret += C[k];
              k -= Lowbit(k);
       }
       return ret;
}


 

若要求i..j的元素和的值,则求出 1~i-1和1~j的值,相减即可

在很多的情况下,线段树都可以用树状数组实现.凡是能用树状数组的一定能用线段树.当题目不满足减法原则的时候,就只能用线段树,不能用树状数组.例如数列操作如果让我们求出一段数字中最大或者最小的数字,就不能用树状数组了.

注:用数组保存静态一维线段数也可以不记录左右孩子的下标
如0号是根节点,其孩子是1、2;
1号的孩子是3、4;
2号的孩子是5、6;
3号的孩子是7、8;
……
n号的孩子是2*n+1、2*n+2。

再说说本题如何用树状数组来解决:

 

想必大家都明白上面说的了吧,就是为了快速的查询一个数组某一段的和,一旦这一段经过修改,依然快速给出答案。

而这个题不正是这个意思么,原先都是一个苹果,摘掉或者新长出,不就是改变数组的某个值么,这时候要查询的不就是某个区间的值么?

 

而上面的例子就是一个一维数组,但是现在如何把一个树转化为一维数组呢?这是这道题目的关键,否则没法做。

如果理解了这一点,就好办了。


对一棵树进行深搜,然后将深搜的顺序重新标上号,然后放到一个数组中,记下每一个枝子得起始点和终结点,然后就可以用树状数组了。

还是举个例子吧。。。


如果一棵苹果树是这样的。

 

那么我们知道1是主干。。。。

深搜的顺序是1 4 6 5 7 2 3 当然这也是根据题目的输入建的广义表。

那么当我们深搜的时候就可以在下面标上:

1 4 6 5 7 2 3 (广义表里面的东西,深搜结果)

1 2 3 4 5 6 7(num数组我们求和要用的)

这样不就是树状数组了么。

我们用start【1】= 1 end【1】=7代表1以上的树枝为1-7,也就是所有

用 start【2】= 6  end【2】 = 7 代表2以上的树枝是 3

同理

start【4】=2 end【4】 = 5 这样我们就知道4上面的树枝了

每一个树枝在num【n】总数组中的位置,这样我们就可以计算啦。。。树状数组,和上面将的一样了,成功转换

具体键代码,main函数主要建广义表,dfs主要是找到每一个树枝在num【n】数组中的起点和终点并记录在start和end数组中。


这样下面就是树状数组的三个函数。

有人会问,为什么要+ 或者-  x & (x^(x-1)),这个我们可以看上面的规律,具体起源于什么,我也不知道,根据上面讲的就是这个数的二进制从右边第一个1的位置。


如果还不是很明白,请回复,我会的尽量解释,谢谢。

代码:

#include <iostream>
#include <cstdio>
using namespace std;

int n, m;
int inc = 0;
int num[100001];
int start[100001];
int end[100001];


//要用到的广义表的结构
struct TREE 
{
	int data;
	TREE * next;
};
TREE tree[100001];


void dfs(int pos)
{
	int i, j, k;
	start[pos] = ++ inc;
	TREE *p = tree[pos].next;
	while (p)
	{
		if (start[p->data] == 0)
		{
			dfs(p->data);
		}
		p = p->next;
	}
	end[pos] = inc;
}

//中间用到的规律值
int lowBit(int x)
{
	return x&(x^(x-1));
}
//求从开始到这里的和
int sSum(int end)
{
	int sum = 0;
	while (end > 0)
	{
		sum += num[end];
		end -= lowBit(end);
	}
	return sum;
}
//更新自己并且和自己相关的
void change(int pos, int tmp)
{
	while (pos <= n)
	{
		num[pos] += tmp;
		pos += lowBit(pos);
	}
}

int main()
{
	int j,k,s,t;
	scanf("%d", &n);
	for (int i = 1; i < n; i ++)
	{
		//每一个点都建一个长长的链表,表示自己的一个分支
		scanf("%d%d", &s, &t);
		TREE *p = new TREE;
		p->data = t;
		p->next = tree[s].next;
		tree[s].next = p;

		TREE *q = new TREE;
		q->data = s;
		q->next = tree[t].next;
		tree[t].next = q;

	}
	//映射到树状数组
	dfs(1);
	//每个初始化有一个苹果
	for (int i = 1; i <= n; i ++)
	{
		change(i, 1);
	}
	char ch;
	scanf("%d", &m);
	for (int i = 0; i < m; i ++)
	{
		getchar();
		scanf("%c%d", &ch, &j);
		if (ch == 'C')
		{
			int sum = sSum(start[j]);
			sum -= sSum(start[j] - 1);
			if (sum == 1)
			{
				change(start[j], -1);
			}
			else
			{
				change(start[j], 1);
			}
		}
		else
		{
			int sum = sSum(end[j]);
			sum -= sSum(start[j] - 1);
			printf("%d\n", sum);
		}
	}
	return 0;
}


 


 

 

  • 7
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 12
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值