bzoj4358.permu 回滚莫队+可删除并查集(简易)|详细解析
今天学了回滚莫队,为什么会有回滚莫队呢?
比如普通莫队求max时就很不好撤销,那我们就换个思路,干脆不删了,通过调整枚举顺序进行暴力加点
回滚莫队的思想主要是这段代码:
bool cmp(const query& a,const query& b){ if(bel[a.l]==bel[b.l]) return a.r<b.r; else return bel[a.l]<bel[b.l]; }
首先按左端点所属的块号排序,然后块号相同的按右端点排序
移动的时候:
1.假设左右端点在同一个块内,因为块的大小不超过sqrt(n),所以直接暴力扫
2.假设此次询问的左端点和上次询问的左端点不在同一个块内,那么令:
l=R[bel[q[i].l]]+1; r=R[bel[q[i].l]];
然后开始跳到询问区间即可
3.假设此次询问的左端点和上一次询问的左端点在同一个块内,那么分别考量l和r
r一定是递增的(想想为什么?我们排序的时候保证了),我们就可以直接更新
但是l就不一定了,是乱序的
虽然是乱序,但也可以暴力跳,因为已经保证了在同一个块内了,暴力跳的话也不会超过sqrt(n)次
我们再设一个临时变量__l=l,跳的时候让__l去跳,而不是让l去跳
这样就保证了到下一次询问时,又能从l开始跳了。
所以大概的移动可能是长这样的:
(呜呜没有数位板)
时间复杂度:r的更新最多只有n次,而l也最多只会跳sqrt(n)次,总的还是n*sqrt(n)
话说回本题上来:
要求最长的连续值域
维护一个并查集,更新时判一下a[pos]-1,和a[pos]+1当前有没有vis到,有的话就塞进并查集里
但是要注意,__l回滚的时候,不仅要撤销vis的标记
还要撤销对并查集的改动,这个可以开个stack记录一下
具体的,如何维护并查集的撤销呢?
在__l往左边跳时,把此时的栈顶当做相对栈底,记作bottom
merge(x,y)的时候把sz小的x压进栈里
撤销时只要:
while(s.size()>bottom){ int u=s.top();s.pop(); siz[fa[u]]-=siz[u]; fa[u]=u; // siz[u]=1; }
很重要的一点是,不能写路径压缩!错误性自己模拟一下就知道了:D
另,这种写法适用范围不是很广,甚至不是板子,因为只适用于小数据,比如这题的sqrt(1e5),不过在这里也够用了
#include<bits/stdc++.h> #define fastio ios_base::sync_with_stdio(false);cin.tie(0);cout.tie(0) using namespace std; typedef long long ll; const int maxn=1e5+5; int out[maxn],dp[maxn],use[maxn]; int n,Q,sz,tot,L[maxn],R[maxn],bel[maxn],a[maxn],vis[maxn]; struct query{ int l,r,id; }q[maxn]; stack<int>s; int fa[maxn],siz[maxn]; void init(){ for(int i=1;i<=n;i++) fa[i]=i,siz[i]=1; } int find(int x){ if(fa[x]!=x) return find(fa[x]); else return fa[x]; } int ask(int u){ return siz[find(u)]; } void merge(int x,int y){ int fx=find(x),fy=find(y); if(siz[fx]>siz[fy]) swap(fx,fy); siz[fy]+=siz[fx]; fa[fx]=fy; s.push(fx); } bool cmp(const query& a,const query& b){ if(bel[a.l]==bel[b.l]) return a.r<b.r; else return bel[a.l]<bel[b.l]; } void build(){ sz=sqrt(n); tot=n/sz; for(int i=1;i<=tot;i++){ L[i]=(i-1)*sz+1; R[i]=i*sz; } if(R[tot]<n) { tot++; L[tot]=R[tot-1]+1; R[tot]=n; } for(int i=1;i<=tot;i++) for(int j=L[i];j<=R[i];j++) bel[j]=i; } void add(int pos,int& ans){ vis[a[pos]]=1; if(vis[a[pos]-1]){ merge(a[pos]-1,a[pos]); } if(vis[a[pos]+1]){ merge(a[pos],a[pos]+1); } ans=max(ans,ask(a[pos])); } int main(){ //freopen("lys.in","r",stdin); fastio; cin>>n>>Q; build(); init(); for(int i=1;i<=n;i++) cin>>a[i]; for(int i=1;i<=Q;i++){ cin>>q[i].l>>q[i].r;q[i].id=i; } sort(q+1,q+Q+1,cmp); int l=1,r=0; int last_block=0; int ans=0; for(int i=1;i<=Q;i++){ //同一块直接暴力扫~ if(bel[q[i].l]==bel[q[i].r]){ int tmp=0; for(int j=q[i].l;j<=q[i].r;j++){ use[++tmp]=a[j]; } sort(use+1,use+tmp+1); use[0]=dp[0]=0; out[q[i].id]=dp[1]=1; for(int j=2;j<=tmp;j++){ if(use[j]==use[j-1]+1) dp[j]=dp[j-1]+1; else dp[j]=1; out[q[i].id]=max(out[q[i].id],dp[j]); } continue; } // 如果进入了新块 if(bel[q[i].l]!=last_block){ // 觉得这边直接memset也可——但是可能会t掉 l=R[bel[q[i].l]]+1; r=R[bel[q[i].l]]; init(); memset(vis,0,sizeof(vis)); last_block=bel[q[i].l]; ans=1; } int __l=l;// 这里l其实是固定的,可以发现只有在块间移动时才会变化 // 回滚其实都是通过__l来实现 while(r<q[i].r) add(++r,ans); int tmp=ans; int bottom=s.size(); while(__l>q[i].l) { // do something add(--__l,tmp); } out[q[i].id]=tmp; while(s.size()>bottom){ int u=s.top();s.pop(); siz[fa[u]]-=siz[u]; fa[u]=u; // siz[u]=1; } while(__l<l) vis[a[__l++]]=0; } for(int i=1;i<=Q;i++) cout<<out[i]<<endl; }
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 无需6万激活码!GitHub神秘组织3小时极速复刻Manus,手把手教你使用OpenManus搭建本
· C#/.NET/.NET Core优秀项目和框架2025年2月简报
· Manus爆火,是硬核还是营销?
· 终于写完轮子一部分:tcp代理 了,记录一下
· 【杭电多校比赛记录】2025“钉耙编程”中国大学生算法设计春季联赛(1)