HDU - 1754 I Hate It(线段树和树状数组---理论解析,代码到位)

参考题目:

  1. HDU - 1754 ----- I Hate It ---------
    https://cn.vjudge.net/contest/316365#problem/E
    维护区间最大值+单点更新

  2. POJ - 3468 ----- A Simple Problem with Integers ---------
    https://cn.vjudge.net/contest/316365#problem/A
    维护区间和+区间更新

  3. POJ - 3264 ----- Balanced Lineup ---------
    https://cn.vjudge.net/contest/316365#problem/C
    维护区间极值差

以下以HDU - 1754 I Hate It为例

在这里插入图片描述

前提:线段树用来高效解决连续区间的动态查询问题
概念:线段树擅长处理区间,根维护的是整个区间,每个节点维护的是父亲的区间二等分后的一个区间。

建树操作:建树的过程是基于递归实现的。(先纵后横)
1.非结构体的写法1。

void build(int l,int r,int o) {
	if(l==r) {
		scanf("%d",&sum[o]);//遍历到叶子结点的位置,输入一个数
		return;
	}
	int mid=(l+r)>>1;
	build(l,mid,o<<1);//左子树
	build(mid+1,r,o<<1|1);//右子数
	sum[o]=max(sum[o<<1],sum[o<<1|1]);//左右儿子有了,在父亲节点(即左右儿子所代表的区间)维护最大值
}

非结构体的写法2

//主函数 
for(int i=1; i<=n; i++)
	scanf("%d",&s[i]);//先在主函数中输入遍历的n个数
build(1,n,1);
//build函数 
void build(int l,int r,int rt) {
	if(l==r) {             //l==r表示到叶子结点的位置
		sum[rt]=s[l];
		return ;
	}
	int mid=(l+r)>>1;
	build(l,mid,rt<<1);
	build(mid+1,r,rt<<1|1);
	sum[rt]=max(sum[rt<<1],sum[rt<<1|1]);
}

2.结构体的写法

struct node {
	int l,r,sum;//左右区间和区间所维护的最大值
} tree[N];
void build(int l,int r,int o) { //从主函数中传过来的l=1,r=n,o=1
	tree[o].l=l;
	tree[o].r=r;
	if(l==r) {
		scanf("%d",&tree[o].sum);//刚开始每个结构体数组中只有l和r有值,遍历到 
		return;                 //叶子节点的时候,才输入sum的值,再往上回溯,把sum的值补上 
	}
	int mid=(l+r)>>1;
	build(l,mid,o<<1);
	build(mid+1,r,o<<1|1);
	tree[o].sum=max(tree[o<<1].sum,tree[o<<1|1].sum);
}

维护和更改操作
以题例所描述:
当C为’Q’的时候,表示这是一条询问操作,它询问ID从A到B(包括A,B)的学生当中,成绩最高的是多少。 (执行quary函数)
当C为’U’的时候,表示这是一条更新操作,要求把ID为A的学生的成绩更改为B(执行update函数)

update函数的更改操作:

void update(int L,int s,int l,int r,int o) { //将下标为L的位置上的值改为s,刚来时传过来l=1,r=n,o=1
	if(l==r) {
		sum[o]=s; //下标为L的位置肯定是叶子结点 
		return ;
	}
	int mid=(l+r)>>1;
	if(L<=mid)
		update(L,s,l,mid,o<<1);
	else
		update(L,s,mid+1,r,o<<1|1);
	sum[o]=max(sum[o<<1],sum[o<<1|1]);//对儿子修改完后,相应的,父亲的值也要发生变化,再一步步回溯,改变父亲的值 
}

quary函数输出最大值的操作

int query(int L,int R,int l,int r,int o) {//维护区间[L,R]的最大值,从总区间[l,r](即l=1,r=n)开始找目标区间
	if(L<=l && R>=r)
		return sum[o];//如果这个区间是维护区间的子区间,就返回这个区间的最大值 
	int mid=(l+r)>>1;
	int ret=0;
	if(L<=mid)  ret=max(ret,query(L,R,l,mid,o<<1));//左相交 
	if(R>mid)   ret=max(ret,query(L,R,mid+1,r,o<<1|1));//右相交 
	return ret;
}

树状数组的引人思路
1.如果线段树每个节点维护的是对应区间的和,比如说计算从s到t的和(a(s)+…+a(t)),在基于线段树的实现中,这个和是可以直接求得到。如果计算(从1到t的和)-(从1到s-1的和),同样能得到s到t的和。也就是说,对于任意i,我们都能计算出1到i的部分和就行了。这就导致了线段树的右儿子的值不需要了。
2.信息学奥赛一本通
根据任意正整数关于2的不重复次幂的唯一分解定理,若一个正整数x的二进制表示为10101,其中等于1的位是0,2,4,则正整数x可以被“二进制分解”成2^4+2 ^2 + 2^0。进一步,区间[1,x]可以分成O(logx)个子区间。
子区间的特点:若区间结尾为R,则区间长度就等于R的“二进制分解”下最小的2的次幂,我们设为lowbit®
lowbit®:求R的最低位的1的2的次幂。
在这里插入图片描述
数组c可以看成如上图所示的树形结构。
该结构满足以下性质:
1.每个内部结点c[x]保存以它为根的子树中所有叶结点的和。
2.每个内部结点c[x]的子结点个数等于lowbit(x)的位数。
3.除树根外,每个内部结点c[x]的父亲结点是c[x+lowbit(x)]。
4.树的深度为O(logN)。

注意:树状数组依照二进制的对应,都已经完美规划

如何保证右儿子不要,左儿子要?

明白lowbit()的作用
c[1]=a[1];----(0001)
c[2]=a[1]+a[2];----(0010)
c[3]=a[3];----(0011)
c[4]=a[1]+a[2]+a[3]+a[4];-----(0100)
c[5]=a[5];-----(0101)
c[6]=a[5]+a[6];----(0110)
c[7]=a[7];-----(0111)
c[8]=a[1]+a[2]+a[3]+a[4]+a[5]+a[6]+a[7]+a[8];-----(1000)
推出c[i]=a[i-2^k +1]+a[i-2^ k+2]+…+a[i];
k为i的二进制从低位到高位连续0的长度

lowbit()的原型

int lowbit(int x) {
	return x&(-x);
}

树状数组的操作

int sum(int i) { //计算前i项的和
	int s=0;
	while(i>0) {
		s+=c[i];
		i-=bowbit(i);
	}
	return s;
}
void add(int i,int x) { //使第i项增加x
	while(i<=n) {
		c[i]+=x;/*不断找其祖先*/
		i+=bowbit(i);
	}
}

注意:树状数组能处理的下标为1…n的数组,绝对不能出现下标为0的情况。

为何说树状数组已经规划好了?
例如计算前7项的和
c[7]代表的是它本身a[7],
7-bowbit(7)=6,c[6]表示的是a[5]+a[6],
6-bowbit(6)=4,c[4]表示的是a[1]+a[2]+a[3+a[4]
一步步把前7项和准确定义出来。

扩展多维的树状数组:
有n*m的二维数组,树状数组为c

单点更新

int update(int x,int y,int z)   //(x,y)的值加上z
{
    int i=x;
    while(i<=n)
    {
        int j=y;
        while(j<=m)
        {
            c[i][j]+=z;
            j+=lowbit(j);
        }
        i+=lowbit(i);
    }
}

查询前缀和

int sum(int x,int y)
{
    int res=0,i=x;
    while(i>0)
    {
        int j=y;
        while(j>0)
        {
            res+=c[i][j];
            j-=lowbit(j);
        }
        i-=lowbit(i);
    }
    return res;
}

在这里插入图片描述

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

zaiyang遇见

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值