Loading

「学习笔记」一维树状数组

树状数组是一种查询和修改都是 $O_{logn} $ 的数据结构
树状数组可以做到单点修改区间查询,单点查询区间修改和区间修改区间查询
树状数组也分一维和二维

由此可以看出
\(C_1 =A_1\)
\(C_2 = A_1 + A_2\)
\(C_3 = A_3\)
\(C_4 = A_1 + A_2 + A_3 + A_4\)
\(C_5 = A_5\)
\(C_6 = A_5 + A_6\)
\(C_7 = A_7\)
\(C_8 = A_1 + A_2 + A_3 + A_4 + A_5 + A_6 + A_7 + A_8\)
\(......\)

要时刻记着每个点是怎么来的,否则后面容易混!!!

设节点编号为 $n$,那么,它所管辖的区域就是 $2^j$($j$ 是 $n$ 的二进制形式的末尾 $0$ 的个数) 子节点找父节点就是加上最后一个 $1$

\(e.g.\)
\(1->2\)—— \(0001 + 0001 = 0010\)
\(2->4\)—— \(0010 + 0010 = 0100\)
\(3->4\)—— \(0011 + 0001 = 0100\)
那现在问题来了,怎么求最后一个 \(1\)
通常,我们把它叫做 \(\text{lowbit(n)}\)
计算方法 n & (-n)
&: 位运算符,按位与,只有参与运算的两个二进制数所对应的二进制位均为 \(1\) ,结果对应位才为 \(1\) ,否则为 \(0\)

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

单点修改,区间查询:

问题:在 \(x\) 位置加上 \(y\),求 \(a\)\(b\)的区间和。
如果 \(x\) 位置加上 \(y\),那么,它的父节点也要加上 \(y\),查询父节点位置前面已经提到了,这个操作叫单点修改。
单点修改代码:

void add(ll x,ll y)//数组x位置加y 
{
	for(rll i=x;i<=n;i+=(i&(-i)))
	{
		s[i]+=y;//逐层向上修改(毕竟子节点改了,父节点也要改) 
	}
}

建树的过程,就可以看作是单点修改。
区间查询,就是找类兄弟
\(e.g.\) (\(sum\) 可以看作前缀和)
\(sum_5 = S_5 + S_4 + 0\)
\(S_5:0101\)
\(S_4:0100\)
\(0:0000\)
\(sum_7 = S_7 + S_6 + S_4 + 0\)
\(S_7:0111\)
\(S_6:0110\)
\(S_4:0100\)
\(0:0000\)
发现了吗,找类兄弟就是减去最后一个 \(1\) ,直至没有 \(1\) 为止
区间查询代码:

ll sum(ll x)
{
    ll ans=0;
    for(rll i=x;i;i-=(lowbit(i)))
    {
        ans+=s[i];
    }
    return ans;
}

单点修改,区间查询完整代码(有注释):

#include<bits/stdc++.h>
#define ll long long
#define rll register long long
using namespace std;
ll n,m,a;
ll s[1000100];
void add(ll x,ll y)//数组x位置加y
{
    for(rll i=x;i<=n;i+=(i&(-i)))
    {
        s[i]+=y;//逐层向上修改(毕竟子节点改了,父节点也要改)
    }
}
ll sum(ll x)
{
    ll ans=0;
    for(rll i=x;i;i-=(i&(-i)))
    {
        ans+=s[i];//求和,找类兄弟
    }
    return ans;
}
int main()
{
    scanf("%lld%lld",&n,&m);
    for(rll i=1;i<=n;++i)
    {
        scanf("%lld",&a);
        add(i,a);//建树过程,把操作之前的树上的点都看作是0
    }
    for(rll p,l,r,i=1;i<=m;++i)
    {
        scanf("%lld%lld%lld",&p,&l,&r);
        if(p==1)
        {
            add(l,r);//在l位置加上r
        }
        else
        {
            printf("%lld\n",sum(r)-sum(l-1));//查询区间[l-r]
        }
    }
    return 0;
}

区间修改,单点查询:

区间修改主要运用差分思想
注意,后面的内容必须会差分才可以看懂,不会差分的请止步。
区间修改代码(差分):

void add(ll x,ll y)
{
    for(rll i=x;i<=n;i+=(i&(-i)))
    {
        s[i]+=y;
    }
}

单点查询,运用差分求和的思想,\([1-i]\) 的差分和就是第i个元素的数值
单点修改代码:

ll sum(ll x)
{
    ll ans=0;
    for(rll i=x;i;i-=(i&(-i)))
    {
        ans+=s[i];//利用差分的思想求和,1-i的差分数组的和就是第i个数的值
    }
    return ans;
}

区间修改,单点查询完整代码(有注释):

#include<bits/stdc++.h>
#define ll long long
#define rll register long long
using namespace std;
ll n,m,last,a;
ll s[1000100];
void add(ll x,ll y)//差分数组x的位置加y
{
    for(rll i=x;i<=n;i+=(i&(-i)))
    {
        s[i]+=y;
    }
}
ll sum(ll x)//求第x个数的数值
{
    ll ans=0;
    for(rll i=x;i;i-=(i&(-i)))
    {
        ans+=s[i];//利用差分的思想求和,1-i的差分数组的和就是第i个数的值
    }
    return ans;
}
int main()
{
    scanf("%lld%lld",&n,&m);
    for(rll i=1;i<=n;++i)
    {
        scanf("%lld",&a);
        add(i,a-last);//记录差分数组
        last=a;//更新last,记录差分
    }
    for(rll p,l,r,j,i=1;i<=m;++i)
    {
        scanf("%lld",&p);
        if(p==1)
        {
            scanf("%lld%lld%lld",&l,&r,&j);//在区间[l-r]中,每一个元素加j
            add(l,j);//差分数组s[l]+j
            add(r+1,-j);//差分数组s[r+1]-j
        }
        else
        {
            scanf("%lld",&j);
            printf("%lld\n",sum(j));//求点j的值,利用差分求和
        }
    }
    return 0;
}

区间修改,区间查询:

这里的区间修改依然是差分思想,但求的是区间和,因此,与上面的有不同

区间和就是这个区间的数都加一遍, \(d(j)\) 是差分数组,差分数组从 \(1\)\(i\) 的和就是 \(a(i)\)

这里要自己亲自加一遍试试,全加起来,就会发现,\(d(1),d(2),d(3)...\) 被加的个数有规律,最后用分配律,就是最终公式了。

公式一定要记住!!!

由此可见,区间修改,区间查询需要多维护一个 $d(i) \times i$ 的和值

区间修改代码:

void add(ll x,ll y)//差分数组x位置的数加y
{
    for(rll i=x;i<=n;i+=(i&(-i)))
    {
        s1[i]+=y;//记录差分
        s2[i]+=x*y;//记录差分的和
    }
}

区间查询代码:

ll sum(ll x)//求前缀和(准确来说也不是前缀和,但性质差不多)
{
    ll ans=0;
    for(rll i=x;i;i-=(i&(-i)))
    {
        ans+=(x+1)*s1[i]-s2[i];//根据公式计算
    }
    return ans;
}

scanf("%lld%lld",&l,&r);//求区间(l-r)的和
printf("%lld\n",sum(r)-sum(l-1));//前缀和计算

区间修改,区间查询完整代码(有注释):

#include<bits/stdc++.h>
#define ll long long
#define rll register long long
using namespace std;
ll n,m,a,last;
ll s1[1000100],s2[1000100];
// s1 记录差分树状数组(公式第一项) s2 记录差分数组×i的和 (公式第二项)
void add(ll x,ll y)//差分数组x位置的数加y
{
    for(rll i=x;i<=n;i+=(i&(-i)))
    {
        s1[i]+=y;//记录差分
        s2[i]+=x*y;//记录差分的和
    }
}
ll sum(ll x)//求前缀和(准确来说也不是前缀和,但性质差不多)
{
    ll ans=0;
    for(rll i=x;i;i-=(i&(-i)))
    {
        ans+=(x+1)*s1[i]-s2[i];//根据公式计算
    }
    return ans;
}
int main()
{
    scanf("%lld%lld",&n,&m);
    for(rll i=1;i<=n;++i)
    {
        scanf("%lld",&a);
        add(i,a-last);//a-last 计算差分
        last=a;//更新last,使得记录的都是后一个数和前一个数的差
    }
    for(rll p,l,r,j,i=1;i<=m;++i)
    {
        scanf("%lld",&p);//判断是修改还是查询
        if(p==1)
        {
            scanf("%lld%lld%lld",&l,&r,&j);//l-r区间的元素加j
            add(l,j);//差分数组s1[l]+j
            add(r+1,-j);//差分数组s1[r+1]-j
        }
        else
        {
            scanf("%lld%lld",&l,&r);//求区间(l-r)的和
            printf("%lld\n",sum(r)-sum(l-1));//前缀和计算
        }
    }
    return 0;
}

喜欢的话,还请支持一下吧,写的不好,dalao 勿喷QWQ

posted @ 2022-06-28 11:50  yi_fan0305  阅读(74)  评论(0编辑  收藏  举报