树状数组 学习笔记

定义

树状数组(Binary Indexed Tree,BIT)是一种数据结构,可以高效地维护一些单点修改的问题。搬一张 OI-wiki 的图:

设原数组为 \(a\),树状数组为 \(c\),有:

\[\begin{aligned}&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\\&\vdots \end{aligned} \]

引入一种新的运算 \(\operatorname{lowbit}\) 表示取一个数二进制最低位的 \(1\),如 \(\operatorname{lowbit}((10100)_2)=(00100)_2,\operatorname{lowbit}((10001)_2)=(00001)_2\),则树状数组中,\(c_i\) 表示区间 \([i-\operatorname{lowbit}(i)+1,i]\)

\(\operatorname{lowbit}\) 有一个经典的求法是 x&-x。这是由于 \(-x\) 的补码是 \(x\) 取反再加一,而一个数加一相当于取反最低位 \(1\) 与更低位的 \(0\),那么 x&-x 只保留最低位的一。

普通树状数组

单点修改,区间查询

对一个点修改,显然只影响其祖先。因此可以由叶节点不断向上跳,每次加上 \(\operatorname{lowbit}\)

for(;x<=n;x+=(x&-x))tr[x]+=k;

区间查询可以差分成两段前缀和。利用树状数组的结构,如果区间 \([1,r]\),可以每次对 \(r\)\(\operatorname{lowbit}\) 并加上树状数组上对应的节点。

for(;x;x-=(x&-x))ans+=tr[x];

另外如果修改变成减 \(\operatorname{lowbit}\),查询变成加 \(\operatorname{lowbit}\),就成了后缀树状数组,查询的是后缀和。这相当于把 \(c_i\) 代表的区间变成 \([i,i+\operatorname{lowbit}-1]\)

建树

最简单的想法是执行 \(n\) 次单点修改,时间复杂度 \(O(n\log n)\)

有两种方法可以 \(O(n)\) 建树:

  1. \(c_i\) 表示区间 \([i-\operatorname{lowbit}(i)+1,i]\),可以前缀和直接算。
for(int i=1;i<=n;i++)a[i]+=a[i-1],tr[i]=a[i]-a[i-(i&-i)];
  1. 每一个结点会贡献给父节点,可以计算贡献。
for(int i=1;i<=n;i++){
   tr[i]+=a[i];
   if(i+(i&-i)<=n)tr[i+(i&-i)]+=tr[i];
}

区间修改,单点查询

对原数组进行差分。那么修改 \([l,r]\) 变为 \(a_l\gets a_l+k,a_{r+1}\gets a_{r+1}-k\),查询 \(x\) 处的值变为查询 \(\sum_{i=1}^n a_i\)。这是一个单点修改,区间查询问题。

区间修改,区间查询

记原数组 \(a\) 的差分数组为 \(b\),则 \(a_i=\sum_{j=1}^ib_j\)

\[\begin{aligned}\sum_{i=1}^ra_i&=\sum_{i=1}^r\sum_{j=1}^ib_j\\&=\sum_{i=1}^r(r-i+1)b_i\\&=\sum_{i=1}^r rb_i-\sum_{i=1}^r(i-1)b_i\\&=r\sum_{i=1}^rb_i-\sum_{i=1}^r(i-1)b_i\end{aligned} \]

同时维护 \(b_i,ib_i\) 即可。

二维树状数组

单点修改,区间查询

二维树状数组是一种树套树,外层树状数组的每一个结点都是一个树状数组,维护的是二维前缀和。

单点加时双重循环,先遍历外层的树状数组,对于每个外层的节点再遍历内层。

for(int i=x;i<=n;i+=(i&-i))for(int j=y;j<=m;j+=(j&-j))tr[i][j]+=k;

区间查询同理。

for(int i=x;i;i-=(i&-i))for(int j=y;j;j-=(j&-j))ans+=tr[i][j];

将询问差分,答案为 \(sum_{c,d}-sum_{a-1,d}-sum_{c,b-1}+sum_{a-1,b-1}\)

区间修改,单点查询

类似一维树状数组,使用差分把问题转化为单点修改,区间查询。修改变为 add(c+1,d+1,k),add(a,d+1,-k),add(c+1,b,-k),add(a,b,k),查询时直接取该位置的前缀和。

区间修改,区间查询

维护原数组 \(a\) 的差分数组 \(b\)\(b\) 作两次前缀和就变成了 \(a\) 的前缀和。

\[\begin{aligned}\sum_{i=1}^x\sum_{j=1}^y a_{i,j}&=\sum_{i=1}^x\sum_{j=1}^y\sum_{k=1}^i\sum_{l=1}^j b_{i,j}\\&=\sum_{i=1}^x\sum_{j=1}^y(x-i+1)(y-j+1)b_{i,j}\\&=xy\sum_{i=1}^x\sum_{j=1}^y b_{i,j}-y\sum_{i=1}^x\sum_{j=1}^y (i-1)b_{i,j}-x\sum_{i=1}^x\sum_{j=1}^y (j-1)b_{i,j}+\sum_{i=1}^x\sum_{j=1}^y (i-1)(j-1)b_{i,j}\end{aligned} \]

维护 \(b_{i,j},(i-1)b_{i,j},(j-1)b_{i,j},(i-1)(j-1)b_{i,j}\)

树状数组上倍增

树状数组也可以二分。初始时位置 \(x=0\),从大到小枚举 \(2^i\),如果加上区间 \([x+1,x+2^i]\) 还是满足条件,就将 \(x\) 往后跳 \(2^i\) 并合并这个区间的信息,最终的 \(x\) 就是最后一个满足条件的位置。而区间 \([x+1,x+2^i]\) 正是 \(x+2^i\) 的信息,因为 \(x\) 加上的位都比 \(2^i\) 大,\(\operatorname{x+2^i}=2^i\)

如果从某个位置开始二分也类似,让 \(x\) 小于左端点时一直加即可。

P6619

将温度离散化。如果温度为下标,可用的冰系战士是一段前缀,火系是一段后缀,能量和都是单调的。而双方消耗的总能量是两边能量的最小值乘二,这是一个单峰函数。

那么可能的答案在峰顶两侧。左侧的答案就是最后一个冰系能量小于火系能量的位置。对于右侧,先找到第一个冰系能量大于等于火系能量的位置(即左侧的答案加一)。由于可能有若干位置火系能量不变,依题意找出最晚的火系能量相同的位置。

用树状数组维护冰系火系的能量,可以树状数组上倍增。

#include<bits/stdc++.h>
using namespace std;
char buf1[2097152],*ip1=buf1,*ip2=buf1;
inline int getc(){
  return ip1==ip2&&(ip2=(ip1=buf1)+fread(buf1,1,2097152,stdin),ip1==ip2)?EOF:*ip1++;
}
template<typename T>void in(T &a)
{
  T ans=0;
  char c=getc();
  for(;c<'0'||c>'9';c=getc());
  for(;c>='0'&&c<='9';c=getc())ans=ans*10+c-'0';
  a=ans;
}
template<typename T,typename... Args>void in(T &a,Args&...args)
{
  in(a),in(args...);
}
int n,temp[2000005],cnt;
long long tr0[2000005],tr1[2000005],sum;
struct query{
  int opt,t,x,k;
  long long y;
}q[2000005];
void add(long long tr[],int x,long long k){
  for(;x<=n;x+=(x&-x))tr[x]+=k;
}
long long query(long long tr[],int x,long long ans=0){
  for(;x;x-=(x&-x))ans+=tr[x];
  return ans;
}
int main(){
  in(n);
  for(int i=1;i<=n;i++){
    in(q[i].opt);
    if(q[i].opt==1)in(q[i].t,q[i].x,q[i].y),temp[++cnt]=q[i].x;
    else in(q[i].k);
  }
  sort(temp+1,temp+cnt+1),cnt=unique(temp+1,temp+cnt+1)-temp-1;
  for(int i=1;i<=n;i++)if(q[i].opt==1)q[i].x=lower_bound(temp+1,temp+cnt+1,q[i].x)-temp;
  for(int i=1;i<=n;i++){
    if(q[i].opt==1){
      if(q[i].t)add(tr1,q[i].x+1,q[i].y),sum+=q[i].y;
      else add(tr0,q[i].x,q[i].y);
    }
    else{
      if(q[q[i].k].t)add(tr1,q[q[i].k].x+1,-q[q[i].k].y),sum-=q[q[i].k].y;
      else add(tr0,q[q[i].k].x,-q[q[i].k].y);
    }
    int p1=0,p2=0;
    long long sum0=0,sum1=sum,ans1=0,ans2=0;
    for(int i=20;i>=0;i--){
      p1+=1<<i;
      if(p1<=cnt&&sum0+tr0[p1]<sum1-tr1[p1])sum0+=tr0[p1],sum1-=tr1[p1];
      else p1-=1<<i;
    }
    ans1=sum0,sum0=0,sum1=sum;
    if(p1<cnt){
      ans2=min(query(tr0,p1+1),sum-query(tr1,p1+1));
      for(int i=20;i>=0;i--){
        p2+=1<<i;
        if(p2<=cnt&&(p2<=p1||min(sum0+tr0[p2],sum1-tr1[p2])==ans2))sum0+=tr0[p2],sum1-=tr1[p2];
        else p2-=1<<i;
      }
    }
    if(!ans1&&!ans2)puts("Peace");
    else if(ans1>ans2)cout<<temp[p1]<<' '<<2*ans1<<'\n';
    else cout<<temp[p2]<<' '<<2*ans2<<'\n';
  }
  return 0;
}

[[数据结构]]

posted @ 2024-03-01 09:29  lgh_2009  阅读(7)  评论(0)    收藏  举报