Living-Dream 系列笔记 第66期

Posted on 2024-07-26 16:52  _XOFqwq  阅读(6)  评论(0编辑  收藏  举报

RMQ 问题 / ST 表:静态区间求最值。

实现(以最大值为例):

  • 倍增 dp,预处理 \(st_{i,j}\) 表示区间 \([i,i+2^j-1]\) 内的最大值,我们有转移方程:

    \[st_{i,j}=\max(st_{i,j-1},st_{i+2^{j-1},j-1}) \]

    相当于是把 \([i,i+2^{j-1}-1]\)\([i+2^{j-1},i+2^j-1]\) 这两段区间的最大值拼了起来。

    倍增中常把 \(2^j\) 拆分为 \(2^{j-1}+2^{j-1}\)

  • 对于每组询问 \([l,r]\),找到大于等于区间长度一半的二次幂(实际上易证这就是 \(\log_2 \ r-l+1\)),从 \(l,r\) 分别找,从而覆盖 \([l,r]\)

    \[\begin{cases} j=\log_2 r-l+1\\ ans=\max(dp_{l,j},dp_{r-2^j+1,r}) \end{cases} \]

P3865

板子。

code
#include<bits/stdc++.h>
#define int long long
using namespace std;

const int N=1e5+5,M=31;
int n,m,a[N];
int dp[N][M];

void STinit(){
	for(int i=1;i<=n;i++)
		dp[i][0]=a[i];
	for(int j=1;(1<<j)<=n;j++)
		for(int i=1;i+(1<<j)-1<=n;i++)
			dp[i][j]=max(dp[i][j-1],dp[i+(1<<(j-1))][j-1]);
} 
int STquery(int l,int r){
	int t=log(r-l+1)/log(2);
	return max(dp[l][t],dp[r-(1<<t)+1][t]);
}

signed main(){
	ios::sync_with_stdio(0);
	cin.tie(0),cout.tie(0);
	cin>>n>>m;
	for(int i=1;i<=n;i++) cin>>a[i];
	STinit(); 
	while(m--){
		int l,r; cin>>l>>r; cout<<STquery(l,r)<<'\n';
	}
	return 0;
}

CF689D

首先发掘性质\(\max^{r}_{i=l} b_i\) 显然单调不减, \(\min^{r}_{i=l} a_i\) 显然单调不增,这说明可以只枚举起点,二分终点,从而确定区间。

接着我们发现两者相等的必定为一段连续区间,于是我们就二分查找满足 \(\max^{r}_{i=l} b_i \le \min^{r}_{i=l} a_i\) 的最大的 \(l1\),以及满足 \(\max^{r}_{i=l} b_i \ge \min^{r}_{i=l} a_i\) 的最大的 \(r2\),这个 \(l\) 对答案的贡献即为 \(l1-r2+1\)

注意最后要在判断一次 \(\max^{r}_{i=l} b_i = \min^{r}_{i=l} a_i\),因为有可能找不到 \(l1,r2\)

code
#include<bits/stdc++.h>
#define int long long
using namespace std;

const int N=2e5+5,M=31; 
int n,m,ans; 
int a[N],b[N]; 
int dpa[N][M],dpb[N][M]; 
 
void STinitA(){ 
	for(int i=1;i<=n;i++) 
		dpa[i][0]=a[i]; 
	for(int j=1;(1<<j)<=n;j++) 
		for(int i=1;i+(1<<j)-1<=n;i++) 
			dpa[i][j]=max(dpa[i][j-1],dpa[i+(1<<(j-1))][j-1]); 
} 
void STinitB(){ 
	for(int i=1;i<=n;i++) 
		dpb[i][0]=b[i]; 
	for(int j=1;(1<<j)<=n;j++) 
		for(int i=1;i+(1<<j)-1<=n;i++) 
			dpb[i][j]=min(dpb[i][j-1],dpb[i+(1<<(j-1))][j-1]); 
} 
int STqueryA(int l,int r){ 
	int t=log(r-l+1)/log(2); 
	return max(dpa[l][t],dpa[r-(1<<t)+1][t]); 
} 
int STqueryB(int l,int r){ 
	int t=log(r-l+1)/log(2); 
	return min(dpb[l][t],dpb[r-(1<<t)+1][t]); 
} 
 
signed main(){ 
	ios::sync_with_stdio(0); 
	cin.tie(0),cout.tie(0); 
	cin>>n; 
	for(int i=1;i<=n;i++) cin>>a[i]; 
	for(int i=1;i<=n;i++) cin>>b[i]; 
	STinitA(),STinitB();  
	for(int i=1;i<=n;i++){ 
		int l1=i-1,r1=n+1; 
		int l2=i-1,r2=n+1; 
		while(l1+1<r1){ 
			int mid=(l1+r1)>>1; 
			if(STqueryA(i,mid)<=STqueryB(i,mid)) 
				l1=mid; 
			else 
				r1=mid; 
		} 
		while(l2+1<r2){ 
			int mid=(l2+r2)>>1; 
			if(STqueryA(i,mid)>=STqueryB(i,mid)) 
				r2=mid; 
			else 
				l2=mid; 
		} 
		if(STqueryA(i,l1)==STqueryB(i,l1)&&STqueryA(i,r2)==STqueryB(i,r2)) ans+=l1-r2+1; 
	} 
	cout<<ans; 
	return 0; 
}

LOJ #10121

首先,容易发现一条性质:若有一个确定的最长完美序列 \([i,j]\),则一定不存在完美序列 \([k,j]\),其中 \(k\) 满足 \(k<i\)。这说明完美序列存在单调性。

然后对于一组询问 \([l,r]\)考虑进行分割,哪部分可以更好算出贡献,就以它进行分割

在这里,我们发现可能存在一个点 \(p\),使得以区间 \([l,p)\) 内的点为右端点的完美序列的左端点 \(L_1\) 满足 \(L_1 < l\),而以区间 \([p,r]\) 内的点为右端点的完美序列的左端点 \(L_2\) 满足 \(l \le L_2 \le r\)

因为单调性的存在,所以 \(p\) 可以二分查找得到。

计算 \([l,p)\) 内的贡献是简单的,即为 \((p-1)-L_1+1=p-L_1\)

对于每个 \(i \in [p,r]\),它贡献为 \(i-L_2+1\),用 ST 表维护这个式子的 \(\max\) 即可。

于是 \([l,r]\) 中的最长完美序列的长度即为 \(\max(p-L_1,\operatorname{query}(p,r))\)\(\operatorname{query}\) 为 ST 表中的查询)。

接下来就是 \(L_1,L_2\) 的维护。

\(last_{a_i}\) 表示 \(a_i\) 上一次出现的位置,\(st_i\) 表示以 \(i\) 为右端点的最长完美序列的左端点。

显然有转移:

\[st_i = \max(st_{i-1},last_i+1) \]

(可以继承 \(st_{i-1}\) 是因为有单调性)

最后注意下标从 \(0\) 开始,以及 \(a_i\) 可能为负,\(last_{a_i}\) 要用 \(\operatorname{map}\) 或者平移也行。

code
#include<bits/stdc++.h>
#define int long long
using namespace std;

const int N=2e5+5,M=31;
int n,m,a[N];
int dp[N][M];
int st[N];
int l,r;
map<int,int> last;

void STinit(){
	for(int i=1;i<=n;i++)
		dp[i][0]=i-st[i]+1;
	for(int j=1;(1<<j)<=n;j++)
		for(int i=1;i+(1<<j)-1<=n;i++)
			dp[i][j]=max(dp[i][j-1],dp[i+(1<<(j-1))][j-1]);
}
int STquery(int l,int r){
	int t=log(r-l+1)/log(2);
	return max(dp[l][t],dp[r-(1<<t)+1][t]);
}
int bin(int l,int r){
	int lt=l-1,rt=r+1;
	while(lt+1<rt){
		int mid=(lt+rt)>>1;
		if(st[mid]<l) lt=mid;
		else rt=mid;
	}
	return rt;
}

signed main(){
	ios::sync_with_stdio(0);
	cin.tie(0),cout.tie(0);
	cin>>n>>m;
	for(int i=1;i<=n;i++) cin>>a[i];
	for(int i=1;i<=n;i++){
		st[i]=max(st[i-1],last[a[i]]+1); 
		last[a[i]]=i;
	}
	STinit();
	while(m--){
		//int l,r; 
		cin>>l>>r;
		l++,r++;
		int p=bin(l,r);
		if(p>r) cout<<r-l+1<<'\n';
		else cout<<max(p-l,STquery(p,r))<<'\n';
	}
	//cout<<ans;
	return 0;
}

CF1142B

对于 \([l,r]\) 中的每个数 \(i\),我们都尝试将其作为 \(p\) 的循环移位的终点,

\(i\) 不断往前跳 \(n-1\) 次,记录它在 \(a_i\) 能跳到的点 \(b_i\)

\(b_i\) 扔进 ST 表中,对于每组询问 \([l,r]\)\(\max\)

若这个值 \(\ge l\),则说明存在,否则不存在。

具体实现细节见 code。

code
#include<bits/stdc++.h>
using namespace std;

const int N=2e5+5,M=31;
int n,m,q;
int p[N],a[N];
int pos[N],last[N],b[N],st[N][M];

void STinit1(){
	//memset(st,0,sizeof st);
	for(int j=1;j<=25;j++)
		for(int i=1;i<=m;i++)
			st[i][j]=st[st[i][j-1]][j-1];
}
void STinit2(){
	memset(st,0,sizeof st);
	for(int i=1;i<=m;i++) st[i][0]=b[i];
	for(int j=1;(1<<j)<=m;j++)
		for(int i=1;i+(1<<j)-1<=m;i++)
			st[i][j]=max(st[i][j-1],st[i+(1<<j-1)][j-1]);
}
int STjump(int x){
	int now=0;
	for(int i=25;i>=0;i--){
		if(now+(1<<i)<n)
			x=st[x][i],
			now+=(1<<i);
	}
	return x;
}
int STquery(int l,int r){
	int t=log2(r-l+1);
	return max(st[l][t],st[r-(1<<t)+1][t]);
}

int main(){
    //last[x]表示x在a中的下标
    //pos[x]表示x在p中的下标
    //st[i][j]表示i跳回2^j步所到的点
	cin>>n>>m>>q;
	for(int i=1;i<=n;i++)
		cin>>p[i],pos[p[i]]=i;
	for(int i=1;i<=m;i++){
		cin>>a[i];
		if(pos[a[i]]==1) st[i][0]=last[p[n]];
		else st[i][0]=last[p[pos[a[i]]-1]]; 
		last[a[i]]=i;
	}
	STinit1();
	for(int i=1;i<=m;i++)
		b[i]=STjump(i);
	STinit2();
	while(q--){
		int l,r; cin>>l>>r;
		cout<<(STquery(l,r)>=l?1:0);
	}
	return 0;
}