以前总以为会线段树就够了,终于遭到社会的毒打了,不得不学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]); */
查询区间[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/