(转)POJ 3468 A Simple Problem with Integers --- 树状数组延迟标记

题意:
有一个数组,有两种操作。1: Q a b 求[a,b]的和 2:C a b c 给[a,b] 的所有元素都加上c。

题目:
You have N integers, A1, A2, ... , AN. You need to deal with two kinds of operations. One type of operation is to add some given number to each number in a given interval. The other is to ask for the sum of numbers in a given interval.

Input
The first line contains two numbers N and Q. 1 ≤N,Q ≤ 100000.
The second line contains N numbers, the initial values of A1, A2, ... , AN. -1000000000 ≤ Ai ≤ 1000000000.
Each of the next Q lines represents an operation.
"C a b c" means adding c to each of Aa, Aa+1, ... , Ab. -10000 ≤ c ≤ 10000.
"Q a b" means querying the sum of Aa, Aa+1, ... ,Ab.

Output
You need to answer all Q commands in order. One answer in a line.

Sample Input
10 5
1 2 3 4 5 6 7 8 9 10
Q 4 4
Q 1 10
Q 2 4
C 3 6 3
Q 2 4

Sample Output
4
55
9
15

Hint
The sums may exceed the range of 32-bit integers.

题解:
区间求和加区间更新,典型的带懒惰标记的线段树。懒惰标记:更新的时候 并不将每个元素都更新,而是将要更新的区间打上懒惰标记,等下次要使用的时候 再向下更新。

线段树的基本思想:二分,线段树是一棵二叉搜索树,它储存的是一个区间的信息

算法思想:

1 。线段树的成段更新--延迟更新;

2。区间查询和更新的时候加入一个延迟节点;

3 。每次要在下次查询或者更新到该区间时;

4 。再把节点的信息传递到左右孩子的结点上; 这样更新大大减少了时间和空间上的开销;

算法过程: 1 。每次更新并不需要更新到叶节点;

2 。只需更新到相应的段就可以了,然后记录个add(懒惰标记);

3 。下次更新或者查询的时候,如果要查到该段的子节点;

4 。就把add加到子节点上去,再将该add设为0; 这样查询子区间的复杂度就是更新的复杂度; 线段树的基础操作主要有5个:建树、单点查询、单点修改、区间查询、区间修改。*/

有两种解法:线段树和树状数组
—,线段树
/为了实现这个,引入一个新的状态——懒标记。

1、直观理解:“懒”标记,懒嘛!用到它才动,不用它就睡觉。

2、作用:存储到这个节点的修改信息,暂时不把修改信息传到子节点。就像家长扣零花钱,你用的时候才给你,不用不给你。 3、实现思路(重点):a.原结构体中增加新的变量,存储这个懒标记。 b.递归到这个节点时,只更新这个节点的状态,并把当前的更改值累积到标记中。 注意是累积, c.什么时候才用到这个懒标记?当需要递归这个节点的子节点时,标记下传给子节 点。这里不必管用哪个子节点,两个都传下去。

d.下传操作: ①当前节点的懒标记累积到子节点的懒标记中。 ②修改子节点状态。在引例中,就是原状态+子节点区间点的个数*父节点传下来的懒标记。 这就有疑问了,既然父节点都把标记传下来了,为什么还要乘父节点的懒标记,乘自 己的不行吗? 因为自己的标记可能是父节点多次传下来的累积,每次都乘自己的懒标记造成重复累积 ③父节点懒标记清0。这个懒标记已经传下去了,不清0后面再用这个懒标记时会重复 下传。

需要注意:1,懒惰标记数组一定要初始化。2. 数据范围*4.
#include<cstdio>
#include<cstring>
#include<algorithm>
#define inf 0x3f3f3f3f
#define ll long long
#define N 400010 //线段树开4倍空间,有时需要离散化让空间压缩
using namespace std;
ll Sum[N],add[N];//add 为懒惰标记,用到它才动,不用它就睡觉
void build(int l,int r,int o)
{
    if(l==r)/*如果是叶子节点,存储要维护的信息*/
    {
        scanf("%lld",&Sum[o]);
        return;
    }
    int mid=(l+r)>>1;/*对于二分到的每一个结点,给它的左右端点确定范围。*/
    build(l,mid,o<<1);
    build(mid+1,r,o<<1|1);/*care 走左儿子mid+1*/
    Sum[o]=Sum[o<<1]+Sum[o<<1|1];//状态合并,把当前结点的信息更新到父结点
}
void pushdown(int o,int l)//把当前结点的信息更新给儿子结点
{
    if(add[o])//懒惰标记下移
    {/*such as 1—>7,变为1—>4{4-1+1}=(7-(7>>1))和5—>7{7-5+1}=(7>>1)*/
        add[o<<1]+=add[o];
        add[o<<1|1]+=add[o];
        Sum[o<<1]+=add[o]*(l-(l>>1)); //左子树长度等于总长度减去右子树长度
        Sum[o<<1|1]+=add[o]*(l>>1); //向下取整  右子树的长度小于等于左子树的长度
        add[o]=0;
    }
}
void update(int x,int y,int l,int r,int o,int c)//区间更新
{
    if(l>=x&&r<=y)
    {
        Sum[o]+=c*(r-l+1);
        add[o]+=c;/*求和+=,若对值覆盖=*/
        return;
    }
    pushdown(o,r-l+1);
    int mid=(l+r)>>1;
    if(x<=mid)update(x,y,l,mid,o<<1,c);
    if(y>mid)update(x,y,mid+1,r,o<<1|1,c);
    Sum[o]=Sum[o<<1]+Sum[o<<1|1];/*对节点数值进行维护*/
}
ll query(int x,int y,int l,int r,int o)//区间查询
{
    if(l>=x&&r<=y)/*用>=和<=,因为可能我们随机生成的数,不在树节点维护的区间上,若==导致死循环*/
        return Sum[o];
    pushdown(o,r-l+1);
    int mid=(l+r)>>1;
    ll sum=0;
    if(x<=mid)sum=query(x,y,l,mid,o<<1);
    if(y>mid)sum+=query(x,y,mid+1,r,o<<1|1);/*节点的数值等于左儿子加上右儿子的和*/
    return sum;
}
int main()
{
    int n,m;
    while(~scanf("%d%d",&n,&m))
    {
        memset(Sum,0,sizeof(Sum));
        memset(add,0,sizeof(add));//将懒惰标记的值初始化为0
        build(1,n,1);
        while(m--)
        {
            char str[10];
            int l,r,c;
            scanf("%s",str);
            if(str[0]=='Q')
            {
                scanf("%d%d",&l,&r);
                printf("%lld\n",query(l,r,1,n,1));
            }
            else
            {
                scanf("%d%d%d",&l,&r,&c);
                update(l,r,1,n,1,c);/*也可开结构体记录1节点下标,节点维护区间*/
            }
        }
    }
}
二,树状数组
区间更新
这就是第一个问题,如果题目是让你把x-y区间内的所有值全部加上k或者减去k,
然后查询操作是问某个点的值,这种时候该怎么做呢。如果是像上面的树状数组
来说,就必须把x-y区间内每个值都更新,这样的复杂度肯定是不行的,这个时候
,就不能再用数据的值建树了,这里我们引入差分,利用差分建树。
假设我们规定A[0] = 0;
则有 A[i] = Σij = 1D[j];(D[j] = A[j] - A[j-1]),即前面i项的差值和,这个有
什么用呢?例如对于下面这个数组
A[] = 1 2 3 5 6 9
D[] = 1 1 1 2 1 3
如果我们把[2,5]区间内值加上2,则变成了
A[] = 1 4 5 7 8 9
D[] = 1 3 1 2 1 1
发现了没有,当某个区间[x,y]值改变了,区间内的差值是不变的,只有D[x]和D[y+1]的值发生改变
所以我们就可以利用这个性质对D[]数组建立树状数组,
区间查询
上面我们说的差值建树状数组,得到的是某个点的值,那如果我既要区间更新,又
要区间查询怎么办。这里我们还是利用差分,由上面可知
则A[1]+A[2]+...+A[n]
= (D[1]) + (D[1]+D[2]) + ... + (D[1]+D[2]+...+D[n])
= n*D[1] + (n-1)*D[2] +... +D[n]
= n * (D[1]+D[2]+...+D[n]) - (0*D[1]+1*D[2]+...+(n-1)*D[n])
如果你理解前面的都比较轻松的话,这里也就知道要干嘛了,维护两个数状数组,
sum1[i] = D[i],sum2[i] = D[i]*(i-1);
#include<iostream>
#include<string.h>
#include<string>
#define kk 100010
using namespace std;
int n,m;
long long a[kk],c[kk];///对应原数组和树状数组[表示差值](D[1] + D[2] + ... + D[n])
long long w[kk]; ///树状数组【要剪的差值(根据推出的公式)】(0*D[1] + 1*D[2] + ... + (n-1)*D[n])
void updata(int i,long long k)     ///在i位置加上k
{
    int x=i;//因为x不变,所以得先保存i值
    for(; i <= m; i+=i&(-i))
    {
        c[i] += k;
        w[i]+=k*(x-1);
    }
}
long long query(int i)         ///求A[1 - i]的和(前缀和)
{
    long long res = 0;
    int x=i;
    for(; i > 0; i-=i&(-i))res += x*c[i]-w[i];
    return res;
}
int main()
{
    std::ios::sync_with_stdio(false);
    cin.tie(0);
    cout.tie(0);
    while(cin>>m>>n)
    {
        memset(a, 0, sizeof a);
        memset(c, 0, sizeof c);
        memset(w,0,sizeof(w));
        for(int i = 1; i <= m; i++)
        {
            cin>>a[i];
            updata(i,a[i]-a[i-1]);   ///输入初值的时候,也相当于更新了值
        }
        string s;
        int x,y;
        long long z;
        while( n--)
        {
            cin>>s;
            if(s[0] == 'Q')     ///求和操作
            {
                cin>>x>>y;
                long long sum = query(y) - query(x-1);    //x-y区间和也就等于1-y区间和减去1-(x-1)区间和
                cout << sum << endl;
            }
            else if(s[0] == 'C')  ///[x,y]区间内加上z
            {
                cin>>x>>y>>z;
                updata(x,z);///A[x] - A[x-1]增加z
                updata(y+1,-z); ///A[y+1] - A[y]减少z
            }
        }
    }
    return 0;
}
 
这里利用的负数的存储特性,负数是以补码存储的,对于整数运算 x&(-x)有
● 当x为0时,即 0 & 0,结果为0;
●当x为奇数时,最后一个比特位为1,取反加1没有进位,故x和-x除最后一位外前
面的位正好相反,按位与结果为0。结果为1。
●当x为偶数,且为2的m次方时,x的二进制表示中只有一位是1(从右往左的第m+1
位),其右边有m位0,故x取反加1后,从右到左第有m个0,第m+1位及其左边全是1
。这样,x& (-x) 得到的就是x。
●当x为偶数,却不为2的m次方的形式时,可以写作x= y * (2^k)。其中,y的最低
位为1。实际上就是把x用一个奇数左移k位来表示。这时,x的二进制表示最右边有
k个0,从右往左第k+1位为1。当对x取反时,最右边的k位0变成1,第k+1位变为0
;再加1,最右边的k位就又变成了0,第k+1位因为进位的关系变成了1。左边的位
因为没有进位,正好和x原来对应的位上的值相反。二者按位与,得到:第k+1位上
为1,左边右边都为0。结果为2^k。
总结一下:x&(-x),当x为0时结果为0;x为奇数时,结果为1;x为偶数时,结果为
x中2的最大次方的因子。
而且这个有一个专门的称呼,叫做lowbit,即取2^k
————————————————
版权声明:本文为CSDN博主「zeng_jun_yv」的原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/zeng_jun_yv/article/details/98233508

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值