Round #4 RMQ问题ST算法
前几天群里看到有人问[JSOI2008]最大数,一道很简单的问题,线段树无脑做,但是看到了动态ST,emmm,学学吧,听大佬说了下思路,还好,不难的;
四道题都可以用其他数据结构或做法代替,例如线段树,dp什么的,但这不重要,毕竟学的就是ST表,触类旁通,数据结构很多知识都是可以互通的,例如一维推广到二维,可持久化这些;
倍增思想,常见的有:
1. 2^(x1)+2^(x2)...2^(xn)=2^n (max{xi}<=logn)
对于正整数x,存在一个二进制表示方法,例如11=1011(2),那么也就是11可以用2^3+2^1+2^0表示;
所以快速幂采用这种方法,在log(n)的时间内求出2^n;
2. 2^n=2*2^(n-1)=2^(n-1)+2^(n-1)
假设我们知道了间隔为2^x的最小值,那么我们可以把两个相邻的间隔为2^x的最小值合并成间隔为2^(x+1)最小值;
那么我们查询区间[L,R]的最小值,我们可以通过查询区间长度为2^k(k=floor(log(R-L+1)))的两个区间[L,L+2^k-1],[R-2^k+1,R]的最小值得到答案,而得到区间以i为起点,长度为2^k的区间的最小值,就可以通过前面说的合并方式预处理得到,那么这就是ST表了;
就...一道裸的ST表,唯一可以说的就是,题目说明了时间卡的紧,务必保证查询复杂度为O(1),一般来说我们可能会对于查询n的logn去枚举一下,那么这题都这样说了,那我就O(1)吧,不枚举了;
我来递推logn的值,边界Log(1)=0,然后就Log(n)=Log(n/2)+1;
注意,这里是下取整,即2^k<=n,k的最大值
#include<bits/stdc++.h> using std::cin; using std::cout; using std::endl; typedef long long ll; typedef unsigned long long ull; typedef std::pair<int,int> P; #define FOR(i,init,len) for(int i=(init);i<(len);++i) #define For(i,init,len) for(int i=(init);i<=(len);++i) #define fi first #define se second #define pb push_back #define is insert namespace IO { inline char getchar() { static const int BUFSIZE=5201314; static char buf[BUFSIZE],*begin,*end; if(begin==end) { begin=buf; end=buf+fread(buf,1,BUFSIZE,stdin); if(begin==end) return -1; } return *begin++; } } inline void read(int &in) { int c,symbol=1; while(isspace(c=IO::getchar())); if(c=='-') { in=0;symbol=-1; } else in=c-'0'; while(isdigit(c=IO::getchar())) { in*=10;in+=c-'0'; } in*=symbol; } inline int read() { static int x;read(x);return x; } ll gcd(ll a,ll b) { return b?gcd(b,a%b):a; } ll lcm(ll a,ll b) { return a/gcd(a,b)*b; } #define PA(name,init,len) cout<<#name"["<<(len-init)<<"]=";FOR(_,init,len) cout<<name[_]<<" \n"[_==(len-1)]; #define Pa(name,init,len) cout<<#name"["<<(len-init+1)<<"]=";For(_,init,len) cout<<name[_]<<" \n"[_==(len)]; #define PV(name) cout<<#name"="<<name<<'\n'; const int maxn=1e5+10; const int logn=18; int n,m; int d[maxn][logn]; int Log[maxn]; int main() { #ifdef MengLan int Beginning=clock(); //freopen("in","r",stdin); //freopen("out","w",stdout); #endif // MengLan Log[1]=0; FOR(i,2,maxn) Log[i]=Log[i/2]+1; scanf("%d%d",&n,&m); For(i,1,n) scanf("%d",&d[i][0]); //For(i,1,10) printf("log[%d]=%d\n",i,Log[i]); for(int j=1;(1<<j)<=n;++j) for(int i=1;i+(1<<j)-1<=n;++i) d[i][j]=std::max(d[i][j-1],d[i+(1<<(j-1))][j-1]); while(m--){ int l,r;scanf("%d%d",&l,&r); int k=Log[r-l+1]; printf("%d\n",std::max(d[l][k],d[r-(1<<k)+1][k])); } #ifdef MengLan printf("Time: %d\n",clock()-Beginning); system("pause"); #endif // MengLan return 0; }
很后悔加了这道题,因为这道题没什么难的,就是要讨论的东西有点多而已。。。
题意有两个条件,1.要求Y年降雨量大于等于X年(重点,一开始没注意这个条件),2.降雨量max{(Y,X)}(注意,(x,y)意思是开区间)的最大值严格小于X年降雨量
分四种情况考虑,每种情况的具体细节见代码注释:
1.Y、X年的降雨量均存在记录,那么按题意做
2.Y年降雨量不存在记录,但X年降雨量存在记录,那么第一个条件无法判断,我们就假设成立吧,则第二个条件成立的话,是maybe,否则就是false
3.Y年存在记录,X年不存在记录,依旧第一个条件无法判断,继续假设成立,那X年的降雨量最做和Y年降雨量一样,那么就取它吧,判断是否符合第二个条件,可以就maybe,否则false
4,X和Y年都不存在,只能maybe了
#include<iostream> #include<cstdio> #include<string> #include<cstring> #include<cstdlib> #include<sstream> #include<fstream> #include<vector> #include<list> #include<deque> #include<stack> #include<queue> #include<map> #include<set> #include<cmath> #include<utility> #include<numeric> #include<iterator> #include<algorithm> #include<functional> #include<ctime> #include<cassert> using std::cin; using std::cout; using std::endl; typedef long long ll; typedef unsigned long long ull; typedef std::pair<int,int> P; #define FOR(i,init,len) for(int i=(init);i<(len);++i) #define For(i,init,len) for(int i=(init);i<=(len);++i) #define fi first #define se second #define pb push_back #define is insert namespace IO { inline char getchar() { static const int BUFSIZE=5201314; static char buf[BUFSIZE],*begin,*end; if(begin==end) { begin=buf; end=buf+fread(buf,1,BUFSIZE,stdin); if(begin==end) return -1; } return *begin++; } } inline void read(int &in) { int c,symbol=1; while(isspace(c=IO::getchar())); if(c=='-') { in=0;symbol=-1; } else in=c-'0'; while(isdigit(c=IO::getchar())) { in*=10;in+=c-'0'; } in*=symbol; } inline int read() { static int x;read(x);return x; } ll gcd(ll a,ll b) { return b?gcd(b,a%b):a; } ll lcm(ll a,ll b) { return a/gcd(a,b)*b; } #define PA(name,init,len) cout<<#name"["<<(len-init)<<"]=";FOR(_,init,len) cout<<name[_]<<" \n"[_==(len-1)]; #define Pa(name,init,len) cout<<#name"["<<(len-init+1)<<"]=";For(_,init,len) cout<<name[_]<<" \n"[_==(len)]; #define PV(name) cout<<#name"="<<name<<'\n'; const int maxn=1e5+10; const int logn=18; int n,m; struct Lan{ int y,r; bool operator<(const int &rhs)const{return y<rhs;} }in[maxn]; int d[maxn][logn],Log[maxn]; int find(int x){return std::lower_bound(in,in+n,x)-in;} int main() { #ifdef MengLan int Beginning=clock(); //freopen("in","r",stdin); //freopen("out","w",stdout); #endif // MengLan scanf("%d",&n); FOR(i,0,n) scanf("%d%d",&in[i].y,&in[i].r),d[i][0]=in[i].r; scanf("%d",&m); Log[1]=0; For(i,2,n) Log[i]=Log[i/2]+1; for(int j=1;(1<<j)<=n;++j) for(int i=0;i+(1<<j)-1<n;++i) d[i][j]=std::max(d[i][j-1],d[i+(1<<(j-1))][j-1]); while(m--){ int x,y;scanf("%d%d",&y,&x); //assert(y+1<x); int l=find(y),r=find(x); if(in[l].y==y&&in[r].y==x){ if(in[l].r<in[r].r) puts("false");//x年的降雨量比y年大,不符合“X年的降雨量不超过Y年” else if(y+1==x) puts("true");//空集,区间(y,x)直接不存在其他年份,由上一个条件知道y年降雨量比x年多,那么显然是对的 else if(l+1==r) puts("maybe");//由两个条件可知,区间(y,x)直接存在其他年份,但是却没有这一年或多年的记录,那么显然maybe else{//上面的特殊情况终于完了,下面是正常情况 int L=l+1,R=r-1; int k=Log[R-L+1]; int max=std::max(d[L][k],d[R-(1<<k)+1][k]);//区间(y,x)的最大值 if(max>=in[r].r) puts("false"); else if(x-y+1==r-l+1) puts("true");//区间(y,x)内每年都有记录,那么可以准确回答 else puts("maybe"); } } else if(in[l].y==y){ if(l+1==r) puts("maybe");//区间(y,x)没有记录,而我也不知道x年的值,那么只能maybe else{//区间(y,x)有记录,那么我假设x年降雨量和y年一样,也符合“X年的降雨量不超过Y年”,那么就看(y,x)是否比y要小了 int L=l+1,R=r-1; int k=Log[R-L+1]; int max=std::max(d[L][k],d[R-(1<<k)+1][k]); if(max>=in[l].r) puts("false"); else puts("maybe"); } } else if(in[r].y==x){ if(l==r) puts("maybe");//y年没记录,我还找不到(y,x)的记录,那么maybe else{//区间(y,x)有记录,假设y年比我x年大,那么我判断(y,x)是不是比x年小就好了 int L=l,R=r-1; int k=Log[R-L+1]; int max=std::max(d[L][k],d[R-(1<<k)+1][k]); if(max>=in[r].r) puts("false"); else puts("maybe"); } } else puts("maybe"); } #ifdef MengLan printf("Time: %d\n",clock()-Beginning); system("pause"); #endif // MengLan return 0; }
ST表,动态ST表,模拟预处理ST表从0层到logn层的过程,每插入一个元素,从0层开始往上更新,定位左边界L,d[L][k]=min(d[L][k-1],d[L+(1<<(k-1))][k-1])即可
emmm,你会发现每次插入n的时候n还要加上上次询问的t作为最终插入值,意思很明显,强制在线,不然的话,可以考虑离线做法,读取插入数据,得到全部元素,对于询问记录询问时的最右边端点R,然后R-L+1得到左端点,即询问[R-L+1,R]的最大值,最后预处理了ST表,再去对这些询问处理,得到结果输出,说说而已,这题又不能离线,强制在线,而且线段树什么的,做起来更容易
#include<bits/stdc++.h> using std::cin; using std::cout; using std::endl; typedef long long ll; typedef unsigned long long ull; typedef std::pair<int,int> P; #define FOR(i,init,len) for(int i=(init);i<(len);++i) #define For(i,init,len) for(int i=(init);i<=(len);++i) #define fi first #define se second #define pb push_back #define is insert namespace IO { inline char getchar() { static const int BUFSIZE=5201314; static char buf[BUFSIZE],*begin,*end; if(begin==end) { begin=buf; end=buf+fread(buf,1,BUFSIZE,stdin); if(begin==end) return -1; } return *begin++; } } inline void read(int &in) { int c,symbol=1; while(isspace(c=IO::getchar())); if(c=='-') { in=0;symbol=-1; } else in=c-'0'; while(isdigit(c=IO::getchar())) { in*=10;in+=c-'0'; } in*=symbol; } inline int read() { static int x;read(x);return x; } ll gcd(ll a,ll b) { return b?gcd(b,a%b):a; } ll lcm(ll a,ll b) { return a/gcd(a,b)*b; } #define PA(name,init,len) cout<<#name"["<<(len-init)<<"]=";FOR(_,init,len) cout<<name[_]<<" \n"[_==(len-1)]; #define Pa(name,init,len) cout<<#name"["<<(len-init+1)<<"]=";For(_,init,len) cout<<name[_]<<" \n"[_==(len)]; #define PV(name) cout<<#name"="<<name<<'\n'; const int maxn=2e5+10; const int logn=22; ll d[maxn][logn]; int M,D; int main() { #ifdef MengLan int Beginning=clock(); //freopen("in","r",stdin); //freopen("out","w",stdout); #endif // MengLan scanf("%d%d",&M,&D); ll t=0; int size=0; while(M--){ char cmd[10]; ll in; scanf("%s%lld",cmd,&in); if(cmd[0]=='A'){ in=(in+t)%D; d[size][0]=in; //直接更新到最高层去,省得以后多加一层,我还需要跑去处理最上层的东西,更好写,常数会高一点,毕竟nlogn跑满了 FOR(k,1,logn){ int len=(1<<k); int L=size-len+1;//区间[L,size],长度为2^k if(L<0) d[0][k]=std::max(d[0][k],in);//2^k>size,d[0][k]表示的区间大于当前我值的个数,那么全部更新到以0开始长度为2^k的区间里面去 else d[L][k]=std::max(d[L][k-1],d[L+(1<<(k-1))][k-1]); } ++size; } else{ if(in==0) puts("0"); else{ int k=0; while((1<<(k+1))<=in) ++k; int L=size-in; t=std::max(d[L][k],d[size-(1<<k)][k]); printf("%lld\n",t); } } } #ifdef MengLan printf("Time: %d\n",clock()-Beginning); system("pause"); #endif // MengLan return 0; }
ST表嘛,那我们就ST表做咯,将一维推广到二维,下午想了一下,然后就去睡觉了,睡醒再一看,简直太简单了,然后开心的算一下空间大小,好像哪里不对,n^2log^2(n),内存有点炸,而且时间和空间复杂度都是n^2log^2(n)的。
二维ST表大概是这样的(开始瞎吹,我也没写过吖)
在每次枚举一个y轴上的间隔为2^l的时候,我们在x轴上做一遍一维的预处理,那么对于矩形查询[n,m]的最大/小值,我们都可以找到一个最大的l和k(2^l<=n&&2^k<=m)直接在ST表上查找;
这题是求正方形,而且一开始就给定了正方形的边长n,那么我们就可以简化一下下,变成一个查找正方形的ST表,而不是查找矩形的ST表;
和上面有什么区别?区别就是间距只有一个k,表示宽度为2^k的正方形的最小值
那么d[i][j][k]就可以从四个点{d[i][j][k-1],d[i][j+(1<<(k-1))][k-1],d[i+(1<<(k-1))][j][k-1],d[i+(1<<(k-1))][j+(1<<(k-1))][k-1]}转移过来
而一维只需要从左右两个点,这里需要从左上,右上,左下,右下四个点;
#include<bits/stdc++.h> using std::cin; using std::cout; using std::endl; typedef long long ll; typedef unsigned long long ull; typedef std::pair<int,int> P; #define FOR(i,init,len) for(int i=(init);i<(len);++i) #define For(i,init,len) for(int i=(init);i<=(len);++i) #define fi first #define se second #define pb push_back #define is insert namespace IO { inline char getchar() { static const int BUFSIZE=5201314; static char buf[BUFSIZE],*begin,*end; if(begin==end) { begin=buf; end=buf+fread(buf,1,BUFSIZE,stdin); if(begin==end) return -1; } return *begin++; } } inline void read(int &in) { int c,symbol=1; while(isspace(c=IO::getchar())); if(c=='-') { in=0;symbol=-1; } else in=c-'0'; while(isdigit(c=IO::getchar())) { in*=10;in+=c-'0'; } in*=symbol; } inline int read() { static int x;read(x);return x; } ll gcd(ll a,ll b) { return b?gcd(b,a%b):a; } ll lcm(ll a,ll b) { return a/gcd(a,b)*b; } #define PA(name,init,len) cout<<#name"["<<(len-init)<<"]=";FOR(_,init,len) cout<<name[_]<<" \n"[_==(len-1)]; #define Pa(name,init,len) cout<<#name"["<<(len-init+1)<<"]=";For(_,init,len) cout<<name[_]<<" \n"[_==(len)]; #define PV(name) cout<<#name"="<<name<<'\n'; const int maxn=1e3+10; const int logn=13; int a,b,n; int in[maxn][maxn]; int max[maxn][maxn][logn],min[maxn][maxn][logn]; int main() { #ifdef MengLan int Beginning=clock(); //freopen("in","r",stdin); //freopen("out","w",stdout); #endif // MengLan scanf("%d%d%d",&a,&b,&n); FOR(i,0,a) FOR(j,0,b){ scanf("%d",in[i]+j); max[i][j][0]=min[i][j][0]=in[i][j]; } for(int k=1;(1<<k)<=n;++k) { for(int i=0;i+(1<<k)-1<a;++i) for(int j=0;j+(1<<k)-1<b;++j){ max[i][j][k]=std::max({max[i][j][k-1],max[i][j+(1<<(k-1))][k-1],max[i+(1<<(k-1))][j][k-1],max[i+(1<<(k-1))][j+(1<<(k-1))][k-1]});//C++11的新特性,函数参数可以传个initializer_list过去,说白了就是传个特定类型数组过去 min[i][j][k]=std::min({min[i][j][k-1],min[i][j+(1<<(k-1))][k-1],min[i+(1<<(k-1))][j][k-1],min[i+(1<<(k-1))][j+(1<<(k-1))][k-1]}); //printf("max[%d][%d][%d]=%d min[%d][%d][%d]=%d\n",i,j,k,max[i][j][k],i,j,k,min[i][j][k]); } } //枚举左上角i,j,求区间[i,i+n][j,j+n]的最大最小值作差更新ans int k=0,ans=2e9; while((1<<(k+1))<=n) ++k; for(int i=0;i+n-1<a;++i) for(int j=0;j+n-1<b;++j){ int Max=std::max({max[i][j][k],max[i][j+n-(1<<k)][k],max[i+n-(1<<k)][j][k],max[i+n-(1<<k)][j+n-(1<<k)][k]}); int Min=std::min({min[i][j][k],min[i][j+n-(1<<k)][k],min[i+n-(1<<k)][j][k],min[i+n-(1<<k)][j+n-(1<<k)][k]}); ans=std::min(ans,Max-Min); } printf("%d\n",ans); #ifdef MengLan printf("Time: %d\n",clock()-Beginning); system("pause"); #endif // MengLan return 0; }
写题解什么的,是真的花时间。。。原本以为一小时写得完,然后emmm,怎么花了好像两个多小时?睡觉睡觉