莫队入门
莫队是个神奇的东西~~
有了他,离线问答再不怕,\(n\sqrt{n}\)秒天下~~
作为一个可与分块相论的优美的暴力,让我们一起学习,一起探索吧!
好的,废话不多说(这还少吗),让我们进入正题~
0. 你需要哪些知识
会写"Hello World!"
学习莫队的最低要求:
- 学的是\(C++\),\(P\)党勿入
- 会熟练运用\(sort\)的进阶用法(为自定义结构体排序自定义函数等)
推荐要求:
- 满足最低要求
- 熟练运用离散化(因为个别毒瘤的出题人会扩大数据范围)
- 会各种卡常技巧
看来,莫队的要求还是不高的。
1. 莫队入门
先来看一道题:P1494 [国家集训队]小Z的袜子
看到国家集训队,不禁瑟瑟发抖......
(这还是入门吗......)
思路一:暴力
代码就不用放了,相信各位一定能够打出来的!
最坏时间复杂度:\(O(nm)\) (当然,如果你能\(n\)方过百万,就当我没说)
思路二:优雅一点的暴力
我们可以思考一下,为什么思路一非常低效?
因为它计算了重复的元素。
比如,我第一问问到了\([1,n]\),第二问问到了\([1,n-1]\),如果按照思路一,第二问就要推倒重算,根本没有利用到第一问所得到的信息。
那么,怎么才能利用询问得到的信息呢?
对于每次询问,我们先不直接枚举,而是定义两个指针\(l,r\),设计数数组为\(cnt\),存储当前区间\([l,r]\)出现的各个颜色出现的次数。第一次询问之前,初始化\(l=1,r=0\)(至于为什么,待会儿讲)。
每一次询问,我们移动\(l,r\),直至\([l,r]\)与询问区间重合。每次移动后,统计一下移动前与移动后答案的差。
具体落实到图上,是这样的:(以下图中所标颜色均为离散化后的,当前答案指\([l,r]\)能找到的同色袜子对数)
就这样,我们避免了区间\([5,9]\)的重复计算。
回到之前的问题,为什么初始化\(l=1,r=0\)呢?
因为如果初始化\(l=r=1\),实际上\([l,r]\)这个区间里面还有\(1\)个数,我们还得手动把这个数统计一下,麻烦。
(其实就是偷懒)
看起来,我们应该优化了不少(雾)。
但仔细思考一下,貌似还是不太行。因为最坏情况下,对于每一个询问,\(l,r\)要移动\(n\)次。比如这样:
时间复杂度依然是\(O(nm)\)。
\(50000\times 50000\) 嘛,常数国国王也救不了你,除非你用指令集优化,但那不是一般人用的,算了吧。
所以,我们仍需另辟蹊径。
思路三:传说中的莫队!
有人问,现在才步入正题,我前面岂不是白看了?
NoNoNo,前面都是打好基础。
我们先思考一下,为什么思路二还是比较低效?
因为询问序列是无序的,很有可能两个询问区间隔的太远,导致指针来回频繁移动。
那怎么办?
把询问变成有序的!
所以,莫队比起思路二,只差两步。而这两步,是莫队的精髓所在,也是决定莫队效率的关键。
那就是——
- 对整个区间进行分块。
- 对询问排序,把询问变得相对有序。排序方法:先按照左端点所属的块从小到大排,再按右端点从小到大排。
排序完后,按照方法2那样搞就行了。
这样,从一个询问区间移动至下一个,左指针、右指针平均移动了\(\sqrt{n}\) 次,复杂度为\(O(n\sqrt{n})\)。
(当然,具体时间复杂度取决于块大小,一般取\(n^{2 \over 3}\)为宜。
欢乐的代码时间:
#include <iostream>
#include <cstdio>
#include <algorithm>
#include <cmath>
#include <cctype>
#define re register
using namespace std;
int n,m,col[50010],L[50010],R[50010],tot,len,belong[50010];
long long cnt[50010];
struct Question{
long long l,r,ans,index;
}q[50010];
inline long long read(){
int s=0;
char ch=getchar();
while(!isdigit(ch)){
ch=getchar();
}
while(isdigit(ch)) {
s=s*10+(ch^48);
ch=getchar();
}
return s;
}
inline void build(){
len=sqrt(m);tot=m/len;
for(re int i=1;i<=tot;i++){
L[i]=R[i-1]+1,R[i]=L[i]+len-1;
}
if(R[tot]<m) L[++tot]=R[tot-1]+1,R[tot]=m;
for(re int i=1;i<=tot;i++){
for(int j=L[i];j<=R[i];j++) belong[j]=i;
}
}
inline bool cmp(const Question& a,const Question& b){
return (belong[a.l]^belong[b.l])?belong[a.l]<belong[b.l]:((belong[a.l]&1)?a.r<b.r:a.r>b.r);//排序
}
inline bool cmp1(const Question& a,const Question& b){
return a.index<b.index;//要按询问顺序输出答案
}
long long gcd(long long a,long long b){
return b?gcd(b,a%b):a;
}
int main(){
scanf("%d%d",&n,&m);
for(re int i=1;i<=n;i++){
col[i]=read();
}
for(re int i=1;i<=m;i++){
q[i].l=read(),q[i].r=read();
q[i].index=i;
}
build();
sort(q+1,q+m+1,cmp);
int l=1,r=0,ans=0;
for(re int i=1;i<=m;i++){
while(l<q[i].l){
ans-=cnt[col[l]]*(cnt[col[l]]-1)/2-(cnt[col[l]]-1)*(cnt[col[l]]-2)/2;
cnt[col[l++]]--;
}
while(l>q[i].l){
cnt[col[--l]]++;
ans+=cnt[col[l]]*(cnt[col[l]]-1)/2-(cnt[col[l]]-1)*(cnt[col[l]]-2)/2;
}
while(r<q[i].r){
cnt[col[++r]]++;
ans+=cnt[col[r]]*(cnt[col[r]]-1)/2-(cnt[col[r]]-1)*(cnt[col[r]]-2)/2;
}
while(r>q[i].r){
ans-=cnt[col[r]]*(cnt[col[r]]-1)/2-(cnt[col[r]]-1)*(cnt[col[r]]-2)/2;
cnt[col[r--]]--;
}
q[i].ans=ans;
}
sort(q+1,q+m+1,cmp1);
for(re int i=1;i<=m;i++){
long long x=(q[i].r-q[i].l+1)*(q[i].r-q[i].l);
if(x==0) puts("0/1");
else{
long long xy=gcd(x,q[i].ans*2);
printf("%lld/%lld\n",q[i].ans*2/xy,x/xy);
}
}
return 0;
}
2.莫队进阶
2.1 带修莫队
你以为到这里就结束了?当然不是。
先来看一个问题:P1903 [国家集训队]数颜色 / 维护队列
我们发现,中间涉及到了对序列的修改,显然不可能用只适用于静态问题的普通莫队来解决。
难道莫队就无用武之地了吗?当然不是。
带修莫队就此登场!思路如下:
首先,我们记录每一次询问前最后一次修改的序号(以下称为时间戳)。同时,按照输入顺序执行每一次修改,并同时记录该修改所造成的影响(在本题中,影响指该修改对应位置执行前后的数值变化)
接下来,进行关键的莫队环节。我先排序,记得还要判断询问区间时间戳的大小关系。们除了定义\(l,r\)两个指针以外,还要定义一个时间戳指针\(t\)。每一次询问,我们移动\(l,r,t\),(当\(t\)减小就撤回对应修改操作,当\(t\)增加就执行对应修改操作)直至\([l,r,t]\)与询问区间重合。每次移动后,统计一下移动前与移动后答案的差,保存即可。
说白了,就是在原来莫队的基础上加了一维,变成了三维空间的莫队而已。
Code:
#include <iostream>
#include <cstdio>
#include <cmath>
#include <cstring>
#include <algorithm>
using namespace std;
int n,m,cnt[1000005],val[1000005],tmp[1000005],res[1000005];
int totm,totq,ans;
int len,tot,L[10005],R[10005],belong[1000005];
struct Question{
int l,r,ti,id;
}q[1000005];
struct Modify{
int x,old,ne;
}mo[1000005];
void build(){
len=pow(n,2.0/3);tot=n/len;
for(int i=1;i<=tot;i++){
L[i]=R[i-1]+1,R[i]=L[i]+len-1;
}
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++) belong[j]=i;
}
}
bool cmp(const Question& a,const Question& b){
return belong[a.l]!=belong[b.l]?belong[a.l]<belong[b.l]:(belong[a.r]!=belong[b.r]?belong[a.r]<belong[b.r]:a.ti<b.ti); //排序函数
}
int main(){
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++)
scanf("%d",&val[i]);
memcpy(tmp,val,sizeof(val));
for(int i=1;i<=m;i++){
char ch[2];
int x,y;
scanf("%s%d%d",ch,&x,&y);
// totq是当前已读入的询问总数,totm是当前已读入的修改次数
if(ch[0]=='Q'){
q[++totq].l=x,q[totq].r=y,q[totq].id=totq,q[totq].ti=totm; //询问区间,保存一下
}
else{
mo[++totm].x=x,mo[totm].old=tmp[x],mo[totm].ne=y,tmp[x]=y; //修改操作,执行当前操作并记录修改位置的前后差异
}
}
memcpy(tmp,val,sizeof(val));
build(); //分块
sort(q+1,q+totq+1,cmp);
int l=1,r=0,ti=0;
for(int i=1;i<=totq;i++){
while(r>q[i].r){
if(cnt[tmp[r]]==1) ans--;
cnt[tmp[r--]]--;
}
while(r<q[i].r){
cnt[tmp[++r]]++;
if(cnt[tmp[r]]==1) ans++;
}
while(l<q[i].l){
if(cnt[tmp[l]]==1) ans--;
cnt[tmp[l++]]--;
}
while(l>q[i].l){
cnt[tmp[--l]]++;
if(cnt[tmp[l]]==1) ans++;
}
while(ti<q[i].ti){ //修改时间戳指针
ti++;
if(mo[ti].x<=r&&mo[ti].x>=l){
if(cnt[mo[ti].old]==1) ans--;
cnt[mo[ti].old]--;
if(cnt[mo[ti].ne]==0) ans++;
cnt[mo[ti].ne]++;
}
tmp[mo[ti].x]=mo[ti].ne;
}
while(ti>q[i].ti){
if(mo[ti].x<=r&&mo[ti].x>=l){
if(cnt[mo[ti].old]==0) ans++;
cnt[mo[ti].old]++;
if(cnt[mo[ti].ne]==1) ans--;
cnt[mo[ti].ne]--;
}
tmp[mo[ti].x]=mo[ti].old;
ti--;
}
res[q[i].id]=ans;
}
for(int i=1;i<=totq;i++) printf("%d\n",res[i]); //输出
return 0;
}
2.2 回滚莫队
有时,我们发现询问区间转移过程中,会遇到添加值困难/减少值困难的情况。这该怎么办呢?
例题:AT1219 歴史の研究
我们发现,在本题中,添加一个值挺容易,减少一个值...不会。
我们观察莫队的性质:左端点在同一块中的所有查询区间右端点单调递增。
这样,对于左端点在同一块中的每个区间,我们可以一次遍历,直接\(O(n)\)解决掉所有的右端点,而且不用减少值。
接着,对于每个块内的左端点,假设每个块内的每个左端点都从块右端开始统计,每次都重新开始暴力统计一次,做完每个左端点复杂度\(O(\sqrt{n})\),共\(n\)个左端点,总复杂度\(O(n\sqrt{n})\)。
这两种做法结合起来,便可得到一个另类的莫队算法——
枚举每个块,先把\(l,r\)置于块尾+1的位置和块尾。先处理左右端点都在同一个块中的情况。接着,右端点向右暴力搞一通。
搞完右端点,就该移动左指针了。移动左指针前,先备份记录一下,移动后再复原即可。
Code:
#include <bits/stdc++.h>
using namespace std;
#define LL long long
int aa[100005], typ[100005], cnt[100005], cnt2[100005], belong[100005], L[100005], R[100005], a[100005];
LL ans[100005];
struct query {
int l,r,id;
}q[100005];
int n,m,size,sum;
int cmp(query a, query b) {
return belong[a.l]!=belong[b.l]?belong[a.l]<belong[b.l]:a.r<b.r;
}
int main() {
scanf("%d%d",&n,&m);
size=sqrt(n);
sum=ceil((double) n / size);
for(int i = 1; i <= sum; i++) {
L[i] = size*(i-1)+1,R[i] = size*i;
for(int j=L[i]; j <= R[i]; j++) belong[j] = i;
}
R[sum] = n;
for(int i = 1; i <= n; i++) scanf("%d",&a[i]),aa[i]=a[i];
sort(a+1,a+n+ 1);
int tot=unique(a+1, a+n+1)-a-1;
for(int i=1;i<=n;i++) typ[i]=lower_bound(a + 1, a + tot + 1, aa[i]) - a;
for(int i=1;i<=m;i++) scanf("%d%d",&q[i].l,&q[i].r),q[i].id=i;
sort(q + 1, q + m + 1, cmp);
int i = 1;
for(int k = 0; k <= sum; k++) {
int l = R[k] + 1, r = R[k];
LL now = 0;
memset(cnt, 0, sizeof(cnt));
for(;belong[q[i].l]==k;i++) {
int ql= q[i].l, qr = q[i].r;
LL tmp;
if(belong[ql] == belong[qr]) {
tmp = 0;
for(int j = ql; j <= qr; ++j) cnt2[typ[j]] = 0;
for(int j = ql; j <= qr; ++j) {
++cnt2[typ[j]]; tmp = max(tmp, 1ll * cnt2[typ[j]] * aa[j]);
}
ans[q[i].id] = tmp;
continue;
}
while(r < qr) {
++cnt[typ[++r]]; now=max(now,(LL)cnt[typ[r]]*aa[r]);
}
tmp=now;
while(l > ql){
++cnt[typ[--l]]; now=max(now,(LL)cnt[typ[l]]*aa[l]);
}
ans[q[i].id] = now;
while(l < R[k] + 1) {
--cnt[typ[l]];
l++;
}
now=tmp;
}
}
for(int i=1;i<=m;i++) printf("%lld\n",ans[i]);
return 0;
}
3. 结束
今天先写到这里,有时间再来补充。再见!