(简记)二分答案 二分查找 倍增 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 表优化建图
考虑到我们可以维护一个并查集,然后对于每个 \(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))\),然后判断即可。

浙公网安备 33010602011771号