【算法与实现】线段树&树状数组(下)

线段树&树状数组(下)

二、树状数组

 

1.结构描述

对于序列a,我们设一个数组C满足下列条件:

①     C[i]=a[i-2^k+1]+ …+a[i]

②     K为i在二进制下末尾0的个数

③     2^k就是i保留最右边的1,其余位全变0

④     i从1开始计算

则有:C即为a的树状数组


对于i,如何求2^k?

2^k=i&(i^(i-1))也就是i&(-i)

通常我们用lowbit(x)表示x对应的2^k,即lowbit(x)=x&(-x)

lowbit(x)实际上就是x的二进制表示形式留下最右边的1,其他位都变成0

则有:C[i]=a[i-lowbit(i)+1]+ … +a[i],用图表示如下:


树状数组的好处在于能快速求任意区间和a[i]+a[i+1]+ … +a[j]

设sum(k)=a[1]+a[2]+ … +a[k]

则a[i]+a[i+1]+ … +a[j]=sum(j)-sum(i-1)


有了树状数组,sum(k)就能在O(logN)时间内求出,N是a数组元素个数。而且更新一个a的元素所花的时间也是O(logN)的(因为a更新了C也得更新)。根据C的构成规律,可以发现sum(k)可以表示为:



如:sum(6)=C[4]+C[6]

Lowbit(x)实际上就是x的二进制表示形式留下最右边的1,其他位都变成0




如果a[i]更新了,那么以下的几项都需要更新:




初始状态下由a构建树状数组C的时间复杂度是O(N)

应为

C[k]=sum(k)-sum(k-lowbit(k))

 

所以,树状数组适合单个元素经常修改而且还反复要求部分的区间的和的情况。上述问题虽然也可以用线段树解决,但是用树状数组来做,编程效率和程序运行效率都更高(时间复杂度相同,但是树状数组常数小)如果每次要修改的不是单个元素,而是一个区间,那就不能用树状数组了(效率过低)。

 

树状数组时间复杂度总结:

建数组:O(n)

更新:O(logn)

局部求和:O(logn)

 


2.相关案例

POJ 3321 Apple Tree

每个分叉点及末梢可能有苹果(最多1个),每次可以摘掉一个苹果,或有一个苹果新长出来,随时查询某个分叉点往上的子树里,一共有多少个苹果。(分叉点数:100,000 )此题可用树状数组来做,代码如下:

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

#define MY_MAX 100010

typedef class Link
{
public:
	int num;
	Link *next;
	Link(int a)
	{
		num=a;
		next=NULL;
	}
}*Head;


Head linkList[MY_MAX]={0};
int hash[MY_MAX];
bool isVisted[MY_MAX]={0};
int nFork[MY_MAX]={0};
int nCount=0;

int dfs(Head p,int fork)
{
	int s=1;
	if(isVisted[fork])
		return 0;
	isVisted[fork]=true;
	hash[fork]=++nCount;
	while(p!=NULL)
	{
		s+=dfs(linkList[p->num],p->num);
		p=p->next;
	}
	nFork[fork]=s;
	return s;
}

void InsertLink(int x,int y)
{
	if(linkList[x]==NULL)
		linkList[x]=new Link(y);
	else
	{
		Head link=linkList[x];
		while(link->next!=NULL)
		{
			link=link->next;
		}
		link->next=new Link(y);
	}
}

int Lowbit[MY_MAX];
int sum[MY_MAX]={0};
int A[MY_MAX];
int C[MY_MAX];

int QuerySum(int x)
{
	int Sum=0;
	while(x>0)
	{
		Sum+=C[x];
		x-=Lowbit[x];
	}
	return Sum;
}

void Change(int x,int val)
{
	while(x<=nCount)
	{
		C[x]+=val;
		x+=Lowbit[x];
	}
}

int main()
{
	char c;
	int i,n,a,b,m,t,s=0;
	scanf("%d",&n);
	for(i=1;i<n;i++)
	{
		scanf("%d%d",&a,&b);
		InsertLink(a,b);
		InsertLink(b,a);
	}
	dfs(linkList[1],1);
	for(i=1;i<=n;i++)
	{
		Lowbit[i]=i&(0-i);
		A[i]=1;
	}
	for(i=1;i<=n;i++)
	{
		s+=A[hash[i]];
		sum[i]=s;
		C[i]=sum[i]-sum[i-Lowbit[i]];
	}
	scanf("%d",&m);
	while(m--)
	{
		getchar();
		scanf("%c%d",&c,&t);
		if(c=='C')
		{
			if(A[hash[t]]==0)
			{
				A[hash[t]]=1;
				Change(hash[t],1);
			}
			else
			{
				A[hash[t]]=0;
				Change(hash[t],-1);
			}
		}
		else
			printf("%d\n",QuerySum(hash[t]+nFork[t]-1)-QuerySum(hash[t]-1));
	}
	return 0;
}//树状数组求和速度比线段树要快,但是试用范围比较有限


三、二维线段树&树状数组

 

1.二维树状数组


案例:POJ 1195Mobile phones,题目大意为一个由数字构成的大矩阵,开始是全0,能进行两种操作

①对矩阵里的某个数加上一个整数(可正可负)。

②查询某个子矩阵里所有数字的和。

要求对每次查询,输出结果


二维树状数组解法代码如下:

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

#define MAX 1100

int C[MAX][MAX];
int lowbit[MAX];
int nCount=0;

void Add(int x,int y,int t)
{
	while(x<=nCount)
	{
		int y0=y;
		while(y0<=nCount)
		{
			C[x][y0]+=t;
			y0+=lowbit[y0];
		}
		x+=lowbit[x];
	}
}

int QuerySum(int x,int y)
{
	int s=0;
	while(x>0)
	{
		int y0=y;
		while(y0>0)
		{
			s+=C[x][y0];
			y0-=lowbit[y0];
		}
		x-=lowbit[x];
	}
	return s;
}

int main()
{
	int n,a,b,c,d,i;
	while(scanf("%d",&n)&&n!=3)
	{
		switch(n)
		{
		case 0:
			{
				scanf("%d",&a);
				nCount=a;
				memset(C,0,sizeof(C));
				for(i=1;i<=a;i++)
					lowbit[i]=i&(0-i);
			}
			break;
		case 1:
			{
				scanf("%d%d%d",&a,&b,&c);
				Add(a+1,b+1,c);
			}
			break;
		case 2:
			{
				scanf("%d%d%d%d",&a,&b,&c,&d);
				a++;b++;c++;d++;
				printf("%d\n",QuerySum(c,d)+QuerySum(a-1,b-1)
					-QuerySum(a-1,d)-QuerySum(c,b-1));
			}
		}
	}
	return 0;
}

2.二维线段树



案例:POJ 1195Mobile phones,还是上面的问题,这次用二维线段树(树套树)的方法来解决,代码如下:

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

#define MY_MAX 1100

//开到4倍存储会溢出
int Tree[MY_MAX * 3][MY_MAX * 3];
int S;

//建树过程省略也是6的不行

void Add_x( int rooty,int rootx, int L, int R, int x ,int a)
{
	Tree[rooty][rootx] += a;
	if( L == R ) 
		return;
	int mid = (L + R )/2;
	if( x <= mid )
		Add_x(rooty,( rootx << 1) + 1, L ,mid, x, a);
	else
		Add_x(rooty,( rootx << 1) + 2, mid + 1,R, x, a);
}

void Add_y(int rooty, int L,int R, int y, int x,int a)
{
	Add_x( rooty,0, 1, S, x,a);
	if( L == R)
		return;
	int mid = (L + R )/2;
	if( y <= mid ) 
		Add_y( ( rooty << 1) + 1, L, mid,y, x, a);
	else
		Add_y( ( rooty << 1) + 2, mid+1, R, y, x, a);
}

int QuerySum_x(int rooty, int rootx, int L, int R ,int x1,int x2)
{
	if( L == x1 && R == x2)
		return Tree[rooty][rootx];
	int mid = ( L + R ) /2 ;
	if( x2 <= mid ) 
		return QuerySum_x( rooty, (rootx << 1) + 1, 
		L, mid,x1,x2);
	else if( x1 > mid )
		return QuerySum_x( rooty, (rootx << 1) + 2, 
		mid+1,R, x1,x2);
	else
		return QuerySum_x( rooty,(rootx << 1) + 1,
		L, mid ,x1,mid) +
		QuerySum_x( rooty,(rootx << 1) + 2,
		mid + 1, R, mid + 1,x2);
}

int QuerySum_y(int rooty, int L, int R ,int y1, int y2, int x1,int x2)
{
	if( L == y1 && R == y2 )
		return QuerySum_x(rooty,0,1,S,x1,x2);
	int mid = ( L + R ) /2;
	if( y2 <= mid ) 
		return QuerySum_y( (rooty << 1) + 1, L,
		mid ,y1,y2,x1,x2);
	if( y1 > mid )
		return QuerySum_y( (rooty << 1) + 2, 
		mid + 1,R, y1,y2,x1,x2);
	else
		return QuerySum_y( (rooty << 1) + 1, L, 
		mid ,y1,mid ,x1,x2) +
		QuerySum_y( (rooty << 1) + 2, 
		mid + 1, R, mid + 1,y2 ,x1,x2);
}

int main()
{
	int cmd; int x,y,a,l,b,r,t;    int Sum = 0;
	while( true) {
		scanf("%d",&cmd);
		switch( cmd) {
		case 0:
			scanf("%d",& S);
			memset( Tree,0,sizeof(Tree));
			break;
		case 1:
			scanf("%d%d%d",&x ,&y,&a);
			Add_y(0, 1,S, y + 1, x + 1, a);
			break;
		case 2:
			scanf("%d%d%d%d",&l , &b, &r,&t);
			l ++; b++; r ++; t ++;
			printf("%d\n",QuerySum_y(0,1,S,b,t,l,r));
			break;
		case 3:
			return 0;
		}
	}
}
在上面的代码中我们可以看出建树的过程是可以省略的,直接以数组的形式进行相关数据的存储然后进行先关操作,但这样是有一定条件的,就个人观点有以下两个条件:

①不能超过存储上限,因为此方法不适用与指针操作,所以存储单元最大要开至4倍大小。

②如果需要再线段树建树过程中完成某些特殊操作则不适用。



3.实战例题

POJ 2155 Matrix大意是N * N 的矩阵,每个元素要么是0,要么是1,开始全0不断进行两种操作:

①C x1 y1 x2 y2表示要将左上角为(x1,y1),右下角为(x2,y2)的子矩阵里的全部元素都取反(0变1,1变0)(1<=x1<=x2<=n,1<=y1<=y2<=n)。

②Q x y查询(x,y)处元素的值。


个人AC代码如下:

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

#define MY_MAX 1010

int Tree[MY_MAX * 3][MY_MAX * 3];
int S;

void Add_x(int rooty,int rootx, int L, int R, int x1 ,int x2)
{
	if(L==x1&&R==x2)
	{
		Tree[rooty][rootx]++;
		return;
	}
	int mid = (L + R )>>1;
	if( x2 <= mid ) 
		Add_x( rooty, ( rootx << 1) + 1, L, mid, x1, x2);
	else if( x1 > mid )
		Add_x( rooty, ( rootx << 1) + 2, mid+1, R, x1, x2);
	else
	{
		Add_x( rooty, ( rootx << 1) + 1, L, mid, x1, mid);
		Add_x( rooty, ( rootx << 1) + 2, mid+1, R, mid+1, x2);
	}
}

void Add_y(int rooty, int L,int R, int x1, int y1, int x2, int y2)
{
	if(L==y1&&R==y2)
	{
		Add_x( rooty, 0, 1, S, x1, x2);
		return;
	}
	int mid = (L + R )>>1;
	if( y2 <= mid ) 
		Add_y( ( rooty << 1) + 1, L, mid, x1, y1, x2, y2);
	else if( y1 > mid )
		Add_y( ( rooty << 1) + 2, mid+1, R, x1, y1, x2, y2);
	else
	{
		Add_y( ( rooty << 1) + 1, L, mid, x1, y1, x2, mid);
		Add_y( ( rooty << 1) + 2, mid+1, R, x1, mid+1, x2, y2);
	}
}

int Query_x(int rooty,int rootx,int L,int R, int x)
{
	int ans=0;
	ans+=Tree[rooty][rootx];
	if(L==R)
		return ans;
	int mid=(L+R)>>1;
	if(x<=mid)
		ans+=Query_x(rooty,(rootx<<1)+1,L,mid,x);
	else
		ans+=Query_x(rooty,(rootx<<1)+2,mid+1,R,x);
	return ans;
}

int Query_y(int rooty, int L,int R, int x, int y)
{
	int ans=0;
	ans+=Query_x(rooty,0,1,S,x);
	if(L==R)
		return ans;
	int mid=(L+R)>>1;
	if(y<=mid)
		ans+=Query_y((rooty<<1)+1,L,mid,x,y);
	else
		ans+=Query_y((rooty<<1)+2,mid+1,R,x,y);
	return ans;
}

int main()
{
	char cc;
	int n,s,m,a,b,c,d;
	scanf("%d",&n);
	while(n--)
	{
		scanf("%d%d",&s,&m);
		memset(Tree,0,sizeof(Tree));
		S=s;
		while(m--)
		{
			getchar();
			scanf("%c",&cc);
			if(cc=='C')
			{
				scanf("%d%d%d%d",&a,&b,&c,&d);
				Add_y(0,1,S,a,b,c,d);
			}
			else
			{
				scanf("%d%d",&a,&b);
				printf("%d\n",Query_y(0,1,S,a,b)&1);
			}
		}
		puts("");
	}
	return 0;
}

在讨论区发现一个代码个人觉得很优秀就贴过来学习一下啦,不是本人写的但很值得学习一下:

#include <iostream>
#include <cstdio>
#include <cstring>
#include <cstdlib>
#include <algorithm>
using namespace std;
#define lson l , m , rt << 1 
#define rson m + 1 , r , rt << 1 | 1

bool seg[4010][4010];
int n,m,T,ans;

void buildy(int i,int l,int r,int rt)
{
	seg[i][rt] = 0;
	if(l == r) return ;
	int m = (l + r) >> 1;
	buildy(i,lson);
	buildy(i,rson);
}

void buildx(int l,int r,int rt)
{
	buildy(rt,1,n,1);
	if(l == r) return ;
	int m = (l + r) >> 1;
	buildx(lson);
	buildx(rson);
}
void updatey(int i,int L,int R,int l,int r,int rt)
{
	if(L <= l && r <= R)
	{
		seg[i][rt]^= 1;
		return ;
	}
	int m = (l + r) >> 1;
	if(m >= L)
	updatey(i,L,R,lson);
	if(m < R)
	updatey(i,L,R,rson);	
}

void updatex(int L,int R,int y1,int y2,int l,int r,int rt)
{
	if(L <= l && r <= R)
	{
		updatey(rt,y1,y2,1,n,1);
		return ;
	}
	int m = (l + r) >> 1;
	if(L <= m) updatex(L,R,y1,y2,lson);
	if(R > m)  updatex(L,R,y1,y2,rson);
}

void queryy(int i,int y,int l,int r,int rt)
{
	ans^= seg[i][rt];
	if(l == r)
	return ; 
	int m = (l + r) >> 1;
	if(y <= m) queryy(i,y,lson);
	else  queryy(i,y,rson);
}

void queryx(int x,int y,int l,int r,int rt)
{
	queryy(rt,y,1,n,1);
	if(l == r)
	return ;
	int m = (l + r) >> 1;
	if(x <= m) queryx(x,y,lson);
	else queryx(x,y,rson);
}

int main()
{
	scanf("%d",&T);
	while(T--)
	{
		scanf("%d%d",&n,&m);
		buildx(1,n,1);
		for(int i = 0 ; i < m ; i++)
		{
			char s[2];
			scanf("%s",s);
			if(s[0] == 'C')
			{
				int x1,x2,y1,y2;
				scanf("%d%d%d%d",&x1,&y1,&x2,&y2);
				updatex(x1,x2,y1,y2,1,n,1);
			}
			else
			{
				ans = 0;
				int x,y;
				scanf("%d%d",&x,&y);
				queryx(x,y,1,n,1);
				printf("%d\n",ans);
			}
		}
		if(T)
		printf("\n");
	}
	return 0;
}
在上面的代码中可以看到线段树的维护和查询或者建树是的区间判断是可以又3个分支的判断简化为2个if进行依次判断的,而且这份代码中位运算的运用很不错,Mark了。




注:以上总结部分类容来自于北京大学ACM暑期课程资料,总结只是为了方便自己查阅&和大家交流=.=

本文固定链接:http://blog.csdn.net/fyfmfof/article/details/39682845

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值