P8415/P10151 SMV CC-64“蝰蛇”
题意
给序列 \(a_1,\dots,a_n\) 和排列 \(b_1,\dots,b_n\),共有 \(m\) 次操作:
修改操作:给定 \(x,y\),将 \(a_x\) 改为 \(y\);
查询操作:给定 \(l,r,x\),查区间 \([l,r]\) 内最长的子区间 \([l',r']\)(即满足 \(l\le l'\le r'\le r\)),使得对 \(l'\le i<r'\) 有 \(a_{i+1}=b_{a_i}\),且存在 \(l'\le i\le r'\) 使得 \(a_i=x\)。需要输出满足条件的 \(r'-l'+1\) 的最大值,若不存在则输出 \(0\)。
\(n,m\le 10^6 , 1\le a_i,b_i \le n\)
题解
显然 , 查询操作的限制是两部分 . 对于前一部分 , 可以发现整个序列被划分成了若干个合法段 , 只有这些合法段和它们的子段可能作为答案 . 每次单点修改只会对这个段造成 \(O(1)\) 次修改 , 可以直接用 set
维护 .
对于后一部分 , 要求段内有值 \(x\) , 这个限制把问题拓展了一个值域维度 . 为了简化问题可以先单独处理区间两端不完整的合法段 , 其余在中间的完整段必然整段贡献 .
这样 , 这些完整段的贡献可以挂在端点上 , 于是问题变成了在序列上若干个位置 , 每个位置覆盖了值域上的若干个点 , 求序列区间中包含了 \(x\) 的所有位置权值最大值 , 这显然是个二维问题 , 直接分成序列 - 值域两个维度考虑 , 发现这相当于在平行于值域轴的直线上修改 , 在平行于序列轴的一条线段上查询最大值 .
现在的修改不够美观 , 发现一个合法段内部元素肯定是一个置换环上的连续一段 , 因此把值域维度映射到把所有置换环展开后前后连接 , 这样每一个合法段的值域在值域上只有 \(O(1)\) 个区间 .
用线段树套平衡树维护二维平面 . 线段树维护值域维 , 内部平衡树维护序列 . 运用标记永久化的技巧 , 在值域区间上的 $O(\log n) $ 个节点上 , 插入或删除序列一个位置的权值 . 查询时 , 在线段树从根到 \(x\) 对应叶子的链上全都查询一遍 .
时间复杂度 \(O(n\log^2 n)\) ,空间复杂度 $O(n \log n) $ .
细节不少 :
- 用
set
维护每一个极长合法段的端点 , 每次单点修改时要分别考虑左右 , 从联通到不连通或者从不连通到联通 . - 注意空间常数 , 因为这道题里插入的线段是会删除的 , 最坏情况下 , 每次修改要进行 3 次删除 , 因此空间最坏情况下带一个 \(4\) 倍常数 , 当然也可以写空间回收 .
- 查询不完整段 , 可以用
set
维护值为 \(x\) 的所有下标 , 这样就可以容易地判定不完整段是否合法 .
点击查看代码
#include<bits/stdc++.h>
#define file(x) freopen(#x ".in","r",stdin),freopen(#x ".out","w",stdout)
#define ll long long
#define INF 0x7fffffff
#define INF64 1e18
using namespace std;constexpr int N=1e6+5;int n,m;struct node{int ls,rs,rnd;pair<int,int> val;int mx;
}a[N*80];inline void pushup(int x){a[x].mx=max(a[a[x].ls].mx,max(a[a[x].rs].mx,a[x].val.second));
}int at;int add(int pos,int val){a[++at]={0,0,rand(),{pos,val},val};return at;
}int merge(int x,int y){if(x==0||y==0) return x+y;if(a[x].rnd<=a[y].rnd){a[x].rs=merge(a[x].rs,y);pushup(x);return x;}a[y].ls=merge(x,a[y].ls);pushup(y);return y;
}void split(int u,pair<int,int> k,int &x,int &y){if(!u){ x=y=0;return;}if(a[u].val<=k){x=u;split(a[u].rs,k,a[x].rs,y);pushup(x);}else{y=u;split(a[u].ls,k,x,a[y].ls);pushup(y);}
}struct tnode{int ls,rs,rt;
}t[N*4];void build(int x,int l,int r){t[x].rt=0;if(l==r) return;t[x].ls=x*2;t[x].rs=x*2+1;int mid=l+r>>1;build(t[x].ls,l,mid);build(t[x].rs,mid+1,r);
}void update(int x,int L,int R,int l,int r,int pos,int val){if(l<=L&&R<=r){int p=0,q=0;split(t[x].rt,{pos,val},p,q);t[x].rt=merge(p,merge(add(pos,val),q));}else if(!(r<L||R<l)){int mid=L+R>>1;update(t[x].ls,L,mid,l,r,pos,val);update(t[x].rs,mid+1,R,l,r,pos,val);}
}int query(int x,int L,int R,int pos,int lp,int rp){int p=0,q=0,u=0,v=0;split(t[x].rt,{lp-1,n+1},p,q);split(q,{rp,n+1},u,v);int res=a[u].mx;t[x].rt=merge(p,merge(u,v));if(L==R) return res;int mid=L+R>>1;if(pos<=mid) res=max(res,query(t[x].ls,L,mid,pos,lp,rp));else res=max(res,query(t[x].rs,mid+1,R,pos,lp,rp) );return res;
}void remove(int x,int L,int R,int l,int r,int pos,int val){if(l<=L&&R<=r){int p=0,q=0,u=0,v=0;split(t[x].rt,{pos,val-1},p,q);split(q,{pos,val},u,v);t[x].rt=merge(p,v);}else if(!(r<L||R<l)){int mid=L+R>>1;remove(t[x].ls,L,mid,l,r,pos,val);remove(t[x].rs,mid+1,R,l,r,pos,val);}
}int b[N],p[N];int dfn[N],rnk[N],tot,len[N],rnd[N],L[N],R[N];set<int> st;
set<int> pos[N];inline void solve(int l,int r,int x,int y){int ln=y-x+1;int tl=dfn[l],tr=dfn[r];if(ln>=len[rnd[l]]){update(1,1,n,L[rnd[l]],R[rnd[l]],x,ln);return;}if(tl+ln-1<=R[rnd[l]]) update(1,1,n,tl,tr,x,ln);else {update(1,1,n,tl,R[rnd[l]],x,ln);update(1,1,n,L[rnd[l]],tr,x,ln);}
}inline void delt(int l,int r,int x,int y){int ln=y-x+1;int tl=dfn[l],tr=dfn[r];if(ln>=len[rnd[l]]){remove(1,1,n,L[rnd[l]],R[rnd[l]],x,ln);return;}if(tl+ln-1<=R[rnd[l]]) remove(1,1,n,tl,tr,x,ln);else {remove(1,1,n,tl,R[rnd[l]],x,ln);remove(1,1,n,L[rnd[l]],tr,x,ln);}
}inline void modify(int x,int y){if(b[x]==y) return;auto it=st.lower_bound(x);int lp,rp;rp=*it;lp=*(--it);delt(b[lp+1],b[rp],lp+1,rp);if(x==rp){if(x!=n&&b[x+1]==p[y]){auto itt=st.upper_bound(rp);delt(b[rp+1],b[*itt],rp+1,*itt);st.erase(rp);rp=*itt;}}else{if(b[x+1]!=p[y]){solve(b[x+1],b[rp],x+1,rp);st.insert(x);rp=x;}}if(x==lp+1){if(x!=1&&y==p[b[x-1]]){auto itt=prev(st.lower_bound(lp));delt(b[(*itt)+1],b[lp],(*itt)+1,lp);st.erase(lp);lp=*itt;}}else{if(y!=p[b[x-1]]){solve(b[lp+1],b[x-1],lp+1,x-1);st.insert(x-1);lp=x-1;}}pos[b[x]].erase(x);pos[y].insert(x);b[x]=y;solve(b[lp+1],b[rp],lp+1,rp);
}inline int ask(int l,int r,int x){int lp=*st.lower_bound(l),rp=(*prev(st.lower_bound(r)))+1;if(lp>=r){auto it=pos[x].lower_bound(l);if(it==pos[x].end()||(*it>r)) return 0;return r-l+1;}int res=0;auto it=pos[x].lower_bound(l);if(it!=pos[x].end()&&(*it<=lp) ) res=max(res,lp-l+1);it=pos[x].lower_bound(rp);if(it!=pos[x].end()&&(*it<=r) ) res=max(res,r-rp+1);if(lp+1<=rp-1) res=max(res, query(1,1,n,dfn[x],lp+1,rp-1)) ;return res;
}int main(){ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);cin>>n>>m;for(int i=1;i<=n;i++) cin>>b[i];for(int i=1;i<=n;i++) cin>>p[i];for(int i=1;i<=n;i++) L[i]=n+1,R[i]=0;for(int i=1;i<=n;i++){int t=i;while(!dfn[t]){rnd[t]=i;len[i]++;dfn[t]=++tot; rnk[tot]=t;L[i]=min(L[i],tot);R[i]=max(R[i],tot);t=p[t];}}build(1,1,n);st.insert(0);for(int i=1;i<=n;i++){pos[b[i]].insert(i);if(i==n||b[i+1]!=p[b[i]]){int last;last=(*prev(st.end()));solve(b[last+1],b[i],last+1,i);st.insert(i);}}for(int i=1;i<=m;i++){int op,x,y,c;cin>>op>>x>>y;if(op==1){modify(x,y);}else{cin>>c;cout<<ask(x,y,c)<<'\n';}}
}
总结
lxl 把这道题扔到了模拟赛里 , 然而这道题只是 3 道题里第二难的 .
其实这道题和极度毒瘤的数据结构比还算简单很多 , 因为这道题的思路比较自然 , 基本全程都是对着问题解决问题 , 能简化的就简化 . 最智慧的一步是把值域散点 , 通过映射到置换环上 , 转化为区间 .要利用好题目的特性 , 转化和找性质不分家 , 要时刻落实到题目的性质上 .
但是实现难度还是有的 , 而且要时刻注意常数 , 尤其是空间常数 . 还是要求一定代码能力的 , 能结构化的一定结构化 , 时刻写常数最小的方法 , 要能耐得住性子讨论 .