zoukankan      html  css  js  c++  java
  • 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/

  • 相关阅读:
    模板之st表
    codevs 1163 访问艺术馆
    noip提高组2000 乘积最大
    [HNOI2008]越狱(luogu P3197)
    [ZJOI2009]假期的宿舍(luogu P2055)
    noip普及组2013 车站分级(luogu P1983)
    [HNOI2010]平面图判定
    sql中对于case when...then...else...end的写法和理解
    java中,去除空白的方法
    关于debug时的一些操作
  • 原文地址:https://www.cnblogs.com/shoulinniao/p/12638725.html
Copyright © 2011-2022 走看看