莫队(板子)
莫队
玄学暴力区间操作算法
PPT解释的很清楚啦~, 导致我没什么可写的 \(qwq\)
把所有询问离线下来后排序(左端点按块,右端点升序),然后从一个小区间通过左右端点的移动扩大区间,更新答案。
复杂度主要在区间扩展,也就是左右指针的移动,对于莫队所有的优化几乎都是调整分块或排序减少指针移动次数。
本质就是暴力,但是复杂度太玄学了,完美剪枝。
排序巧妙优化复杂度,带来NOIP前的最后一丝宁静。几个活蹦乱跳的指针的跳跃次数,决定着莫队算法的优劣……
1.奇偶化排序
对于每一个块来说,询问的右端点都是从小到大排序,
每次遍历完一个块右指针指在最右端,然后又千里迢迢跑回最左端,中间完全没干活。
也就是右指针只有从左到右移动是有效的,从右到左的移动完全没用。
所以加大右端点工作效率,右端点对于奇数块升序排序,对于偶数块降序排序。
这样右端点就24小时全程加班,大大减少移动次数。
最佳分块大小: \(\frac{n}{\sqrt{m}}\)
(例题: HH的项链)
#include<bits/stdc++.h>
using namespace std;
const int N = 50005, M = 200005;
int n,m,a[N],l=1,r=0,cnt[1000005],tmp;
struct Q
{
int l,r,id;
} q[M];
int ans[M];
int L[10000],R[10000],bl[N];
void bui()//分块
{
int cnt=n/sqrt(m);
for(int i=1;i<=cnt;i++)
{
L[i]=n/cnt*(i-1)+1;
R[i]=n/cnt*i;
}
R[cnt]=n;
for(int i=1;i<=cnt;i++)
for(int j=L[i];j<=R[i];j++)
bl[j]=i;
}
bool cmp(Q &x,Q &y)
{
if(bl[x.l]!=bl[y.l]) return bl[x.l]<bl[y.l];
if(bl[x.l] & 1) return x.r<y.r;//奇偶化
return x.r>y.r;
}
void add(int x)
{
if(!cnt[x]) tmp++;
cnt[x]++;
}
void del(int x)
{
cnt[x]--;
if(!cnt[x]) tmp--;
}
int main()
{
scanf("%d",&n);
for(int i=1;i<=n;i++) scanf("%d",&a[i]);
scanf("%d",&m);
bui();//注意调用
for(int i=1;i<=m;i++)
{
int x,y;
scanf("%d%d",&x,&y);
q[i]={x,y,i};
}
sort(q+1,q+1+m,cmp);
for(int i=1;i<=m;i++)
{
int x=q[i].l,y=q[i].r;
while(l>x) add(a[--l]);
while(l<x) del(a[l++]);
while(r>y) del(a[r--]);
while(r<y) add(a[++r]);
ans[q[i].id]=tmp;
}
for(int i=1;i<=m;i++) printf("%d\n",ans[i]);
return 0;
}
带修莫队
众所周知,莫队是一种离线算法,不支持修改。
但是莫队这种 暴力+优化 的思想仍可以应用。
我们从最简单的思路想,如果把修改也离线下来呢?
那也就是在 \(l,r\) 轴以外再加一维 \(t\) 轴,维护当前的修改数。
这就是带修莫队,把二维的区间变成三维。
那么怎么实现时间轴的移动呢?
首先我们要记录每一次查询是在哪一次修改后,
然后判断本次修改对当前区间的影响:
-
修改位置在当前区间外,没有影响。
-
修改位置在当前区间内,那也就是删去一个修改位置的值,添加一个修改后的值。
最后记得把原数组也修改。
这里有一个很巧妙的操作,对于原数组,需要支持两种操作(改过去和改回来),
我们可以把修改值和原值交换,那么两种操作其实就合并成一种了。
至于排序时,左右端点都按分块排,相同比时间轴。
最佳分块大小: \(n^{\frac{2}{3}}\) ( \(cnt=n^{\frac{1}{3}}\))
(例题:数颜色)
#include<bits/stdc++.h>
using namespace std;
const int N = 1e6+6,M = 133335;
int n,m,a[M],cnt[N],mq,mre,tmp,l=1,r=0,t=0,ans[M];
struct Q
{
int l,r,t,id;
} q[M];
struct R
{
int x,c;
} re[M];
int L[3005],R[3005],bl[M];
void bui()
{
int cnt=pow(n,1.0/3);
for(int i=1;i<=cnt;i++)
{
L[i]=n/cnt*(i-1)+1;
R[i]=n/cnt*i;
}
R[cnt]=n;
for(int i=1;i<=cnt;i++)
for(int j=L[i];j<=R[i];j++)
bl[j]=i;
}
bool cmp(Q &x,Q &y)
{
if(x.l!=y.l) return bl[x.l]<bl[y.l];
if(x.r!=y.r) return bl[x.r]<bl[y.r];
return x.t<y.t;
}
void add(int x)
{
if(!cnt[x]) tmp++;
cnt[x]++;
}
void del(int x)
{
cnt[x]--;
if(!cnt[x]) tmp--;
}
void mdf(int x)
{
if(re[x].x>=l&&re[x].x<=r)
{
del(a[re[x].x]);
add(re[x].c);
}
swap(a[re[x].x],re[x].c);
}
int main()
{
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++) scanf("%d",&a[i]);
for(int i=1;i<=m;i++)
{
int x,y;
char c;
scanf(" %c%d%d",&c,&x,&y);
if(c=='Q') q[++mq]={x,y,mre,mq};
else re[++mre]={x,y};
}
bui();
sort(q+1,q+1+mq,cmp);
for(int i=1;i<=mq;i++)
{
int x=q[i].l,y=q[i].r,now=q[i].t;
while(l>x) add(a[--l]);
while(l<x) del(a[l++]);
while(r<y) add(a[++r]);
while(r>y) del(a[r--]);
while(t>now) mdf(t--);
while(t<now) mdf(++t);
ans[q[i].id]=tmp;
}
for(int i=1;i<=mq;i++) printf("%d\n",ans[i]);
return 0;
}
回滚莫队
回滚莫队用于处理一些只能进行 \(add\) 或 \(del\) 其中一种操作的。
我们将每一个块的左端点或右端点设为基点,保留这个状态,然后每次 "滚回来"。
之前写过博客 回滚莫队
最佳分块大小: \(\sqrt{n}\) (???)
(例题:历史研究)
#include<bits/stdc++.h>
using namespace std;
typedef long long LL;
const int N = 1e5+5;
int n,m;
int a[N],cnt[N],cntb[N],f[N],tot;
LL tmp,ans[N];
int l,r;
map<int,int> mp;
struct Q
{
int l,r,id;
} q[N];
int L[4000],R[4000],bl[N];
void bui()
{
int cnt = sqrt(n);
for(int i=1;i<=cnt;i++)
{
L[i]=n/cnt*(i-1)+1;
R[i]=n/cnt*i;
}
R[cnt]=n;
for(int i=1;i<=cnt;i++)
{
for(int j=L[i];j<=R[i];j++)
{
bl[j]=i;
}
}
}
void add(int x)
{
cnt[x]++;
tmp=max((LL)cnt[x]*f[x],tmp);
}
bool operator < (Q &x,Q &y)
{
if(bl[x.l]!=bl[y.l]) return bl[x.l]<bl[y.l];
else return x.r<y.r;
}
void mo_q()
{
for(int i=1;i<=m;)
{
memset(cnt,0,sizeof(cnt)); tmp=0;
int j=i; int right=bl[q[i].l];
while(right==bl[q[j].l]&&j<=m) j++;//找到左端点在同一块内的
r=R[right]; l=r+1;
while(i<j)
{
if(q[i].r<=R[right])//同一块内
{
for(int k=q[i].l;k<=q[i].r;k++) add(a[k]);
ans[q[i].id]=tmp;
tmp=0; memset(cnt,0,sizeof(cnt));
i++;
}
else//不同块
{
while(r<q[i].r) add(a[++r]);
memcpy(cntb,cnt,sizeof(cnt));
LL bbb=tmp;
while(l>q[i].l) add(a[--l]);
ans[q[i].id]=tmp; i++;
tmp=bbb;
l=R[right]+1;
memcpy(cnt,cntb,sizeof(cntb));
}
}
}
}
int main()
{
freopen("a.in","r",stdin);
freopen("a.out","w",stdout);
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++)
{
scanf("%d",&a[i]);
if(mp[a[i]]==0)
{
mp[a[i]]=++tot;
f[tot]=a[i];
a[i]=tot;
}
else a[i]=mp[a[i]];
}
bui();//注意调用
for(int i=1;i<=m;i++)
{
int x,y;
scanf("%d%d",&x,&y);
q[i]=Q{x,y,i};
}
sort(q+1,q+1+m);
mo_q();//注意调用
for(int i=1;i<=m;i++) printf("%lld\n",ans[i]);
return 0;
}
完结撒花 ✿ヽ(°▽°)ノ✿
好像还有个树上莫队,遇到再写吧。
\(\mathbb{mo\_q}\)