(简记)二分答案 二分查找 倍增 RMQ ST 表优化建图

二分查找

保证序列 \(a_1\)\(a_n\) 的单调性,可以通过二分查找迅速定位 \(v\le a_i\) 的最小 \(i\) 的位置,这也是 lower_bound(a+1,a+1+n,v)-a 的主要功能,此外还有 upper_bound 相应地表示 \(v<a_i\) 最小的 \(i\) 的位置。

二分答案

联想到这个 trick 一般是题目中出现最小值最大化最大值最小化或者当题目答案存在连续数域上的一个分界点,在此之前都合法,在此之后都不合法,这时候可以使用二分答案避免一个个枚举可能的答案,时间复杂度 \(O(\log n)\)

P9755 [CSP-S 2023] 种树

选择二分出最早结束时间,可以通过分类讨论计算出节点 \(i\) 从时间 \(l\) 开始种树种到 \(r\) 的树高 \(h(i,l,r)\),对于每个二分的 check 函数分别计算每个节点 \(r=mid\)\(l\) 最迟(最大)取到多少可以使节点合法,记为 \(t_i\),那么按照 \(t_i\) 升序排序然后贪心地对于每个当前最小 \(t_i\) 画一条链下来使得其覆盖直到节点 \(i\) 即可,链上的种树时间顺序递增。这样做总共是 \(O(n\log V\log n)\) 的,要用 __int128 且微卡常。

点击查看代码
#include<bits/stdc++.h>
using namespace std;
typedef __int128 LL;
const int N=1e5+5;
int n,rk[N],fat[N],t[N];
LL a[N],b[N],c[N];
int vis[N],mx;
vector<int>G[N];
inline LL height(int id,int l,int r){
	if(!c[id])return b[id]*(r-l+1);
	if(c[id]>0)return b[id]*(r-l+1)+c[id]*(l+r)*(r-l+1)/2;
	int imax=(1-b[id])/c[id];
	if(imax>=r)return b[id]*(r-l+1)+c[id]*(l+r)*(r-l+1)/2;
	if(imax<l)return (r-l+1);
	return b[id]*(imax-l+1)+c[id]*(l+imax)*(imax-l+1)/2+(r-imax);
}
inline int calc(int id,int lim){
	int l=1,r=lim,res=lim+1;
	while(l<=r){
		int mid=(l+r)>>1;
		if(height(id,mid,lim)>=a[id])res=mid,l=mid+1;
		else r=mid-1;
	}
	return res;
}
bool cmp(int x,int y){return t[x]<t[y];}
void dfs(int u){
	for(int v:G[u]){
		if(v==fat[u])continue;
		fat[v]=u;
		dfs(v);
	}
}
int tp,stk[N];
inline bool check(int mid){
	for(int i=1;i<=n;i++){
		vis[i]=0;rk[i]=i;
		t[i]=calc(i,mid);
		if(t[i]>mid){return 0;}
	}
	sort(rk+1,rk+1+n,cmp);
	int cgt=0;
	for(int i=1;i<=n;i++){
		int u=rk[i];
		if(vis[u])continue;
		stk[++tp]=u;
		while(fat[u]&&!vis[fat[u]]){
			u=fat[u];
			stk[++tp]=u;
		}
		while(tp)vis[stk[tp--]]=++cgt;
	}
	for(int i=1;i<=n;i++)
		if(height(i,vis[i],mid)<a[i])return 0;
	return 1;
}
int solve(){
	int l=n,r=min((int)1e9,mx)+n/3,res=r;
	while(l<=r){
		int mid=(l+r)>>1;
		if(check(mid))res=mid,r=mid-1;
		else l=mid+1;
	}
	return res;
}
int main(){
	ios::sync_with_stdio(0);
	cin.tie(0);cout.tie(0);
	cin>>n;
	for(int i=1;i<=n;i++){
		long long x;cin>>x;a[i]=x;
		cin>>x;b[i]=x;mx=max(mx,(int)x);
		cin>>x;c[i]=x;
	}
	for(int i=1;i<n;i++){
		int u,v;cin>>u>>v;
		G[u].emplace_back(v);
		G[v].emplace_back(u);
	}
	dfs(1);
	cout<<solve();
	return 0;
}

倍增 RMQ

主要用途有树上 \(\text{LCA}\) 求取等等,也可以应用于某些 DP 优化。具体来说,\(f_{u,i}\) 表示 \(u\) 走了 \(2^i\) 步的结果,在 \(\text{LCA}\) 中是向上跳了若干级,然后转移是 \(O(1)\) 的,空间是 \(O(n\log n)\) 的,可以通过 \(v\) 的二进制拆分组合成任意的 \(g_{u,v}\) 表示走了 \(v\) 步的结果,时间复杂度为 \(O(\log n)\)

应用

Problem:在 \(i\) 可以使用一步移到 \([\max(1,i-a_i),\min(n,i+a_i)]\),求 \(p\) 步内(包含 \(p\))使任意两点至少其中一点可以到达另一点最小的 \(p\)

\(n\le 10^5\)

设计出朴素 DP,维护对于每个点 \(i\)\(j\) 步能走到的区间 \([L_{i,j},R_{i,j}]\),那么有转移 \(L_{i,j}\leftarrow \min_{k\in [L_{i-1,j},R_{i-1,j}]} L_{k,j-1}\)\(R_{i,j}\leftarrow \max_{k\in [L_{i-1,j},R_{i-1,j}]} R_{k,j-1}\)

考虑怎么优化状态,可以把第二维直接倍增,转移还是一样的,每个维度上区间取 \(\min \max\) 搞个什么数据结构或者单调栈上二分即可,这样预处理就是 \(O(n\log^2 n)\) 的。

考虑二分答案,发现验证答案需要 \(O(\log^2 n)\) 的,套上二分简直赤石,那么我们考虑类似 \(\text{LCA}\) 直接对每一位考虑其能不能为 \(1\),如果为 \(1\) 导致合法那么就置为 \(0\),否则为 \(1\),然后最终答案 \(+1\) 就可以得到真实答案,逐位考虑总共是 \(O(n\log^2 n)\) 的。

更进一步的,我们考虑上式 \(k\) 的取值,发现其一定在取到 \(i\),且 \(i\) 在询问区间中有最小的 \(i-a_i\) 或者最大的 \(i+a_i\)。如何证明?以 \(i-a_i\) 为例,考虑取一个 \(i\neq j\),那么必然有 \(i-a_i\le j-a_j\),即 \(i\) 的限制会比所有的 \(j\) 要宽,考虑多走几步,发现 \(i\) 的限制其实是一个后缀,并且其包含 \(j\) 的限制,那么在递归的过程中 \(i\) 的所有情况一定包含 \(j\),所以取到 \(i\) 可以包含所有情况,一定最优,\(i+a_i\) 的考虑也是同理。

那么我们可以用 ST 表 \(O(n\log n)\) 预处理,\(O(1)\) 查询 \(i\in [l,r]\) 分别取到最小和最大的 \(i-a_i,i+a_i\)\(i\) 值,然后转移就变成了 \(O(1)\) 的,全局复杂度可优化至 \(O(n\log n)\)

两只 log
#include<bits/stdc++.h>
using namespace std;
const int N=1e5+5;
int n,a[N],fL[N][20],fR[N][20];
int stk[N],tp,FL[N],FR[N],preR[N];
int dtL[N],dtR[N];
int main(){
	//freopen("jump.in","r",stdin);
	//freopen("jump.out","w",stdout);
	ios::sync_with_stdio(0);
	cin.tie(0);cout.tie(0);
	cin>>n;
	for(int i=1;i<=n;i++)
		cin>>a[i],fL[i][0]=max(1,i-a[i]),fR[i][0]=min(n,i+a[i]);
	int mx=0;
	for(int j=1;(1<<j)<=n;j++){
		mx=max(mx,j);
		tp=0;
		for(int i=1;i<=n;i++){
			while(tp&&fL[stk[tp]][j-1]>=fL[i][j-1]){tp--;}
			stk[++tp]=i;
			fL[i][j]=fL[stk[lower_bound(stk+1,stk+1+tp,fL[i][j-1])-stk]][j-1];
		}
		tp=0;
		for(int i=1;i<=n;i++){
			while(tp&&fR[stk[tp]][j-1]<=fR[i][j-1]){tp--;}
			stk[++tp]=i;
			fR[i][j]=fR[stk[lower_bound(stk+1,stk+1+tp,fL[i][j-1])-stk]][j-1];
		}
		stk[tp=n+1]=n;
		for(int i=n;i>=1;i--){
			while(tp<n+1&&fR[stk[tp]][j-1]<=fR[i][j-1]){tp++;}
			stk[--tp]=i;
			fR[i][j]=max(fR[i][j],fR[stk[upper_bound(stk+tp,stk+1+n,fR[i][j-1])-(stk+1)]][j-1]);
		}
		stk[tp=n+1]=n;
		for(int i=n;i>=1;i--){
			while(tp<n+1&&fL[stk[tp]][j-1]>=fL[i][j-1]){tp++;}
			stk[--tp]=i;
			fL[i][j]=min(fL[i][j],fL[stk[upper_bound(stk+tp,stk+1+n,fR[i][j-1])-(stk+1)]][j-1]);
		}
	}
	int res=0;
	for(int i=1;i<=n;i++)
		FL[i]=FR[i]=i;
	for(int j=mx;j>=0;j--){
		tp=0;
		for(int i=1;i<=n;i++){
			while(tp&&fL[stk[tp]][j]>=fL[i][j]){tp--;}
			stk[++tp]=i;
			dtL[i]=fL[stk[lower_bound(stk+1,stk+1+tp,FL[i])-stk]][j];
		}
		tp=0;
		for(int i=1;i<=n;i++){
			while(tp&&fR[stk[tp]][j]<=fR[i][j]){tp--;}
			stk[++tp]=i;
			dtR[i]=fR[stk[lower_bound(stk+1,stk+1+tp,FL[i])-stk]][j];
		}
		stk[tp=n+1]=n;
		for(int i=n;i>=1;i--){
			while(tp<n+1&&fR[stk[tp]][j]<=fR[i][j]){tp++;}
			stk[--tp]=i;
			dtR[i]=max(dtR[i],fR[stk[upper_bound(stk+tp,stk+1+n,FR[i])-(stk+1)]][j]);
		}
		stk[tp=n+1]=n;
		for(int i=n;i>=1;i--){
			while(tp<n+1&&fL[stk[tp]][j]>=fL[i][j]){tp++;}
			stk[--tp]=i;
			dtL[i]=min(dtL[i],fL[stk[upper_bound(stk+tp,stk+1+n,FR[i])-(stk+1)]][j]);
		}
		preR[0]=n+1;
		bool tf=1;
		for(int i=1;i<=n;i++){
			preR[i]=min(preR[i-1],dtR[i]);
			if(preR[dtL[i]-1]<i)tf=0;
		}
		if(!tf){
			res|=(1<<j);
			for(int i=1;i<=n;i++)
				FL[i]=dtL[i],FR[i]=dtR[i];
		}
	}
	cout<<res+1;
	return 0;
}
一只 log
#include<bits/stdc++.h>
using namespace std;
const int N=1e5+5;
int n,a[N],fL[N][20],fR[N][20];
int FL[N],FR[N],preR[N];
int dtL[N],dtR[N],lg2[N];
int mnL[N][20],mxR[N][20],idL[N][20],idR[N][20];
int queL(int l,int r){
	int siz=lg2[r-l+1];
	if(mnL[l][siz]<=mnL[r-(1<<siz)+1][siz])return idL[l][siz];
	return idL[r-(1<<siz)+1][siz];
}
int queR(int l,int r){
	int siz=lg2[r-l+1];
	if(mxR[l][siz]>=mxR[r-(1<<siz)+1][siz])return idR[l][siz];
	return idR[r-(1<<siz)+1][siz];
}
int main(){
	//freopen("jump.in","r",stdin);
	//freopen("jump.out","w",stdout);
	ios::sync_with_stdio(0);
	cin.tie(0);cout.tie(0);
	cin>>n;
	lg2[1]=0;
	for(int i=2;i<=n;i++)
		lg2[i]=lg2[i>>1]+1;
	for(int i=1;i<=n;i++)
		cin>>a[i],fL[i][0]=max(1,i-a[i]),fR[i][0]=min(n,i+a[i]),
		mnL[i][0]=max(1,i-a[i]),mxR[i][0]=min(n,i+a[i]),
		idL[i][0]=idR[i][0]=i;
	for(int j=1;(1<<j)<=n;j++)
		for(int i=1;i+(1<<j)-1<=n;i++){
			mnL[i][j]=min(mnL[i][j-1],mnL[i+(1<<(j-1))][j-1]),
			mxR[i][j]=max(mxR[i][j-1],mxR[i+(1<<(j-1))][j-1]);
			if(mnL[i][j-1]<=mnL[i+(1<<(j-1))][j-1])idL[i][j]=idL[i][j-1];
			else idL[i][j]=idL[i+(1<<(j-1))][j-1];
			if(mxR[i][j-1]>=mxR[i+(1<<(j-1))][j-1])idR[i][j]=idR[i][j-1];
			else idR[i][j]=idR[i+(1<<(j-1))][j-1];
		}
	int mx=0;
	for(int j=1;(1<<j)<=n;j++){
		mx=max(mx,j);
		for(int i=1;i<=n;i++)
			fL[i][j]=min(fL[queL(fL[i][j-1],i)][j-1],fL[queR(i,fR[i][j-1])][j-1]),
			fR[i][j]=max(fR[queL(fL[i][j-1],i)][j-1],fR[queR(i,fR[i][j-1])][j-1]);
	}
	int res=0;
	for(int i=1;i<=n;i++)
		FL[i]=FR[i]=i;
	for(int j=mx;j>=0;j--){
		for(int i=1;i<=n;i++)
			dtL[i]=min(fL[queL(FL[i],i)][j],fL[queR(i,FR[i])][j]),
			dtR[i]=max(fR[queL(FL[i],i)][j],fR[queR(i,FR[i])][j]);
		preR[0]=n+1;
		bool tf=1;
		for(int i=1;i<=n;i++){
			preR[i]=min(preR[i-1],dtR[i]);
			if(preR[dtL[i]-1]<i)tf=0;
		}
		if(!tf){
			res|=(1<<j);
			for(int i=1;i<=n;i++)
				FL[i]=dtL[i],FR[i]=dtR[i];
		}
	}
	cout<<res+1;
	return 0;
}

ST 表

很有用,\(O(n\log n)\) 预处理,\(O(1)\) 查询,适用于一些运算场景中,重复出现的数字不会影响运算结果的情况,如 \(+,\oplus\) 不适用该结构,但是 \(\min,\max,\cup,\cap\) 等运算都适用。

这是预处理 \(\cup,\cap\) 的方法。

for(int j=1;(1<<j)<=n;j++)
	for(int i=1;i+(1<<j)-1<=n;i++){
		lor[i][j]=lor[i][j-1]|lor[i+(1<<(j-1))][j-1],
		land[i][j]=land[i][j-1]&land[i+(1<<(j-1))][j-1];
	}

应用

ST 表维护区间 \(\cup,\cap\)

Problem:给定一个长度为 \(n\) 的序列 \({a_n}\),求有多少对 \([l,r]\) 满足:

\[(a_l\cup a_{l+1}\cup\dotsc\cup a_r)\oplus (a_l\cap a_{l+1}\cap\dotsc\cap a_r)\geq \max(a_l,a_{l+1},\dotsc,a_r) \]

(多测,\(\sum n\le 10^6\)

观察到一个核心性质,一个区间向外扩展,它的贡献 \(W=(a_l\cup a_{l+1}\cup\dotsc\cup a_r)\oplus (a_l\cap a_{l+1}\cap\dotsc\cap a_r)\) 单调不减。利用这个性质,我们先对序列建出大根堆笛卡尔树,然后在笛卡尔树上选取 \(u\) 的左右子树中较小的一个进行暴力枚举(作为左端点或右端点),其对应的区间可以二分出来。根据启发式合并这样做总共是 \(O(n\log^2 n)\) 的,卡卡能过。

点击查看代码
#include<bits/stdc++.h>
using namespace std;
typedef long long LL;
const int N=1e6+5;
int n,rt;
LL a[N];
int stk[N],tp;
struct Node{
	int lc,rc,fa;
	LL val;
}t[N];
inline void clr(int x){t[x].lc=t[x].rc=t[x].fa=0;t[x].val=0;}
void insert(int x){
	while(tp&&t[stk[tp]].val<t[x].val)tp--;
	if(!tp){
		t[rt].fa=x;
		t[x].lc=rt;
		rt=x;
	}
	else {
		int u=stk[tp],rc=t[u].rc;
		t[rc].fa=x;t[x].lc=rc;
		t[u].rc=x;t[x].fa=u;
	}
	stk[++tp]=x;
}
int L[N],R[N],lg2[N];
LL lor[N][21],land[N][21];
LL qor(int l,int r){
	int siz=lg2[r-l+1];
	return lor[l][siz]|lor[r-(1<<siz)+1][siz];
}
LL qand(int l,int r){
	int siz=lg2[r-l+1];
	return land[l][siz]&land[r-(1<<siz)+1][siz];
}
LL ans;
void dfs(int u){
	L[u]=u,R[u]=u;
	if(t[u].lc)dfs(t[u].lc),L[u]=min(L[u],L[t[u].lc]);
	if(t[u].rc)dfs(t[u].rc),R[u]=max(R[u],R[t[u].rc]);
	if(u-L[u]+1<=R[u]-u+1){
		for(int i=L[u];i<=u;i++){
			int l=u,r=R[u],res=R[u]+1;
			while(l<=r){
				int mid=(l+r)>>1;
				if((qor(i,mid)^qand(i,mid))>=t[u].val)res=mid,r=mid-1;
				else l=mid+1;
			}
			ans+=R[u]-res+1;
		}
	}
	else {
		for(int i=u;i<=R[u];i++){
			int l=L[u],r=u,res=L[u]-1;
			while(l<=r){
				int mid=(l+r)>>1;
				if((qor(mid,i)^qand(mid,i))>=t[u].val)res=mid,l=mid+1;
				else r=mid-1;
			}
			ans+=res-L[u]+1;
		}
	}
}
int main(){
	//freopen("us.in","r",stdin);
	//freopen("us.out","w",stdout);
	ios::sync_with_stdio(0);
	cin.tie(0);cout.tie(0);
	int T;cin>>T;
	lg2[1]=0;t[0].val=(1ll<<61);
	for(int i=2;i<=N-5;i++)lg2[i]=lg2[i>>1]+1;
	while(T--){
		cin>>n;rt=0;ans=0;tp=0;
		for(int i=1;i<=n;i++)
			cin>>a[i],clr(i),
			t[i].val=a[i],insert(i),
			lor[i][0]=land[i][0]=a[i];
		for(int j=1;(1<<j)<=n;j++)
			for(int i=1;i+(1<<j)-1<=n;i++){
				lor[i][j]=lor[i][j-1]|lor[i+(1<<(j-1))][j-1],
				land[i][j]=land[i][j-1]&land[i+(1<<(j-1))][j-1];
			}
		dfs(rt);
		cout<<ans<<'\n';
	}
	return 0;
}

ST 表优化建图

P3295 [SCOI2016] 萌萌哒

考虑到我们可以维护一个并查集,然后对于每个 \(l_1+i,l_2+i\) 一一对应连边,在同一集合内就需要取相同值,这有点类似扩展域并查集的思想,我们发现承受不了这么多的一对一连边,这样是 \(O(n^2\alpha(n))\) 的,考虑优化,不少人应该会想到线段树优化建图类似的技巧,但是这里的连边是具有特殊性的,线段树优化建图能够解决的问题通常是一边多用的图上最短路/最长路的问题,然而这里需要一一连边保证对应性,所以我们引进了 ST 表优化建图。

具体来说,在赋值 \([l_1,r_1]\leftarrow [l_2,r_2]\) 时,用普通 ST 表将 \([l_1,r_1],[l_2,r_2]\) 分别拆成两个区间(总共四个),然后对这四个区间相应地两两连边,具体来说,不妨令 \(siz=\log_2(r_1-l_1+1)\),则并查集连接 \((siz,l_1)\to (siz,l_2),(siz,r_1-2^{siz} +1)\to (siz,r_2-2^{siz}+1)\)。ST 表的区间长度总共有 \(\log n\) 种,每个长度有 \(O(n)\) 个区间,相应地我们也需要针对每个 \((siz,i)\)\(O(n\log n)\)\(\text{father}\) 数组,然后在每一层上都初始化 \(fa(siz,i)=i\)

我们在解决询问时从上往下下放信息,区间长度从大到小遍历,如果记录到 \((k,i)(k>0)\) 连着别的点 \((k,fa)\),则说明 \(i\) 开头的与 \(fa\) 开头的长为 \(k\) 的区间需要一一对应连边,于是我们连接 \((k-1,i)\to (k-1,fa),(k-1,i+2^{k-1})\to (k-1,fa+2^{k-1})\)。每个节点的连边都是 \(O(\alpha(n))\) 的,瓶颈主要在处理区间,总时间复杂度 \(O(n\alpha(n)\log n)\)

#include<bits/stdc++.h>
using namespace std;
typedef long long LL;
const LL MOD=1e9+7;
const int N=1e5+5;
int n,m,rt[N][20],lg2[N];
inline int fr(int k,int x){
	if(rt[x][k]==x)return x;
	return rt[x][k]=fr(k,rt[x][k]);
}
void merge(int k,int x,int y){
	int frx=fr(k,x),fry=fr(k,y);
	if(frx==fry)return ;
	rt[fry][k]=frx;
}
bool vis[N];
LL ans=9;
int main(){
	ios::sync_with_stdio(0);
	cin.tie(0);cout.tie(0);
	cin>>n>>m;
	for(int i=2;i<=n;i++)
		lg2[i]=lg2[i>>1]+1;
	for(int i=1;i<=n;i++){
		for(int j=0;j<=lg2[n];j++){
			rt[i][j]=i;
		}
	}
	for(int i=1;i<=m;i++){
		int la,ra,lb,rb;
		cin>>la>>ra>>lb>>rb;
		int siz=lg2[ra-la+1];
		merge(siz,la,lb);
		merge(siz,ra-(1<<siz)+1,rb-(1<<siz)+1);
	}
	for(int k=lg2[n];k>0;k--){
		for(int i=1;i+(1<<k)-1<=n;i++){
			int id=i;
			int fa=fr(k,id);
			if(fa!=id){
				merge(k-1,fa,id);
				id=i+(1<<(k-1));
				fa=fa+(1<<(k-1));
				merge(k-1,fa,id);
			}
		}
	}
	vis[fr(0,1)]=1;
	for(int i=1;i<=n;i++)
		if(!vis[fr(0,i)])vis[fr(0,i)]=1,ans=ans*10ll%MOD;
	cout<<ans;
	return 0;
}

扩展:如果我们把操作改成钦定 \([l,r]\) 为回文子区间,那么我们大可以直接把序列倒着再复制一遍接到原序列后边,所有连边操作完成后连接对应的 \((siz,i)\to (siz,rev(i))\),然后判断即可。

posted @ 2025-08-07 21:31  TBSF_0207  阅读(16)  评论(0)    收藏  举报