数据结构 (线段树入门详细介绍)(单点更新)(结构体)

 (线段树入门)

HDU 1166 敌兵布阵(单点更新)(结构体形式)

题目意思:输入T,T个测试样例

每一次输入一个n,说明有n个数据。再输入n个数据。

输入Query a b :查询a b 之间的和

输入Add i x:在 i 处增加 x个数

输入Sub i x:在 i 处删除 x个数

输入End 结束


举例:

1

10

1 2 3 4 5 6 7 8 9 10

Query 1 3:  输出 6(1+2+3)

Add 3 6:     数组变为1 2 9 4 5 6 7 8 9 10

Query 2 7:  输出33(2+9+4+5+6+7)

Sub 10 2:   数组变成1 2 9 4 5 6 7 8 9 8

Add 6 3:     数组变成1 2 9 4 5 9 7 8 9 8

Query 3 10:输出59(9+4+5+9+7+8+9+8)

End :          结束


建树: 线段树的优点在于:★将可能的和提前算好(如:要算结点16,17,9,10,11的和,那么只用输出结点2那么就是了,减少了查询时相加的过程)

                                                                     55(1)

                                                /                                                   \

                                 15(2)                                                               40(3)

                             /                    \                                                  /                           \

                   6(4)                           9(5)                        21(6)                        19(7)

                /              \                      /              \                       /                \                  /                  \

          3(8)           3(9)      4(10)      5(11)     13(12)     8(13)     9(14)        10(15)  

       /            \            【3】         【4】          【5】       /             \        【8】        【9】             【10】

1(16)     2(17)                                             6(18)        7(19)

【1】          【2】                                                 【6】             【7】

tip:()中结点编号,【】中为范围下标,普通的就是输入的数值和部分范围内的和

每棵树结点:

#define ll rt<<1
#define rr rt<<1|1
using namespace std;
const int maxn=50010;
struct Tree{
	int l,r;//sum值包含的 最左边界 和 最右边界 
	int sum;
}tree[maxn<<2];


①建线段树:

void PushUP(int rt){
	tree[rt].sum= tree[ll].sum +tree[rr].sum;
}
void build(int l,int r,int rt)
{
	tree[rt].l=l; tree[rt].r=r;
	if(l==r){
		scanf("%d",&tree[rt].sum);
		return ;
	}
	int m=(l+r)>>1;
	build(l,m,ll);  //ll  -> #define ll rt<<1
	build(m+1,r,rr);//rr  -> #define rr rt<<1|1
	PushUP(rt);
}

输入建树的范围最左值(默认从1开始),最右值(输入的n),和树的总根结点(默认以1为总根结点)

build(1,n,1);

tree[ rt ].l = l ;  tree[ rt ].r = r ; 

总的根节点 rt = 1(1)。那么tree[1]最左为1,最右为n,意思是:结点(1)所包含的和是从1到n的

然后分别向左右两边差分建树。

build( l , m , rt * 2 );

左边( 1,(n+1)/2  )

那么左边的根节点为 rt = 2*rt =2(2) 。那么tree[2]最左为1,最右为(n+1)/2,意思是:结点(2)所包含的和是从1到 n/2 的

继续往左右建树 ……

build( m+1 , r , rt * 2+1 );

右边类似。

PushUP( rt );

执行完上面两步之后,根节点rt 已经有 左右节点了,那么rt 的值就为 rt * 2与 rt * 2 + 1的和

if ( l == r )

直到 l == r,意思是范围缩小到一个点,那么在这个点输入值。

★为什么建树 最底层的叶子节点 是值 1 ,2 ,6  ,7而不是1 ,2 ,3 ,4 ?

因为树建立过程是折半的,将 数组[ 10 ]= 1 2 3 4 5 6 7 8 9 10

分成两半,一半 1 2 3 4 5 ,一半 6 7 8 9 10。

然后继续分成两边。分到底的时候,将1,2放到最底部,然后往上。

(取巧)因为一半一半,12345 和678910是等效的。所以最底层叶子节点的值为1,2,和6,7

★左右建树的顺序能换吗?

不能,输入的顺序从左到右,所以要优先往左边探,折半探索到左边的底,开始输入第一个数,然后依次探索到底,依次输入其他数。这样就是按顺序到树里了。

左右建树的顺序换了之后,那输入的数就不是原来的顺序了(就乱了)

★数值比较多,再帮忙屡一下,分为三类:

(1)rt 这个值是结点编号

(2)tree[ rt ].sum 这个值是 输入的值,和输入值的部分和

(3)tree[ rt ].l ,tree[ rt ].r 的值是左右范围,即1~n的数


②更新操作

void update(int p,int add,int rt)
{
	if( tree[rt].l == tree[rt].r ) {
		tree[rt].sum += add;
		return ;
	}
	int m = ( tree[rt].l + tree[rt].r ) >>1;
	if(p <= m) update( p, add, ll );
	else update( p, add, rr );
	PushUP(rt);
}

输入:要改的值的位置p,要增加的数值add,总根结点rt(默认1)

当根节点rt 的左右范围值相等时,那么就是该查询的点了。根节点 这个值 +add

如果要更新的那个值所在的位置 比 rt的左右范围的中间值小,那么要更新位置在 rt 的 左边,反之在右边

做完上面的更新之后,然后重新计算和值。


③查询操作

int query(int L,int R,int rt)
{
	if(L <= tree[rt].l && tree[rt].r <= R)  
		return tree[rt].sum;
	int m = ( tree[rt].l + tree[rt].r )>>1;
	if(R <= m) return query(L,R,ll);
	else if(L > m) return query(L,R,rr);
	else return query(L,R,ll) + query(L,R,rr);
} 

输入:要查询的左右范围,L~R(包括L,R)和总根结点rt
如果所要查的根节点范围L R 刚好包含了 结点rt 左边到右边,那么太好了,因为结点rt 左边到右边的值sum早就已经计算好了,不用往下再去找点了,就是这个和了

                                          m

     ( tree[ rt ].l ) |_______|_______|( tree[ rt ].r)

                (L)|_____________________|(R)

         【状态一(左边和右边都还剩下一点没包含到里面,可能是是下面其他状态,然后用状态四整合)】

然后再看如果R 比 m小

                                                                   m

                           ( tree[ rt ].l ) |________|________|( tree[ rt ].r)

                     ( tree[ rt * 2 ].l ) |________|( tree[ rt * 2 ].r )

      (L)|_____________________|(R)

         【状态二:向左缩小范围(此图只考虑右边部分,左边部分可能是其他状态,然后用状态四整合)】

如果L 比m大

                                     m

    ( tree[ rt ].l ) |______|______|( tree[ rt ].r)

      ( tree[ rt * 2 +1 ].l ) |______|( tree[ rt * 2 +1 ].r)

                                       (L)|_____________________|(R)

         【状态三:向右缩小范围(此图只考虑左边部分,右边部分可能是其他状态,然后用状态四整合)】

其他情况:

                                                        m                                                                                             m

    ( tree[ rt ].l ) |______________|_____________|( tree[ rt ].r)    ( tree[ rt ].l ) |________|________|( tree[ rt ].r)

                            |______________||_____________|                                                  |________||________|

                            (L)|_____________________|(R)                                          (L)|_____________________|(R)

                          【状态四:需要左右相加】                                                               【状态四的子状态(还有右边的情况就不画了)】

可能有人问还有其他状态:

                                     m

    ( tree[ rt ].l ) |______|______|( tree[ rt ].r)

                                                       (L)|_____________________|(R)

★如果这样呢,这样和【状态三】不是一样吗?

答:这部分不存在,我们不考虑这部分。因为一开始范围肯定是最大的1~n,包含了所有情况,那么就是【状态四】,不断的缩小到我们要的部分。

有这种情况,但这个情况正好是【状态二】和【状态三】所丢弃的部分,且看第二行,我们的折半操作,另一半已经丢掉不要了。


④主函数:

int main()
{
	int cas=1,T,n;  
	scanf("%d",&T);  
	while(T--)  
	{  
		printf("Case %d:\n",cas++);  
		scanf("%d",&n);  
		build(1,n,1);
		/*
		for(int i=1;i<=2*n;i++){
			printf("%d %d %d\n",tree[i].l , tree[i].sum , tree[i].r);
		}
		查看建树  
		for(int i=1;i<=3*n;i++) 
			printf("%d--",sum[i]);*/  
		char op[10];
		while(scanf("%s",op)){  
			if(op[0]=='E') //结束  
				break;   
			int a,b;  
			scanf("%d%d",&a,&b);  
			if(op[0]=='Q') //询问   
				printf("%d\n",query(a,b,1));  
			else if(op[0]=='S') //减去   
				update(a,-b,1);  
			else if(op[0]=='A') //增加   
				update(a,b,1);  
		}
	}  
	return 0;  
} 






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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值