C++简单莫队(一)
莫队
- 写在最前
- 莫队
- 基本原理
- 优化
- 实现
- 应用
- P1997
- P4462
- P5268
- 总结
写在最前
嗯,认真算一下,应该已经有一年多没有写博客了。
说实在的,我可能甚至都忘记了MarkDown是怎么写的了。
很怀念 算了也不怎么怀念,就是回忆一下2023年骨折卧床时期写下的博客,后来基本上也没时间写了。
今天重登博客,正好就来写写我正在学习的东西吧。
可能在强省,我的水平已经算很弱很弱了,大概也只有我这么菜才会去学习莫队这些手段仅仅为了暴力弄点分吧。
大概就说这么多,我想应该也是给未来的我看的,因为是真的没人看。
要AFO了,希望未来的我如果想重拾信息的话,能更快一点吧。
莫队
基本原理
首先,我这一篇只讲普通莫队,因此这是一个序列上,不带修,支持增添和删除的东西。
假如说,你需要处理一个序列上的静态问题,但是n≤105,m≤105n \le 10^5,m \le 10^5n≤105,m≤105(n是序列长度,m是询问次数),那么你并不能预处理,因为区间数量是O(n2)O(n^2)O(n2)的,然而你如果正常去对询问一个一个去计算的话,那么也会是O(nm)O(nm)O(nm)的,这很悲伤了。
对于某一种特殊的情况,就是你可以用O(1)O(1)O(1)的时间复杂度来完成将[l,r][l,r][l,r]的答案转化成[l−1,r],[l+1,r],[l,r−1],[l,r+1][l-1,r],[l+1,r],[l,r-1],[l,r+1][l−1,r],[l+1,r],[l,r−1],[l,r+1],那么我们就可以使用莫队了
具体来说,你把l分成n/blocks块(也就是一块有blocks这么大),然后对于每个块内按照r的大小排序,那么在这一个过程中,那么你最坏是O(n2/blocks+m×blocks)O(n^2 / blocks+m \times blocks)O(n2/blocks+m×blocks)
前面是指你在一个块内,r最坏从1~n,那么n/blocks块就是n2×blocksn ^2\times blocksn2×blocks
后面指的是,你lll变化一次,最大是变化blocksblocksblocks这么大,而你总共也只有mmm次询问。
根据基本不等式,我们知道,当blocks=nmblocks=\frac{n}{ \sqrt m }blocks=mn的时候,时间复杂度是最小的。当然,一般n,m一个量级,所以设置成n\sqrt nn问题也不是很大。
优化
补充一个小优化,也叫奇偶性优化,就是奇数块按照r从小到大排序,偶数块按照r从大到小排序。
优化的原因是显然的,因为你避免了r跑到n之后回到1重新开始的时间,而是按照折返跑的方式节省时间。
虽然并没有带来量级上的优化,但是对于常数的优化十分显著。
实现
接下来就是说说实现了,这里我们以洛谷的模板题为例。
P2709 小B的询问/[模板]莫队
显然,我们可以记录对于当前区间中每个数出现的次数
假如我们新加入一个点,这个点的值是xxx,目前已经出现了cxc_xcx次,那么答案新增就是(cx+1)2−cx2=2cx+1(c_x+1)^2-c_x^2=2c_x+1(cx+1)2−cx2=2cx+1,这个值是可以O(1)计算的
那么删除也是同理的,直接上代码吧
注意,一般来说,扩展区间的时候先放扩大(即l–,r++),再放缩小(即l++,r–),可以避免潜在的问题。
#include<iostream>
#include<algorithm>
#include<cmath>
using namespace std;
int n,m,c[50005],block,b[50005],k;
long long sum,a1[50005];
struct node{int l,r,d;
}a[50005];
bool cmp(node x,node y){if(x.l/block!=y.l/block) return x.l<y.l;return x.r<y.r;
}
void add(int x){//扩展区间sum+=2*(b[x]++)+1;
}
void del(int x){//缩小区间sum-=2*(--b[x])+1;
}
int main(){cin>>n>>m>>k;for(int i=1;i<=n;i++) cin>>c[i];for(int i=1;i<=m;i++){cin>>a[i].l>>a[i].r;a[i].d=i;}block=sqrt(n);sort(a+1,a+1+m,cmp);for(int i=1,l=1,r=0;i<=m;i++){while(l>a[i].l) add(c[--l]);while(r<a[i].r) add(c[++r]);//先扩展while(r>a[i].r) del(c[r--]);while(l<a[i].l) del(c[l++]);//后缩小a1[a[i].d]=sum;//存答案}for(int i=1;i<=m;i++){cout<<a1[i]<<endl;}return 0;
}
请注意这一篇代码并没有加入奇偶性优化。
应用
简单放几道题吧,基本上都是洛谷的蓝题。
P1997
P1997 febdc的烦恼
这道题也差不多,就是只需要考虑难度出现次数的计数就行了,由于这个难度的出现次数最多一次只会-1,所以答案也最多-1
另外P3709是双倍经验,当然要小心题意稍稍有一点不同。
#include<iostream>
#include<algorithm>
#include<cmath>
#define N 200005
using namespace std;
int n,m,c[N],block,b[N],maxn,cnt[N];
long long sum,a1[N];
struct node{int l,r,d;
}a[N];
bool cmp(node x,node y){if(x.l/block!=y.l/block) return x.l<y.l;return x.r<y.r;
}
void add(int x){cnt[b[x]]--;cnt[++b[x]]++;if(b[x]>maxn) maxn=b[x];
}
void del(int x){cnt[b[x]]--;if(b[x]==maxn&&cnt[b[x]]==0) maxn--;//如果这一出现次数的难度不存在了,那么答案-1cnt[--b[x]]++;
}
int main(){cin>>n>>m;for(int i=1;i<=n;i++) cin>>c[i];for(int i=1;i<=m;i++){cin>>a[i].l>>a[i].r;a[i].d=i;}block=sqrt(n);sort(a+1,a+1+m,cmp);for(int i=1,l=1,r=0;i<=m;i++){while(l>a[i].l) add(c[--l]);while(r<a[i].r) add(c[++r]);while(r>a[i].r) del(c[r--]);while(l<a[i].l) del(c[l++]);a1[a[i].d]=maxn;}for(int i=1;i<=m;i++){cout<<a1[i]<<endl;}return 0;
}
P4462
P4462 异或序列
这个题也不太难吧,一个数减少之后,ans减少异或对应的数出现次数,出现次数这个东西显然是O(1)转移的
#include<iostream>
#include<algorithm>
#include<cmath>
using namespace std;
int n,m,c[100005],block,b[100005],k,d[100005];
long long sum,a1[100005];
struct node{int l,r,d;
}a[100005];
bool cmp(node x,node y){if(x.l/block!=y.l/block) return x.l<y.l;return x.r<y.r;
}
void add(int x){sum+=b[x^k];b[x]++;
}
void del(int x){b[x]--;sum-=b[x^k];
}
int main(){cin>>n>>m>>k;for(int i=1;i<=n;i++){cin>>c[i];d[i]=c[i]^d[i-1];}for(int i=1;i<=m;i++){cin>>a[i].l>>a[i].r;a[i].d=i;a[i].l--;}block=sqrt(n);sort(a+1,a+1+m,cmp);for(int i=1,l=1,r=0;i<=m;i++){while(l>a[i].l) add(d[--l]);while(r<a[i].r) add(d[++r]);while(r>a[i].r) del(d[r--]); while(l<a[i].l) del(d[l++]); a1[a[i].d]=sum;}for(int i=1;i<=m;i++){cout<<a1[i]<<endl;}return 0;
}
P5268
P5268 一个简单的询问
这道题还是很有思维深度的。get(l1,r1,x)和get(l2,r2,x)get(l1,r1,x)和get(l2,r2,x)get(l1,r1,x)和get(l2,r2,x)的积显然并不好算,如果是get(1,r1,x)×get(1,r2,x)get(1,r1,x) \times get(1,r2,x)get(1,r1,x)×get(1,r2,x)这样子就好了。
我们发现get(l1,r1,x)=get(1,r1,x)−get(1,l1−1,x)get(l1,r1,x)=get(1,r1,x)-get(1,l1-1,x)get(l1,r1,x)=get(1,r1,x)−get(1,l1−1,x)所以我们就把两个拆开,再分别相乘,能得到四个式子g[r1]∗g[r2]−g[r1]∗g[l2−1]−g[l1−1]∗g[r2]+g[l1−1]∗g[l2−1]g[r1]*g[r2]-g[r1]*g[l2-1]-g[l1-1]*g[r2]+g[l1-1]*g[l2-1]g[r1]∗g[r2]−g[r1]∗g[l2−1]−g[l1−1]∗g[r2]+g[l1−1]∗g[l2−1]
每个部分我们都可以用莫队去计算,这个并不复杂,然后记一下是加还是减就可以了。
#include<iostream>
#include<algorithm>
#include<cmath>
#define ll long long
#define N 50005
using namespace std;
ll n,a[N],Q,cnt;
ll b[N],ans[N],block,now,cntl[N],cntr[N];
struct query{int l,r,id,type;
}q[4*N];
bool cmp(query A, query B){if(A.l/block==B.l/block){if((A.l/block)&1) return A.r>B.r;return A.r<B.r;}return A.l<B.l;
}
void addl(int x){now+=cntr[x];cntl[x]++;
}
void dell(int x){now-=cntr[x];cntl[x]--;
}
void addr(int x){now+=cntl[x];cntr[x]++;
}
void delr(int x){now-=cntl[x];cntr[x]--;
}
int main(){cin>>n; block=n/sqrt(n);for(int i=1;i<=n;i++) cin>>a[i];cin>>Q;for(int i=1,l1,r1,l2,r2;i<=Q;i++){ cin>>l1>>r1>>l2>>r2;q[++cnt]=(query){r1,r2,i,1};q[++cnt]=(query){l1-1,l2-1,i,1};q[++cnt]=(query){r1,l2-1,i,-1};q[++cnt]=(query){r2,l1-1,i,-1};}for(int i=1;i<=cnt;i++){if(q[i].l>q[i].r) swap(q[i].l,q[i].r);}sort(q+1,q+1+cnt,cmp);for(int i=1,l=0,r=0;i<=cnt;i++){while(q[i].r>r) addr(a[++r]);while(q[i].l>l) addl(a[++l]);while(q[i].l<l) dell(a[l--]);while(q[i].r<r) delr(a[r--]);ans[q[i].id]+=(q[i].type*now);}for(int i=1;i<=Q;i++) cout<<ans[i]<<endl;return 0;
}
总结
实际上普通莫队的题看起来长得都还是差不多的,至于树上莫队和带修莫队之类的,我只能说我正在学。
但是我可能很快就AFO了,所以不一定来得及写莫队(二),如果我鸽了,那么很抱歉。
今年CSP-S 226pts还算是个不错的成绩吧,现在正在集训。希望能拿到期待了很久的NOIP
如果真的有人在看,而且觉得这篇还勉强能看的话,希望你能在评论区回复我一下,让我知道有人在看。
当然如果没有,那么我的判断就是没问题的,就是真的没有人看。
