数据结构知识点摘要

注意:本文适合已经学过该知识点的人快速复习记忆,有些原理会直接略过,初学请留步

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)) \]

补充:

  1. ST表可以解决任何可重复贡献问题,指一个值被多次统计不会影响答案,正例有max, min, gcd, lcm等,反例有区间和等
  2. 建议预处理\(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\)为差分数组

\[\sum_{i=1}^{pos}a[i]=\sum_{i=1}^{pos}\sum_{j=1}^{i}d[j]=\sum_{i=1}^{pos}d[i]*(pos-i+1) \\ \large=\sum_{i=1}^{pos}d[i]*(pos+1)-\sum_{i=1}^{pos}d[i]*i \]

因此,我们需要维护树状数组维护\(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*2rch为右孩子,等价于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
    
posted @ 2023-03-18 15:46  MessageBoxA  阅读(62)  评论(0编辑  收藏  举报