ST表、树状数组、线段树

适用范围

ST表:

静态区间查询,并且仅仅适用于max,min,gcd等类型的区间查询。

树状数组

可以进行单点修该,区间查询(或者利用差分进行区间修改,单点查询)
常用于求前缀和。

线段树

都可以,但是我目前只会单点修改,区间查询。

ST表

天才的记忆

从前有个人名叫 WNB,他有着天才般的记忆力,他珍藏了许多许多的宝藏。

在他离世之后留给后人一个难题(专门考验记忆力的啊!),如果谁能轻松回答出这个问题,便可以继承他的宝藏。

题目是这样的:给你一大串数字(编号为 1
到 N,大小可不一定哦!),在你看过一遍之后,它便消失在你面前,随后问题就出现了,给你 M 个询问,每次询问就给你两个数字 A,B,要求你瞬间就说出属于 A 到 B

这段区间内的最大数。

一天,一位美丽的姐姐从天上飞过,看到这个问题,感到很有意思(主要是据说那个宝藏里面藏着一种美容水,喝了可以让这美丽的姐姐更加迷人),于是她就竭尽全力想解决这个问题。

但是,她每次都以失败告终,因为这数字的个数是在太多了!

于是她请天才的你帮他解决。如果你帮她解决了这个问题,可是会得到很多甜头的哦!

主要思路:
ST表模板题

#include<iostream>
#include<algorithm>
#include<cmath>
using namespace std;
const int N=2e5+10;
int maxa[N][30];
int a[N];
int n,m;
void init()
{
    int k=floor((double)log(n)/log(2.0));
    for(int j=1;j<=k;j++)
    {
        for(int i=1;i+(1<<j)-1<=n;i++)
        {
            maxa[i][j]=max(maxa[i][j-1],maxa[i+(1<<(j-1))][j-1]);
        }
    }
}
int query(int l,int r)
{
    int k=floor((double)log(r-l+1)/log(2.0));
    return max(maxa[l][k],maxa[r-(1<<k)+1][k]);
}
int main()
{
    cin>>n;
    for(int i=1;i<=n;i++)
    {
        cin>>a[i];
        maxa[i][0]=a[i];
    }
    init();
    cin>>m;
    while(m--)
    {
        int l,r;
        cin>>l>>r;
        cout<<query(l,r)<<endl;
    }
    return 0;
}

与众不同

A 是某公司的 CEO,每个月都会有员工把公司的盈利数据送给 A,A 是个与众不同的怪人,A 不注重盈利还是亏本,而是喜欢研究「完美序列」:一段连续的序列满足序列中的数互不相同。

A 想知道区间 [L,R]
之间最长的完美序列长度。

主要思路:
这个问题有三个主要知识点:
dp,ST,二分。
利用简单的dp: f [ i ] = m a x ( b o o k [ a [ i ] ] + 1 , f [ i − 1 ] ) f[i]=max(book[a[i]]+1,f[i-1]) f[i]=max(book[a[i]]+1,f[i1])
来维护以i为结尾的完美序列的端点坐标。
处理完以后利用ST表预处理一下,方便O(1)的查询。
至于为什么要二分呢?
因为以l为结尾的端点可能不在查询范围内,会使答案错误。

设las[x]表示盈利x最近出现位置。

设st[i]表示以第i个数结尾的最长完美序列的起始位置。

st[i]=max(st[i−1],las[a[i]+1])
设f[i]表示以第i个数结尾的最长完美序列的长度

f[i]=i−st[i]+1
由st的递推式可知,st的值是一个非递减的序列。

对于一个询问区间[li,ri],该区间内的st值可能会有两种情况:

左边一部分的st值不在区间内,即<li
右边一部分的st值不在区间内,即≥li
由于st的值具有单调性,所以这个边界可以通过二分得到。设求出的边界为mid_i,可得:

st[li…midi−1]<li
st[midi…ri]≥li
那么整个区间[li,ri]的最长完美序列的长度可以分两部分来求。

左边:很显然为midi−li

右边:MAX(mi…ri)

所以右边的长度要使用ST表,即RMQ来求。

整个问题的时间复杂度:

O((M+N)×logN)

#include<iostream>
#include<algorithm>
#include<cstring>
#include<cmath>
#include<map>
using namespace std;
const int N=2e5+10;
int f[N][50];
int st[N];
int n,m;
map<int,int> book;
void init()
{
	int k=floor((double)log(n)/log(2));
	for(int j=1;j<=k;j++)
		for(int i=1;i+(1<<j)-1<=n;i++)
			f[i][j]=max(f[i][j-1],f[i+(1<<(j-1))][j-1]);
}
int find(int l,int r)
{
	int ll=l;
	if(st[l]==l) return l;//恰好开头是开头 
	if(st[r]<l) return r+1;//没有完整的 
	while(l<r)
	{
		int mid=(l+r)>>1;
		if(st[mid]<ll) l=mid+1;
		else r=mid;
	}
	return l;
}
int query(int l,int r)
{
	int k=floor((double)log(r-l+1)/log(2));
	return max(f[l][k],f[r-(1<<k)+1][k]);
}
int main()
{
	cin>>n>>m;
	for(int i=1;i<=n;i++)
	{
		int x;
		cin>>x;
		st[i]=max(st[i-1],book[x]+1);
		f[i][0]=i-st[i]+1;
		book[x]=i;
	}
	init();
	while(m--)
	{
		int l,r;
		cin>>l>>r;
		l++,r++;
		int m=find(l,r);
		int ans=0;
		if(m>l) ans=m-l;
		if(m<=r)
		{
			int cnt=query(m,r);
			ans=max(ans,cnt);
		}
		cout<<ans<<endl;
	}
}

树状数组

楼兰图腾

在这里插入图片描述
主要思路:
利用树状数组快速求解出前缀和。
首先将两个符号抽象化,V即为分别求每个点左边大于他的节点数*右边大于的节点数 的和。
那么重点就在于如何求解需要的个数。
乍一看感觉有点单调栈的感觉,但是再进一步思考,这个题目与直方图那道题还是有很大的不同的,
直方图要求连续,所以可以用单调栈维护,但是该题的点可以是离散的,所以正向逆向两边树状数组求解该问题。

#include<iostream>
#include<algorithm>
#include<cstring>
using namespace std;
typedef long long ll;
const int N=2e5+10;
ll a[N],tr[N];
ll ggreater[N],lower[N];
int n;
int lowbit(int x)
{
    return x&-x;
}
void add(int x,int c)
{
    for(int i=x;i<=n;i+=lowbit(i)) tr[i]+=c;
}
int sum(int x)
{
    int ans=0;
    for(int i=x;i;i-=lowbit(i)) ans+=tr[i];
    return ans;
}
int main()
{
    cin>>n;
    for(int i=1;i<=n;i++)  cin>>a[i];
    for(int i=1;i<=n;i++)
    {
        int y=a[i];
        ggreater[i]=sum(n)-sum(y);//右边
        lower[i]=sum(y-1);
        add(y,1);
    }
    ll ans1=0,ans2=0;
    memset(tr,0,sizeof(tr));
    for(int i=n;i>0;i--)
    {
        int y=a[i];
        ans1+=ggreater[i]*(ll)(sum(n)-sum(y));
        ans2+=lower[i]*(ll)sum(y-1);
        add(y,1);
    }
    cout<<ans1<<' '<<ans2<<endl;
    return 0;
}

一个简单的整数问题

给定长度为 N 的数列 A,然后输入 M行操作指令。

第一类指令形如 C l r d,表示把数列中第 l∼r个数都加 d。

第二类指令形如 Q x,表示询问数列中第 x个数的值。

对于每个询问,输出一个整数表示答案。

主要思路:
利用差分+树状数组轻松求解该题。

#include<iostream>
#include<algorithm>
#include<cstring>
using namespace std;
const int N=1e5+10;
typedef long long ll;
ll tr[N];
ll a[N];
int n,m;
int lowbit(int x)
{
    return x&-x;
}
void add(int x,int c)
{
    for(int i=x;i<=n;i+=lowbit(i)) tr[i]+=c;
}
ll sum(int x)
{
    ll ans=0;
    for(int i=x;i;i-=lowbit(i)) ans+=tr[i];
    return ans;
}
int main()
{
    cin>>n>>m;
    for(int i=1;i<=n;i++)
    {
        cin>>a[i];
        add(i,a[i]-a[i-1]);
    }
    while(m--)
    {
        char op;
        cin>>op;
        if(op=='C')
        {
            int l,r,d;
            cin>>l>>r>>d;
            add(l,d);
            add(r+1,-d);
        }
        else
        {
            int x;
            cin>>x;
            cout<<sum(x)<<endl;
        }
    }
    return 0;
}

一个简单的整数问题2

给定一个长度为 N 的数列 A,以及 M条指令,每条指令可能是以下两种之一:

C l r d,表示把 A[l],A[l+1],…,A[r]都加上 d。
Q l r,表示询问数列中第 l∼r个数的和。

对于每个询问,输出一个整数表示答案。

主要思路:
这个问题一看,是区间和的查询,感觉不能再使用树状数组来求解了,但是仔细分析题目可以发现:
a 1 = b i a_1=b_i a1=bi
a 2 = b 1 + b 2 a_2=b_1+b_2 a2=b1+b2
a 3 = b 1 + b 2 + b 3 a_3=b_1+b_2+b_3 a3=b1+b2+b3
a 4 = b 1 + b 2 + b 3 + b 4 a_4=b_1+b_2+b_3+b_4 a4=b1+b2+b3+b4
缺失的部分再加上一行,缺失部分的值为:
i ∗ b i + ( i − 1 ) ∗ b i − 1 … … i*b_i+(i-1)*b_{i-1}…… ibi+(i1)bi1
所以可以再维护一个 i ∗ b i i*b_i ibi的树状数组,求出前缀和即为缺失的部分。

#include<iostream>
#include<cstring>
#include<algorithm>
using namespace std;
const int N=1e5+10;
typedef long long ll;
int a[N];
ll tr1[N],tr2[N];
int n,m;
int lowbit(int x)
{
    return x&-x;
}
void add(ll tr[],int x,ll c)
{
    for(int i=x;i<=n;i+=lowbit(i)) tr[i]+=c;
}
ll sum(ll tr[],int x)
{
    ll ans=0;
    for(int i=x;i;i-=lowbit(i)) ans+=tr[i];
    return ans;
}
ll get_sum(int x)
{
    ll ans=0;
    ans=sum(tr1,x)*(x+1)-sum(tr2,x);
    return ans;
}
int main()
{
    cin>>n>>m;
    for(int i=1;i<=n;i++)
    {
        cin>>a[i];
        add(tr1,i,a[i]-a[i-1]);
        add(tr2,i,(ll)i*(a[i]-a[i-1]));
    }
    while(m--)
    {
        int l,r,x;
        char op;
        cin>>op;
        if(op=='Q')
        {
            cin>>l>>r;
            ll ans=get_sum(r)-get_sum(l-1);
            cout<<ans<<endl;
        }
        else
        {
            cin>>l>>r>>x;
            add(tr1,l,x);
            add(tr1,r+1,-x);
            add(tr2,l,(ll)l*x);
            add(tr2,r+1,(ll)(-x)*(r+1));
        }
    }
    return 0;
}

谜一样的牛

有 n 头奶牛,已知它们的身高为 1∼n且各不相同,但不知道每头奶牛的具体身高。

现在这 n头奶牛站成一列,已知第 i 头牛前面有 Ai 头牛比它低,求每头奶牛的身高。

主要思路:
首先需要仔细分析题目,如何确定一个牛的身高。
思考后发现,最后一个牛的最低身高是可以确定的。
因为前边有x个比他矮的,所以他的身高为x+1.
那么这个问题是如何利用树状数组求解的呢?

可以先把1~n上边的每一个数都加上1,然后从后边开始遍历。
每个数都代表他前边含有多少个比他矮的牛,直接二分,寻找前缀和 ≥ m i d + 1 \geq mid+1 mid+1的点,然后将该点的值-1,一直往前搜索,然后存储一下每头牛的身高即可。

#include<iostream>
#include<algorithm>
#include<cstring>
using namespace std;
const int N=1e5+10;
int tr[N];
int n;
int a[N];
int lowbit(int x)
{
    return x&-x;
}
void add(int x,int c)
{
    for(int i=x;i<=n;i+=lowbit(i)) tr[i]+=c;
}
int sum(int x)
{
    int ans=0;
    for(int i=x;i;i-=lowbit(i)) ans+=tr[i];
    return ans;
}
int main()
{
    cin>>n;
    for(int i=1;i<=n;i++)
        add(i,1);
    for(int i=2;i<=n;i++)
        cin>>a[i];
    for(int i=n;i>0;i--)
    {
        int l=1,r=n;
        while(l<r)
        {
            int mid=(l+r)>>1;
            if(sum(mid)>=a[i]+1) r=mid;
            else l=mid+1;
        }
        a[i]=l;
        add(l,-1);
    }
    for(int i=1;i<=n;i++)
    {
        cout<<a[i]<<endl;
    }
    return 0;
}

线段树

最大数

给定一个正整数数列 a1,a2,…,an,每一个数都在 0∼p−1之间。

可以对这列数进行两种操作:

添加操作:向序列后添加一个数,序列长度变成 n+1;
询问操作:询问这个序列中最后 L个数中最大的数是多少。

程序运行的最开始,整数序列为空。

一共要对整数序列进行 m次操作。

写一个程序,读入操作的序列,并输出询问操作的答案。

主要思路:
最最最简单的线段树。

#include<iostream>
#include<algorithm>
using namespace std;
const int N=2e5+10;
int m,p;
struct node
{
    int l,r;
    int v;
}t[4*N];
void pushup(int u)
{
    t[u].v=max(t[u<<1].v,t[u<<1|1].v);
}
void build(int u,int l,int r)
{
    t[u]={l,r};
    if(l==r) return;
    int mid=l+r>>1;
    build(u<<1,l,mid);
    build(u<<1|1,mid+1,r);
}
void modify(int u,int x,int v)
{
    if(t[u].l==x&&t[u].r==x) t[u].v=v;
    else
    {
        int mid=t[u].l+t[u].r>>1;
        if(x<=mid) modify(u<<1,x,v);
        else modify(u<<1|1,x,v);
        pushup(u);
    }
}
int query(int u,int l,int r)
{
    if(t[u].l>=l&&t[u].r<=r) return t[u].v;
    int v=0;
    int mid=t[u].l+t[u].r>>1;
    if(l<=mid) v=query(u<<1,l,r);
    if(r>mid) v=max(v,query(u<<1|1,l,r));
    return v;
}
int main()
{
    int n=0,last=0;
    cin>>m>>p;
    build(1,1,m);
    char op;
    int x;
    while(m--)
    {
        cin>>op>>x;
        if(op=='Q')
        {
            last=query(1,n-x+1,n);
            cout<<last<<endl;
        }
        else
        {
            n++;
            modify(1,n,(last+x)%p);
        }
    }
    return 0;
}

GSS1

给出了序列 A[1],A[2],…,A[N]A[1],A[2],…,A[N] 。 (a[i]≤15007,1≤N≤50000a[i]≤15007,1≤N≤50000 )。查询定义如下: 查询 ( x , y ) = max ⁡ { a [ i ] + a [ i + 1 ] + . . . + a [ j ] ; x ≤ i ≤ j ≤ y } ( x , y ) = m a x a [ i ] + a [ i + 1 ] + . . . + a [ j ] ; x ≤ i ≤ j ≤ y (x,y)=\max\{a[i]+a[i+1]+...+a[j];x≤i≤j≤y\}(x,y)=max{a[i]+a[i+1]+...+a[j];x≤i≤j≤y} (x,y)=max{a[i]+a[i+1]+...+a[j];xijy}(x,y)=maxa[i]+a[i+1]+...+a[j];xijy。 给定M个查询,程序必须输出这些查询的结果。

主要思路:
找最大子段和,主要就是pushup函数的编写。

#include<iostream>
#include<algorithm>
#include<cstring>
using namespace std;
const int N=5e5+10;
struct node
{
    int l,r;
    int sum,lmax,rmax,tmax;
}t[N*4];
int w[N];
int n,m;
void pushup(node &u,node &l,node &r)//用子更新父
{
    u.sum=l.sum+r.sum;
    u.lmax=max(l.lmax,l.sum+r.lmax);
    u.rmax=max(r.rmax,r.sum+l.rmax);
    u.tmax=max(max(l.tmax,r.tmax),l.rmax+r.lmax);
}
void pushup(int u)
{
    pushup(t[u],t[u<<1],t[u<<1|1]);
}
void build(int u,int l,int r)
{
    if(l==r) t[u]={l,r,w[l],w[l],w[l],w[l]};
    else
    {
        t[u]={l,r};
        int mid=l+r>>1;
        build(u<<1,l,mid);
        build(u<<1|1,mid+1,r);
        pushup(u);
    }
}
void modify(int u,int x,int v)
{
    if(t[u].l==x&&t[u].r==x) t[u]={x,x,v,v,v,v};
    else
    {
        int mid=t[u].l+t[u].r>>1;
        if(x<=mid) modify(u<<1,x,v);
        else modify(u<<1|1,x,v);
        pushup(u);
    }
}
node query(int u,int l,int r)
{
    if(t[u].l>=l&&t[u].r<=r) return t[u];
    else
    {
        int mid=t[u].l+t[u].r>>1;
        if(r<=mid) return query(u<<1,l,r);
        else if(l>mid) return query(u<<1|1,l,r);
        node left=query(u<<1,l,r);
        node right=query(u<<1|1,l,r);
        node ans;
        pushup(ans,left,right);
        return ans;
    }
}
int main()
{
    cin>>n;
    for(int i=1;i<=n;i++)
        cin>>w[i];
    build(1,1,n);
    cin>>m;
    while(m--)
    {
        int x,y;
        cin>>x>>y;
        if(x>y) swap(x,y);
        cout<<query(1,x,y).tmax<<endl;
    }
    return 0;
}

GSS3

给定长度为 N 的数列 A,以及 M条指令,每条指令可能是以下两种之一:

1 x y,查询区间 [x,y]中的最大连续子段和,即 m a x x ≤ l ≤ r ≤ y ∑ i = l r A [ i ] maxx≤l≤r≤y{∑i=lrA[i]} maxxlryi=lrA[i]
2 x y,把 A[x]改成 y 。

对于每个查询指令,输出一个整数表示答案。

主要思路:
比1多了一个单点修改。

#include<iostream>
#include<algorithm>
#include<cstring>
using namespace std;
const int N=5e5+10;
struct node
{
    int l,r;
    int sum,lmax,rmax,tmax;
}t[N*4];
int w[N];
int n,m;
void pushup(node &u,node &l,node &r)//用子更新父
{
    u.sum=l.sum+r.sum;
    u.lmax=max(l.lmax,l.sum+r.lmax);
    u.rmax=max(r.rmax,r.sum+l.rmax);
    u.tmax=max(max(l.tmax,r.tmax),l.rmax+r.lmax);
}
void pushup(int u)
{
    pushup(t[u],t[u<<1],t[u<<1|1]);
}
void build(int u,int l,int r)
{
    if(l==r) t[u]={l,r,w[l],w[l],w[l],w[l]};
    else
    {
        t[u]={l,r};
        int mid=l+r>>1;
        build(u<<1,l,mid);
        build(u<<1|1,mid+1,r);
        pushup(u);
    }
}
void modify(int u,int x,int v)
{
    if(t[u].l==x&&t[u].r==x) t[u]={x,x,v,v,v,v};
    else
    {
        int mid=t[u].l+t[u].r>>1;
        if(x<=mid) modify(u<<1,x,v);
        else modify(u<<1|1,x,v);
        pushup(u);
    }
}
node query(int u,int l,int r)
{
    if(t[u].l>=l&&t[u].r<=r) return t[u];
    else
    {
        int mid=t[u].l+t[u].r>>1;
        if(r<=mid) return query(u<<1,l,r);
        else if(l>mid) return query(u<<1|1,l,r);
        node left=query(u<<1,l,r);
        node right=query(u<<1|1,l,r);
        node ans;
        pushup(ans,left,right);
        return ans;
    }
}
int main()
{
    cin>>n>>m;
    for(int i=1;i<=n;i++)
        cin>>w[i];
    build(1,1,n);
    while(m--)
    {
        int k,x,y;
        cin>>k>>x>>y;
        if(k==1)
        {
            if(x>y) swap(x,y);
            cout<<query(1,x,y).tmax<<endl;
        }
        else
        {
            modify(1,x,y);
        }
    }
    return 0;
}

GSS5

给定一个序列。查询左端点在 [ x 1 , y 1 ] [x_1, y_1] [x1,y1] 之间,且右端点在 [ x 2 , y 2 ] [x_2, y_2] [x2,y2] 之间的最大子段和,
数据保证 x 1 ≤ x 2 , y 1 ≤ y 2 x_1\leq x_2,y_1\leq y_2 x1x2,y1y2 ,但是不保证端点所在的区间不重合。

主要思路:
在这里插入图片描述
难点是查询时要分情况讨论。

#include<iostream>
#include<algorithm>
using namespace std;
const int N=1e4+10;
struct node
{
	int l,r;
	int sum,lmax,rmax,tmax;
}t[N*4],tree;
int n,m;
int w[N];
void pushup(node &u,node &l,node &r)
{
	u.sum=l.sum+r.sum;
	u.lmax=max(l.lmax,l.sum+r.lmax);
	u.rmax=max(r.rmax,r.sum+l.rmax);
	u.tmax=max(max(l.tmax,r.tmax),l.rmax+r.lmax);
}
void pushup(int u)
{
	pushup(t[u],t[u<<1],t[u<<1|1]);
}
void build(int u,int l,int r)
{
	if(l==r) t[u]={l,r,w[l],w[l],w[l],w[l]};
	else
	{
		t[u]={l,r};
		int mid=l+r>>1;
		build(u<<1,l,mid);
		build(u<<1|1,mid+1,r);
		pushup(u);
	}
}
void modify(int u,int x,int v)
{
	if(t[u].l==x&&t[u].r==x) t[u]={x,x,v,v,v,v};
	else
	{
		int mid=t[u].l+t[u].r>>1;
		if(x<=mid) modify(u<<1,x,v);
		else modify(u<<1|1,x,v);
		pushup(u);
	}
}
node query(int u,int l,int r)
{
	if(l>r) return tree;
	if(t[u].l>=l&&t[u].r<=r) return t[u];
	int mid=t[u].l+t[u].r>>1;
	if(r<=mid) return query(u<<1,l,r);
	else if(l>mid) return query(u<<1|1,l,r);
	else
	{
		node left=query(u<<1,l,r);
		node right=query(u<<1|1,l,r);
		node ans;
		pushup(ans,left,right);
		return ans;
	}
}
int main()
{
	int t;
	cin>>t;
	while(t--)
	{
		cin>>n;
		for(int i=1;i<=n;i++)
			cin>>w[i];
		build(1,1,n);
		cin>>m;
		while(m--)
		{
			int l1,r1,l2,r2;
			cin>>l1>>r1>>l2>>r2;
			int ans=0;
			if(r1<l2)
			{
				ans+=query(1,r1,l2).sum;
				ans+=max(0,query(1,l1,r1-1).rmax);
				ans+=max(0,query(1,l2+1,r2).lmax);
			}
			else
			{
				ans=query(1,l2,r1).tmax;
				ans=max(ans,query(1,l1,l2-1).rmax+query(1,l2,r2).lmax);
				ans=max(ans,query(1,l1,r1).rmax+query(1,r1+1,r2).lmax);
				ans=max(ans,query(1,l2,r1).sum+max(0,query(1,l1,l2-1).rmax)+max(0,query(1,r1+1,r2).lmax));
			}
			cout<<ans<<endl;
		}
	}
	return 0;
}
  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值