king_xbz
2020-10-09 10:17:48
树状数组,又称二叉索引树,是一种代码简单,应用广泛的神奇数据结构!今天我来带大家详细的了解一下这个神奇的数据结构!!!超级详细,不看后悔哦!
下面是目录!
话不多说,我们开始内容!
设黑色框内数组为
那么可以得到以下式子:
我们便可以得到
在这里,
那么,如何求出二进制中从最低位到高位连续零的长度呢?
我们需要找最低位的1!!!
如何找最低位的1呢?
我们需要引入lowbit
lowbit
inline int lowbit(int x)
{
return x&(-x);
}
&运算,即与运算,即按位比较都是1则为1,否则为0。
lowbit的原理简单说一下:
在计算机中二进制是以补码存储的。对于
总结一下规律:x&(-x),当x为0时结果为0;x为奇数时,结果为1;x为偶数时,结果为x中2的最大次方的因子。用处呢就是找最低位的1的位置。
那么我们树状数组的雏形就搭建好了,看看几个基础函数的用法吧。
(其实我们发现,树状数组其实就是个特殊的前缀和数组!)
add
inline void adds(int x,int y)
{
for(fint i=x;i<=n;i+=lowbit(i))
t[i]+=y;
return ;
}
query
inline int query(int x)
{
int tot=0;
for(fint i=x;i;i-=lowbit(i))
tot+=t[i];
return tot;
}
有了上面的模板,我们可以解决基础的实际性问题:
adds(x,y);
//在第x个数上加y
query(y)-query(x-1);
//求区间x~y的和(前缀和思想)。
【模板】树状数组 1
代码:
signed main()
{
n=read(),m=read();
nn=(n<<1);
for(fint i=1;i<=n;i++)
a[i]=read(),adda(i,a[i]),addb(i,a[i]*i);
for(fint i=1;i<=m;i++)
{
int x,y;
scanf("%s",s+1);
if(s[1]=='Q')
x=read(),cout<<((x+1)*get_tota(x)-get_totb(x))<<endl;
else
x=read(),y=read(),adda(x,y-a[x]),addb(x,(y-a[x])*x),a[x]=y;
}
return 0;
}
inline int lowbit(int x)
{
return x&(-x);
}
inline void adda(int x,int y)
{
for(fint i=x;i<=p;i+=lowbit(i))
trea[i]+=y;
return ;
}
inline void addb(int x,int y)
{
for(fint i=x;i<=p;i+=lowbit(i))
treb[i]+=y;
return ;
}
inline int get_tota(int x)
{
int ans=0;
for(fint i=x;i;i-=lowbit(i))
ans+=trea[i];
return ans;
}
inline int get_totb(int x)
{
int ans=0;
for(fint i=x;i;i-=lowbit(i))
ans+=treb[i];
return ans;
}
知道这个,我们就可以求逆序对了,先卖个关子,后面部分有,接着看吧!
差分就是将数列中的每一项分别与前一项数做差,例如:
原序列 | 1 | 2 | 5 | 4 | 7 | 3 | \ |
---|---|---|---|---|---|---|---|
差分序列 | 1 | 1 | 3 | -1 | 3 | -4 | -3 |
观察表格,我们可以得到以下规律:
差分序列第一个数和原来的第一个数一样 |
---|
差分序列最后比原序列多一个数,即最后一个数的相反数 |
差分序列求前缀和可得原序列 |
将原序列区间[L,R]中的元素全部+1,可以转化操作为差分序列L处+1,R+1处-1 |
根据规律3我们可以得到(
for(fint i=1;i<=n;i++)
s[i]=s[i-1]+c[i];
s[i]=a[i];
根据上文规律4我们可以得到区间修改代码(设区间为
adds(l,x),adds(r+1,-x);
query(x)
//求出第x个数的值
我们设树状数组
那么我们的
这样的话,我们维护两个数组->
ta数组维护a的前缀和,tb数组维护
ta[i]=a[i]-a[i-1];
tb[i]=ta[i]*(i-1);
adds
那么
即
我们更新
inline void adds(int x,int y)
{
for(fint i=x;i<=n;i+=lowbit(i))
ta[i]+=y,tb[i]+=y*(x-1);
return ;
}
query
求和公式
inline int query(int x)
{
int tot=0;
for(fint i=x;i;i-=lowbit(i))
tot+=x*ta[i]-tb[i];
return tot;
}
如此状况下
单点修改:
区间修改(l,r)
单点查询:
区间查询
【模板】线段树 1
树状数组版代码:
signed main()
{
n=read(),m=read();
for(fint i=1;i<=n;i++)
a[i]=read(),adds(i,a[i]-a[i-1]);
for(fint i=1;i<=m;i++)
{
int b,c,d,e;
b=read();
if(b==1)
c=read(),d=read(),e=read(),adds(c,e),adds(d+1,-e);
else
if(b==2)
c=read(),d=read(),cout<<query(d)-query(c-1)<<endl;
}
return 0;
}
inline int lowbit(int x)
{
return x&(-x);
}
inline void adds(int x,int y)
{
for(fint i=x;i<=n;i+=lowbit(i))
ta[i]+=y,tb[i]+=y*(x-1);
return ;
}
inline int query(int x)
{
int tot=0;
for(fint i=x;i;i-=lowbit(i))
tot+=x*ta[i]-tb[i];
return tot;
}
回顾一下之前说到的一维差分:
差分与前缀和互为逆运算,即差分数组的前缀和数组为原数组,前缀和数组的差分数组为原数组.二者都利用了容斥原理.
那么二维差分呢?
我们先来看看二维差分的逆运算二维前缀和。
我们定义为以二维数组的首行首列(即左上角)元素为左上角,当前位置元素为右下角的矩阵的元素和.
我们要求黑色部分的和其实就是红色部分(当前位置值)加上绿色部分和紫色部分【
根据容斥原理,我们可以得到二维前缀和的式子:
如果我们想通过前缀和求得原数组(单个元素的值),就做差即可:
黑色块的值=红色块的值减去绿色和黄色块的值,橙色部分被减了两次,再加回来。
得到柿子:
众所周知,前缀数组还原成原数组需要的操作正是差分!
那么,推广到二维的容斥上,可得类似推论
那么修改其实也一样。如果想对黑色部分全部+1,那么在
看看代码:
如果想做这样的区间加减
for(fint i=la;i<=ra;i++)
for(fint j=lb;j<=rb;j++)
a[i][j]+=x;
使用差分的话,就是
a[la][ra]++,a[lb][rb]++;
a[la][rb+1]--,a[lb+1][ra]--;
也是个多退少补吧!
是不是很像,恰好是二维前缀和反过来了,这就是逆运算吧,i了i了。
lowbit不变
单差分修改,查询变为:
inline void adds(int x,int y,int z)
{
for(fint i=x;i<=n;i+=lowbit(i))
for(fint j=y;j<=m;j+=lowbit(j))
t[i][j]+=z;
return ;
}
inline int query(int x,int y)
{
int tot=0;
for(fint i=x;i;i-=lowbit(i))
for(fint j=y;j;j-=lowbit(j))
tot+=t[i][j];
return tot;
}
讲真,除了多出来一维之外和一维树状数组没毛区别。
就是把一维前缀和求差分变成了二维前缀和求差分(还原数组)
while(cin>>op)
{
int x,y,k;
if(op==1)
cin>>x>>y>>k,adds(x,y,k);
int a,b,c,d;
if(op==2)
cin>>a>>b>>c>>d,cout<<query(c,d)-query(a-1,d)-query(c,b-1)+query(a-1,b-1)<<endl;
}
其实就是个对差分数组求前缀和的操作,修改变得复杂,查询变得简单
while(cin>>op)
{
int a,b,c,d,k;
if(op==1)
cin>>a>>b>>c>>d>>k,adds(a,b,k),adds(a,d+1,-k),adds(c+1,b,-k),adds(c+1,d+1,k);
int x,y;
if(op==2)
cin>>x>>y,cout<<query(x,y)<<endl;
}
修改和查询都变得复杂,本质上是差分求前缀和再求前缀和的操作
先看公式推导
此时需要开4个操作数组:
inline void adds(int x,int y,int z)
{
for(fint i=x;i<=n;i+=lowbit(i))
for(fint j=y;j<=m;j+=lowbit(j))
ta[i][j]+=z,tb[i][j]+=(x-1)*z,tc[i][j]+=(y-1)*z,td[i][j]+=(x-1)*(y-1)*z;
return ;
}
inline int query(int x,int y)
{
int tot=0;
for(fint i=x;i;i-=lowbit(i))
for(fint j=y;j;j-=lowbit(j))
tot+=ta[i][j]*x*y-tb[i][j]*y-tc[i][j]*x+td[i][j];
return tot;
}
查询修改代码:
while(cin>>op)
{
int a,b,c,d,k;
if(op==1)
cin>>a>>b>>c>>d>>k,adds(a,b,k),adds(a,d+1,-k),adds(c+1,b,-k),adds(c+1,d+1,k);
int x,y;
if(op==2)
cin>>x>>y,cout<<query(x,y)<<endl;
}
树状数组本身就是一种类似树形的数据结构,一般来说只适合维护线性序列区间(或者矩阵区间)。对于树形结构我们有办法维护吗?
答案是肯定的!
但在此之前,我想先请大家了解几个小的前置知识。
首先请确保你已经完全理解了一维差分的内容和LCA的知识,知道差分是如何对区间进行修改的,并会使用倍增or树链剖分求LCA。如果不知道,请先仔细学习。
话不多说,开始讲述。
什么是树上差分?
树上差分就是对一条树链进行
树上差分有点差分和边差分两种
对于一条路径
为了方便设
我们把链拆分成两部分
代码:
ans[s]++,ans[t]++;
ans[LCA(s,t)]--,ans[Fa[LCA(s,t)]]--;
对于一条路径
设
如果我们还按照点差分的方法,那就会使回溯的步骤经过
我们分别对图中
也就是
这样就不会经过
此时代码
ans[s]++,ans[t]++;
ans[LCA(s,t)]--,ans[Fa[LCA(s,t)]]--;
那么我们就掌握了最基础的树上差分啦
dfs序,顾名思义就是dfs的顺序,在一棵树中我们记录它第一次被访问的时间和被回溯后的时间,这两者差的时间就是访问子树的时间,也可以说就是子树的大小啦。在DFS序的帮助下,我们可以将树转化为线性结构。然后使用其他数据结构(如树状数组维护)达到修改查询的目的。
我们将这棵树进行了DFS的处理操作,就使得其变成了如上的线性结构。
知道了这些,我们就可以往树上套树状数组了!
我们用dfs序记录时间戳,根据访问时间让结构变成线性的,然后在dfs序的位置是维护一个树状数组,设这个记录访问时间的数组为
addup(dfn[a],x)
而我们根据dfs序的性质可以知道初次访问时间-回溯时间=子树大小。我们可以记录回溯过来的时间,也可以简单dp一下记录子树和。这里先介绍一下第二种方法:
inline void dfs(int x,int fa)
{
dfn[x]=++tim;
siz[x]=1;
for(fint i=head[x];i;i=e[i].nxt)
if(e[i].to!=fa)
dfs(e[i].to,x),siz[x]+=siz[e[i].to];
return ;
}
那么查询很简单了
cout<<query(dfn[a]+siz[a]-1)-query(dfn[a]-1)<<endl
其他部分套树状数组模板即可
既然要修改一棵子树,那我们自然需要用到差分,在树上的一条链差分并不是v[i]-v[i-1]
那么简单。我们用
跑出dfs序
inline void dfs(int x,int fa)
{
dfn[x]=++tim;
siz[x]=1;
for(fint i=head[x];i;i=e[i].nxt)
if(e[i].to!=fa)
dfs(e[i].to,x),siz[x]+=siz[e[i].to];
return ;
}
由于我们在树状数组中维护的是
这是对树状数组的初始元素添加:
addup(dfn[i],v[i]-v[viss[dfn[i]-1]]);
求和的话就直接用树状数组维护回溯时间减去初次访问时间的权值即可
cout<<query(ou[a])-query(dfn[a]-1)<<endl;
这是这一部分的重难点!!!
首先链修改,修改的是点权,那么我们之前介绍的点差分就派上了用场,此时我们用差分树状数组维护一个点差分数组(好像在套娃)。大家应该还没忘记点差分的做法吧?!
addup(dfn[x],val,1),addup(dfn[lca],-val,1);
addup(dfn[y],val,1),addup(dfn[f[lca][0]],-val,1);
此时如果只有单点查询,那么树状数组直接求前缀和就可以帮助我们了:
只需要将回溯时间减去初次访问时间即可
query(ou[a],1)-query(dfn[a]-1,1)
目前这部分看起来就是将上一部分内容再差分了一步。
但是还有子树查询,这可怎么办?
有人可能会觉得用之前差分树状数组中的区间求和操作即可。但子树可能含有很多条链,这对于维护线性序列的树状数组来说十分困难。所以我们需要结合dfs序来处理。
我们可以考虑子树中节点对整个子树的贡献,画个图看一下
不难发现,同一子树中深度大的节点对深度浅的节点的子树和有贡献,贡献就是
上个代码:
addup(dfn[x],val*dep[x],2),addup(dfn[lca],-val*dep[lca],2);
addup(dfn[y],val*dep[y],2),addup(dfn[f[lca][0]],-val*dep[f[lca][0]],2);
那么查询就不难了
我们对以
(query(ou[a],2)-query(dfn[a]-1,2))-(query(ou[a],1)-query(dfn[a]-1,1))*(dep[x]-1)
这时的树状数组需要两个,分别维护两种操作。
inline void addup(int x,int y,int id)
{
for(fint i=x;i<=p;i+=lowbit(i))
t[i][id]+=y;
return ;
}
inline int query(int x,int id)
{
int tot=0;
for(fint i=x;i;i-=lowbit(i))
tot+=t[i][id];
return tot;
}
inline void dfs(int x,int fa)
{
dfn[x]=++tim;
for(fint i=head[x];i;i=e[i].nxt)
if(e[i].to!=fa)
dfs(e[i].to,x);
ou[x]=tim;
return ;
}
inline void add(int x,int y,int val)
{
int lca=LCA(x,y);
addup(dfn[x],val,1),addup(dfn[lca],-val,1);
addup(dfn[y],val,1),addup(dfn[f[lca][0]],-val,1);
addup(dfn[x],val*dep[x],2),addup(dfn[lca],-val*dep[lca],2);
addup(dfn[y],val*dep[y],2),addup(dfn[f[lca][0]],-val*dep[f[lca][0]],2);
return ;
}
好了,这样我们就可以用树状数组实现链修改+单点查询+子树求和啦!是不是很神奇呢?
你可能想不到,树状数组还有这么多功能!快来学习一下吧!
我们设树状数组
那么我们的
排序后:
用
此时c数组的下标代表着a数组的大小关系
通俗点说就是:
这样的话,
for(fint i=1;i<=n;i++)
adds(c[i],1),ans+=i-query(c[i]);
来道例题吧:
最接近神的人
求逆序对,要先对数据进行离散化
如样例:
2,8,0,3序号为1,2,3,4
排序后序号为3,1,4,2
离散化后
而我们要求的排序后序列为:
加入树状数组后即可求逆序对。
signed main()
{
cin>>n;
for(fint i=1;i<=n;i++)
cin>>a[i],num[i]=i;
stable_sort(num+1,num+n+1,cmp);
for(fint i=1;i<=n;i++)
a[num[i]]=i;
int ans=0;
for(fint i=1;i<=n;i++)
adds(a[i],1),ans+=(i-query(a[i]));
cout<<ans;
return 0;
}
inline int lowbit(int x)
{
return x&(-x);
}
inline void adds(int x,int y)
{
for(fint i=x;i<=n;i+=lowbit(i))
t[i]+=y;
return ;
}
inline int query(int x)
{
int tot=0;
for(fint i=x;i;i-=lowbit(i))
tot+=t[i];
return tot;
}
inline int cmp(int x,int y)
{
return a[x]<a[y];
}
可能你已经看出来了,树状数组离散化后求逆序对其实正是找区间
Enemy is weak
就是求
我们用两个树状数组,分别求从
代码:
signed main()
{
cin>>n;
for(fint i=1;i<=n;i++)
cin>>a[i];
for(fint i=1;i<=n;i++)
b[i]=a[i];
sort(b+1,b+n+1);
int len=unique(b+1,b+n+1)-b-1;
for(fint i=1;i<=n;i++)
a[i]=lower_bound(b+1,b+len+1,a[i])-b;
for(fint i=n;i>=1;i--)
l[i]+=query_a(a[i]-1),adds_a(a[i],1);
for(fint i=1;i<=n;i++)
r[i]+=query_b(n)-query_b(a[i]-1),adds_b(a[i],1);
int ans=0;
for(fint i=1;i<=n;i++)
ans+=l[i]*r[i];
cout<<ans;
return 0;
}
延申,再延申!
我们通过查找知道左边有几个比自己大的和右边有几个比自己大的,不就知道了排名吗?有平衡树那味儿了,hhh。
代码:
signed main()
{
cin>>n;
for(fint i=1;i<=n;i++)
cin>>a[i];
for(fint i=1;i<=n;i++)
b[i]=a[i];
sort(b+1,b+n+1);
int len=unique(b+1,b+n+1)-b-1;
for(fint i=1;i<=n;i++)
a[i]=lower_bound(b+1,b+len+1,a[i])-b;
for(fint i=n;i>=1;i--)
l[i]+=query_a(a[i]-1),adds_a(a[i],1);
for(fint i=1;i<=n;i++)
r[i]+=query_b(a[i]-1),adds_b(a[i],1);
int ans=0;
for(fint i=1;i<=n;i++)
rank[i]=l[i]+r[i]+1,cout<<rank[i]<<" ";
return 0;
}
再离散化后的有序数组内,知道了排名就相当于知道了前驱后继。
假如我们要查询第
mp[rank[x]]=i;
然后在
当然也可以像之前luogu日报里一样用倍增解决,这里不再赘述。
拉格朗日恒等式是18世纪由法国数学家约瑟夫·路易斯·拉格朗日提出的数学恒等式。
内容
也就是说,我们随便乱拆就可以把式子化简为:
这很显然啊!
我们可以倒推来证明嘛
所以呢,由此可知
这就是著名的拉格朗日恒等式的内容.
那么对于式子:
其实就等于
式子被分为三个部分,我们可以使用树状数组来维护。
代码:
p=read(),l_a=read(),r_a=read(),l_b=read(),r_b=read();
cout<<((1LL*query(0,l_a,r_a)*query(1,l_a,r_a)-Pow(query(2,l_a,r_a)))%mods+mods)%mods;
inline int Pow(int x)
{
return 1LL*x*x%mods;
}
inline int lowbit(int x)
{
return x&(-x);
}
inline void adds(int id,int x, int y)
{
y=(y+mods)%mods;
for(fint i=x,j=0;i<=n;i+=lowbit(i))
t[i][id]+=y,t[i][id]>mods?t[i][id]-=mods:j++;
return ;
}
inline int query(int id, int l, int r)
{
return qry(id,r)-qry(id,l-1);
}
inline int qry(int id,int x)
{
int tot=0;
for(fint i=x,j=0;i;i-=lowbit(i))
tot+=t[i][id],tot>mods?tot-=mods:j++;
return tot;
}
非常合理
神奇的树状数组也可以处理区间最值的问题
修改
浅显易懂,直接暴力求最值
inline void adds(int x,int y)
{
for(fint i=x;i<=n;i+=lowbit(i))
t[x]=max(t[x],y);
}
查询
若
我们可以发现[ r-lowbit(y)+1,r ]=tree[r]
否则的话原问就变成了:求 [x,y-1]中最大值 与 a[y] 的最大值。
inline void findx(intl,int r)
{
if(r-lowbit(r)>l)
return max(t[r],findx(l,y-lowbit(r)))
return max(a[y],findx(x,y-1));
}
看看这道题Balanced Lineup G
signed main()
{
cin>>n>>q;
int a,b,c;
memset(treeb,0x7f,sizeof(treeb));
for(fint i=1;i<=n;i++)
{
cin>>a;
f[i]=a;
addup(i,a);
}
for(fint i=1;i<=q;i++)
{
cin>>b>>c;
cout<<totmax(b,c)-totmin(b,c)<<endl;
}
return 0;
}
int totmax(int x, int y)
{
if(y>x)
{
if(y-lowbit(y)>x)
return max(treea[y],totmax(x,y-lowbit(y)));
else
return max(f[y],totmax(x,y-1));
}
return f[x];
}
int totmin(int x, int y)
{
if(y>x)
{
if(y-lowbit(y)>x)
return min(treeb[y],totmin(x,y-lowbit(y)));
else
return min(f[y],totmin(x, y-1));
}
return f[x];
}
inline void addup(int x,int y)
{
for(fint i=x;i<=n;i+=lowbit(i))
treea[i]=max(treea[i],y),treeb[i]=min(treeb[i],y);
}
inline int lowbit(int x)
{
return x&(-x);
}
接着延申
ST表经典题——静态区间最大值
会查区间最值了,自然就能解决ST表
直接上代码(尼古卡的严,只有92分):
signed main()
{
scanf("%d%d",&n,&q);
for(fint i=1;i<=n;i++)
scanf("%d",&a[i]),adds(i,a[i]);
int l,r;
for(fint i=1;i<=q;i++)
scanf("%d%d",&l,&r),printf("%d\n",totmax(l,r));
return 0;
}
inline int totmax(int x,int y)
{
if(y>x)
{
if(y-lowbit(y)>x)
return max(t[y],totmax(x,y-lowbit(y)));
else
return max(a[y],totmax(x,y-1));
}
return a[x];
}
inline void adds(int x,int y)
{
for(fint i=x;i<=n;i+=lowbit(i))
t[i]=max(t[i],y);
}
inline int lowbit(int x)
{
return x&(-x);
}
最后推荐几道简单的练习题给大家:
守墓人
Agent2
Haircut G
逆序对
火柴排队
删除物品
祝大家学习愉快哦!
一张图片选自Xenny的博客
二维树状数组区间修改区间查询公式推导图片摘自CSDN Lv1_kangdi
拉格朗日恒等式图片摘自百度百科
在此表示感谢!!!