ST表入门和例题

以前总以为会线段树就够了,终于遭到社会的毒打了,不得不学ST表。

ST 表是用于解决 可重复贡献问题 的数据结构。

可重复贡献问题是指对于运算opt,满足x opt x=x,则对应的区间询问就是一个可重复贡献问题。例如,最大值有max(x,x)=x,gcd 有gcd(x,x)=x,所以 RMQ 和区间 GCD 就是一个可重复贡献问题。像区间和就不具有这个性质,如果求区间和的时候采用的预处理区间重叠了,则会导致重叠部分被计算两次,这是我们所不愿意看到的。另外,opt还必须满足结合律才能使用 ST 表求解。

思想:倍增和dp

先上一道模板题,慢慢理解。

题目:给定一个长度为n的数列a,和m次询问,求出每一次询问的区间[L,R]内数字的最大值。

数据范围:n∈[1,1e5],m∈[1,2e6],ai∈[1,1e9],1<=L<=R<=n

刚上手啥也不懂,直接理解代码。

dp[i][j]表示,区间[i,i+2j-1]的最大值。例如dp[3][2]=max[3,6],一次查询当前至后面2j范围。

初始化dp[i][0]=a[i]。

dp数组的第二维度每次跳跃2j-1范围,这就是倍增的意思吧。

通用的状态转移方程:dp[i][j]=max( dp[i][j-1], dp[i+2j-1][j-1] );

看起来十分抽象,难以理解,打印输出区间范围,看懂后套模板也能套得心安理得。

int two[20];

void init()
{
    two[0]=1;
    for(int i=1;i<20;i++)
        two[i]=two[i-1]*2;

}

int main()
{
    init();
    int i,j;
    while(scanf("%d%d",&i,&j)!=EOF)
    {
        int a=i+two[j-1];
        int b=j-1;
        printf("dp[%d][%d]=max(dp[%d][%d],dp[%d][%d]);
",i,j, i,j-1, a,b );
        printf("[%d,%d]=max([%d,%d],[%d,%d]);
",i,i+two[j]-1, i,i+two[j-1]-1,  a,a+two[b]-1  );
    }

    return 0;
}
/**
6
dp[3][6]=max(dp[3][5],dp[35][5]);
[3,66]=max([3,34],[35,66]);
5
dp[4][5]=max(dp[4][4],dp[20][4]);
[4,35]=max([4,19],[20,35]);
3
dp[9][3]=max(dp[9][2],dp[13][2]);
[9,16]=max([9,12],[13,16]);
5
dp[14][5]=max(dp[14][4],dp[30][4]);
[14,45]=max([14,29],[30,45]);
10
dp[33][10]=max(dp[33][9],dp[545][9]);
[33,1056]=max([33,544],[545,1056]);
16
dp[100005][16]=max(dp[100005][15],dp[132773][15]);
[100005,165540]=max([100005,132772],[132773,165540]);
*/
View Code

查询区间[l,r]怎么分割?

[l,r]=[l,l+2k-1]+[r-2k+1,r]

max(dp[l,k],dp[r-2k+1][k]);

其中k=[log2(r-l+1)],二分,计算跨步。如果区间大小不是2的幂次,两个子区间中间会有重复,无所谓,覆盖了整个[l,r]区间就好。

优化:查询次数m比n多得多,线性打表,预处理后直接获取更快,调用log2()函数是log2n的时间复杂度。

第一次提交每次用log2()函数,可以AC,题解也看到一些线段树可以AC,单个查询在log级别也是可以过的。

第二次改用打表提交时间明显更短。

int two[21];
int Log[100086];
int dp[100086][21];
int n,m,l,r,k;

inline int read()///读写挂
{
    char c=getchar();int x=0,f=1;
    while(c<'0'||c>'9'){if(c=='-')f=-1;c=getchar();}
    while(c>='0'&&c<='9'){x=x*10+c-'0';c=getchar();}
    return x*f;
}

void init()///预处理打表
{
    two[0]=1;
    for(int i=1; i<21; i++)
        two[i]=two[i-1]*2;
    Log[0]=-1;
    for(int i=1;i<100086;i++)
        Log[i]=Log[i/2]+1;
}

int main()
{
    init();
    n=read();
    m=read();
    for(int i=1; i<=n; i++)
        dp[i][0]=read();
    ///状态转移方程
    for(int j=1; j<21; j++)
        for(int i=1; i+two[j]-1<=n; i++)
            dp[i][j]=max( dp[i][j-1], dp[ i+two[j-1]  ][j-1] );

    while(m--)
    {
        l=read();
        r=read();
        ///k=log2(r-l+1);
        k=Log[r-l+1];
        int ans=max( dp[l][k],dp[ r-two[k]+1 ][k] );
        printf("%d
",ans);
    }

    return 0;
}

题目2:牛客练习赛14B-区间的连续段

题意:有106个数,有106个查询,有1个k,查询区间[l,r]内的数最少分成多少个连续段,使得每段的和都<=k。

思路:

最少个区间段,连续段的和也是小于等于,尽量让连续段的值最大,但不超过k,有贪心思想。

k固定,对于每个数a[i]都可以向右延展到某个数a[j],使得[i,j]这一段最大但不超过k。将这个区间的左右连一条边,称之为跳,相当于一次就跳过了很多数字,不用挨个遍历求和,如果只有这样,遇到极限数据全部是1并且k也是1的时候还是要一步一步跳,时间耗费也变多了。

按倍增的思想,两条边的首尾再连一条边,这样一个大区间一次可以跳2个小区间,再把两个大区间首位相连,变成一个特大区间,一次可以跳4个小区间,以此类推,查询的时候一次可以尝试一个特特特特特大的区间,逐渐缩小,时间复杂度就降到log级别的了。

dp[i][j]表示以i为起点,跳2j个区间能跳到哪里,第二维度只需要开到20就够了。

dp[i][0]就是最普通的一段连续区间,一开始一直不能理解区间[i,j]为什么dp[i][0]的值不取j而取j+1。随后一句递推式体现了为何要取j+1:dp[i][j]=dp[ dp[i][j-1] ][j-1];这一句将左右两个区间合并在一起,dp[i][j-1]设为x,[i,x-1]和[x,dp[x][j-1]],dp[i][0]取值j也行,但代码需要修改多出几行,取j+1可以直接跳到第二个区间的末尾。

查询答案log效率,j从大到小,找一个在范围内的大区间,然后l直接跳到区间右端。区间是左闭右开,只要l在查询区间内,就有答案,l在跳动过程中<=r,初始化res=1是应对最后一个l区间。

#include<stdio.h>
#include<iostream>
#include<algorithm>
#include<cstring>
#include<math.h>
#include<string>
#include<map>
#include<queue>
#include<stack>
#include<set>
#include<ctime>
#define ll long long
#define inf 0x3f3f3f3f ///填充无限小-0x7f
using namespace std;

int two[21];
ll dp[1000086][21];
ll a[1000086];
ll s[1000086];
int ans[1000086];
int n,m,k;

inline int read()///读写挂
{
    char c=getchar();int x=0,f=1;
    while(c<'0'||c>'9'){if(c=='-')f=-1;c=getchar();}
    while(c>='0'&&c<='9'){x=x*10+c-'0';c=getchar();}
    return x*f;
}

void init()///预处理打表
{
    two[0]=1;
    for(int i=1; i<21; i++)///two[20]=1048576
        two[i]=two[i-1]*2;
}

int main()
{
    n=read();
    m=read();
    k=read();
    init();
    for(int i=1;i<=n;i++)
    {
        a[i]=read();
        s[i]=s[i-1]+a[i];
        ans[i]=ans[i-1];
        if(a[i]>k)///前缀和,用于判断区间内有没有大于k的数
            ans[i]++;
    }
    for(int i=1;i<=n;i++)
    {
        ///s是前缀和数组,有序的,可以用二分
        int pos=upper_bound(s+i,s+n+1, s[i-1]+k )-s;
        dp[i][0]=pos;///表示最远到达哪个下标,不包括
    }
    for(int i=0;i<21;i++){
        dp[n+1][i]=n+1;
    }

    for(int j=1; two[j]<=n; j++)
    {
        for(int i=1; i+two[j]<=n; i++)
            dp[i][j]=dp[ dp[i][j-1] ][j-1];
    }

    while(m--)
    {
        int l,r;
        l=read();
        r=read();
        //scanf("%d%d",&l,&r);
        int now=l,res=1;
        if((ans[r]-ans[l-1])>0)
            printf("Chtholly
");
        else
        {
            for(int i=20;dp[l][0]<=r;i--)
            {
                if(dp[l][i] && dp[l][i]<=r)
                {
                    l=dp[l][i];///二分法,找到一次
                    res+=two[i];
                }
            }
            printf("%d
",res);
        }
    }
    return 0;
}

参考:

https://oi-wiki.org/ds/sparse-table/

原文地址:https://www.cnblogs.com/shoulinniao/p/12638725.html