数据结构知识点摘要
注意:本文适合已经学过该知识点的人快速复习记忆,有些原理会直接略过,初学请留步
ST表
RMQ离线查询,不支持修改
预处理\(O(n\log n)\) 查询\(O(1)\)
-
思想:倍增,设\(f(i,j)\)为区间\([i,i+2^j-1]\)的最大值
-
预处理:
\[j=1\rightarrow\log_2n,\ i+2^j-1\leq n \\ f(i,j)=\begin{cases} a_i,j=0 \\ \max(f(i,j-1),f(i+2^{j-1},j-1)),j\neq0 \end{cases} \] -
查询:
\[\text{令}s=\lfloor \log_2(r-l+1)\rfloor \\ Ans=\max(f(l,s),f(r-2^s+1,s)) \]
补充:
- ST表可以解决任何可重复贡献问题,指一个值被多次统计不会影响答案,正例有max, min, gcd, lcm等,反例有区间和等
- 建议预处理\(log_2\)的值
树状数组
单点修改区间查询,改为维护差分数组可以做到区间加区间和
修改\(O(\log n)\) 查询\(O(\log n)\)
单点修改区间查询
-
单点修改
void add(int pos,int v){ while(pos<=n){ c[pos]+=v; pos+=lowbit(pos); } }
-
区间查询
int getpresum(int pos){//获取前缀和 int res=0; while(pos){ res+=c[pos]; pos-=lowbit(pos); } return res; } cout<<getpresum(r)-getpresum(l-1)<<endl;//获取l到r的区间和
区间修改单点查询
也好办,让树状数组维护差分数组即可
-
区间修改
add(l,v); add(r+1,-v); //add函数与单点修改一致
-
单点查询
getpresum(x) //getpresum函数与区间查询一致
区间修改区间查询
依然使用差分数组,不过有变化,需要推下式子,以下求位置\(pos\)的前缀和,\(a\)为原数组,\(d\)为差分数组
因此,我们需要维护树状数组维护\(d[i]\)(代码中为diff
)和\(d[i]\times i\)(代码中为exdiff
)
整体代码如下
void SingleAdd(int pos,int k){
int _k=k*pos;
while(pos<=n){
diff[pos]+=k;
exdiff[pos]+=_k;
pos+=lowbit(pos);
}
}
void SectionAdd(int l,int r,int k){
SingleAdd(l,k);
SingleAdd(r+1,-k);
}
int SinglePreSum(int *t,int pos){
int ans=0;
while(pos>=1){
ans+=t[pos];
pos-=lowbit(pos);
}
return ans;
}
int SectionPreSum(int pos){
return SinglePreSum(diff,pos)*(pos+1)-SinglePreSum(exdiff,pos);
}
int SectionSum(int l,int r){
return SectionPreSum(r)-SectionPreSum(l-1);
}
解决RMQ问题(改版树状数组)
时间复杂度\(O(n\log^2 n)\)
-
区间查询
int getmax(int l,int r){ int res=numeric_limits<int>::min(); while(l<=r){ res=max(res,a[r--]); while(r-lowbit(r)>=l){ res=max(res,a[r]); r-=lowbit(r); } } }
-
单点修改
枚举受到影响的区间并修改(外循环),修改完需要重构(内循环)
void add(int pos,int v){ a[pos]=v; for(int i=x;i<=n;i+=lowbit(x)){ c[i]=a[i]; for(int j=1;j<lowbit(i);j<<=1){ c[i]=max(c[i],c[i-j]); } } }
求逆序对
时间复杂度\(O(n\log n)\)
构建权值树状数组,对于每个\(a[i]\),add(a[i],1)
,而它前面的逆序对就有getpresum(a[i]-1)
个
O(n)建树优化
看图理解
memset(c,0,sizeof(c));
for(int i=1;i<=n;i++){
cin>>a[i];
c[i]+=a[i];
if(i+lowbit(i)<=n) c[i+lowbit(i)]+=c[i];
}
补充:
经典树状数组可以维护的运算需要满足可逆运算性和结合律,可逆运算性指知道\(x\circ y\)和\(y\)后可以倒推出\(x\),如加法、乘法、异或等
线段树
线段树适用于很多区间操作,变式、操作、改版有很多,基本上时间复杂度都是\(O(\log n)\)的
线段树的思想可以主要概括为:分治+线段树+懒标记
基本线段树
基本线段树即为最基础的区间和、区间积、RMQ等等
因为对于不同的操作代码细节差异很多,这里仅用伪代码助于理解流程和思想
下面代码中lch
指左孩子,等价于pos*2
,rch
为右孩子,等价于pos*2+1
-
build 建树
build(pos,l,r): if(l==r)://如果是叶子节点 tree[pos]=arr[l] mid=(l+r)/2 build(lch,l,mid) build(rch,mid+1,r) push_up(pos)//见下文
-
push_up 向上合并
push_up(pos): merge(lch,rch) //merge为合并两个子节点的值,这里具体如何合并需要依据题目条件 //如:求区间最大值则代码为tree[pos]=max(tree[pos*2],tree[pos*2+1])
-
push_down 向下传递标记
push_down(pos,l,r): if(tag[pos] is valid)://如果有懒标记 mid=(l+r)/2 addtag(lch,l,mid,tag[pos]) addtag(rch,mid+1,r,tag[pos]) //addtag为给指定的节点(左右孩子)打标记,具体实现依据题目条件 //如tag表示的是区间加上一个值,则对应代码为 //addtag(pos,l,r,val): // tag[pos]+=val // tree[pos]+=(r-l+1)*val clear tag[pos]//清空懒标记
-
update 区间修改
update(pos,Nowl,Nowr,Secl,Secr,val)://Nowl和Nowr为现在所处的l和r,Secl和Secr为希望修改的区间 if(Nowl>Secr || Nowr<Secl): return if(Secl<=Nowl && Nowr<=Secr)://如果当前区间全部在选中的区间内 addtag(pos,Nowl,Nowr,val)//与上文中的相同 else: push_down(pos,Nowl,Nowr)//先下放标记再访问子节点 mid=(l+r)/2 if(Secl<=mid): update(lch,Nowl,mid,Secl,Secr,val) if(Secr>mid): update(rch,mid+1,Nowr,Secl,Secr,val) push_up(pos)
-
query 区间查询
和update相似
query(pos,Nowl,Nowr,Secl,Secr)://Nowl和Nowr为现在所处的l和r,Secl和Secr为希望修改的区间 if(Nowl>Secr || Nowr<Secl): return if(Secl<=Nowl && Nowr<=Secr)://如果当前区间全部在选中的区间内 return tree[pos] else: push_down(pos,Nowl,Nowr)//先下放标记再访问子节点 res=0 mid=(l+r)/2 if(Secl<=mid): res+=query(lch,Nowl,mid,Secl,Secr) if(Secr>mid): res+=query(rch,mid+1,Nowr,Secl,Secr) return res