刚打完cf,迫不及待的来写了。前两天又不知道忙啥去了。这次总结一下莫队和带修莫队。
1.莫队
1.1问题引入
前置知识:双指针,分块。
这里用的洛谷的P3901。
题意:给定一个序列,然后是若干组询问。每组询问区间[l,r]的出现的数是否互不相同。
把题意翻译过来,就是区间内每个数出现的次数最多不超过1。我们考虑暴力的做法,因为数据范围是1e5,所以可以直接开一个数组cnt[i],表示i这个数当前出现的次数。移动左右指针来移动区间。但是这样的时间复杂度会很大,最坏的情况下会到O(nm),这样的复杂度肯定是很难接受的,我们考虑沿用分块的一些思想,我们将所有询问(和操作)都给离线下来,所以普通莫队实际上是个离线的算法。然后记录每个询问的左右端点,然后排序。如果左端点在同一个块中按右端点排序,否则按左端点排序。为什么要这样排序呢?我们不妨从复杂度的角度来考虑一下这个问题。考虑分成块,则每个块的大小也是,如果左指针在块内移动,则单词最多移动的位置。如果左指针要跨过某个块,最多是个块,所以复杂度也是O()级别的。再来看右指针,由于右指针被左指针限制移动,它的移动只能像右移动。复杂度也是O()的,总复杂度是O(m)。
(感觉这里讲的不是特别清楚qwq,佬们轻喷啊)
莫队实际上就是这样的一个算法(实际上和数据结构的联系更加紧密),假如我们当前求得了f(l,r)区间内所维护的答案,我们能在O(1)的时间内求解f(l+1,r),f(l-1,r),f(l,r+1),f(r-1)的状态,则可以用莫队求解,下面我们来具体的看一看add与del函数一般干了什么事情。
接下来讲一下本题的add与del函数,这俩函数是根据题目而决定的。在本题中,我们需要维护区间内出现的元素的个数,如果当前cnt[a[x]]为0时增加,累加答案。如果当前cnt[a[x]]为1时减少,计数减少。
细节在代码中看把~
1.2例题代码(洛谷P3901)
#include<iostream>
#include<cmath>
#include<algorithm>
#define endl '\n'
using namespace std;
const int N=1e5+10;
int n,m,a[N],bsz,cnt[N],sum,ans[N],L[N],R[N];
struct query{
int l,r,id;
bool operator<(const query &t){
if(t.l/bsz!=l/bsz)return l<t.l;
else return r<t.r;
}
}q[N];
void add(int x){
if(cnt[a[x]]==0)sum++;
cnt[a[x]]++;
}
void del(int x){
if(cnt[a[x]]==1)sum--;
cnt[a[x]]--;
}
int main(){
cin>>n>>m;
bsz=sqrt(n);
for(int i=1;i<=n;i++)cin>>a[i];
for(int i=1;i<=m;i++){
cin>>q[i].l>>q[i].r;
q[i].id=i;
}
sort(q+1,q+m+1);
int l=1,r=0;
for(int i=1;i<=m;i++){
while(q[i].l<l)add(--l);
while(q[i].r>r)add(++r);
while(q[i].l>l)del(l++);
while(q[i].r<r)del(r--);
ans[q[i].id]=sum;
L[q[i].id]=l;
R[q[i].id]=r;
}
for(int i=1;i<=m;i++){
if(ans[i]==R[i]-L[i]+1)cout<<"Yes"<<endl;
else cout<<"No"<<endl;
}
return 0;
}
1.3洛谷P2709
莫队裸题。假如某个数当前的个数是x,当变为(x+1),则会增加(2x+1)。同理从x+1减少到x的时候,数量会减少2x+1。各位可以将完全平方展开自行理解(如果不会完全平方应该也不会来学莫队吧)
下面贴个代码
#include<iostream>
#include<map>
#include<algorithm>
#include<cmath>
#define ll long long
#define endl '\n'
using namespace std;
const int N=5e4+10;
inline int read(){
char ch = getchar();
int x = 0, f = 1;
while(ch < '0' || ch > '9'){
if(ch == '-') f = -1;
ch = getchar();
}
while('0' <= ch && ch <= '9'){
x = x * 10 + ch - '0';
ch = getchar();
}
return x * f;
}
inline void write(ll x){
if (x < 0) x = ~x + 1, putchar('-');
if (x > 9) write(x / 10);
putchar(x % 10 + '0');
}
int mp[N];
int n,m,bsz,a[N],k;
ll sum,ans[N];
struct query{
int l,r,id;
bool operator<(const query &q ){
if(l/bsz!=q.l/bsz)return l<q.l;
else return r<q.r;
}
}q[N];
void add(int x){
sum+=(2*mp[a[x]]+1);
mp[a[x]]++;
}
void del(int x){
mp[a[x]]--;
sum-=(2*mp[a[x]]+1);
}
int main(){
n=read();m=read();
k=read();
bsz=sqrt(n);
for(int i=1;i<=n;i++)a[i]=read();
//m=read();
for(int i=1;i<=m;i++){
q[i].l=read();
q[i].r=read();
q[i].id=i;
}
sort(q+1,q+m+1);
int l=1,r=0;
for(int i=1;i<=m;i++){
while(l>q[i].l)add(--l);
while(r<q[i].r)add(++r);
while(l<q[i].l)del(l++);
while(r>q[i].r)del(r--);
ans[q[i].id]=sum;
}
for(int i=1;i<=m;i++){
write(ans[i]);
puts("");
}
return 0;
}
1.4洛谷P1194
题意:给定一个区间。询问当前区间任取两个,恰好颜色相同的概率。要求用分数表达。
首先,假设区间长度是l,那么分母就是,而分子就是其他颜色可能相同的袜子的和,假设某一时刻,某一颜色的袜子是x只,当增加后,会变成x+1只,会增加种可能,而从x+1只减少到x只也是如此,所以我们考虑记录下每种颜色的袜子当前的数量cnt[i],再记录一个sum值用于记录当前分子的可能性,注意特判下分子是0的时候的输出。记得约下分就可以快乐的AC了。
#include<iostream>
#include<algorithm>
#include<cmath>
#define ll long long
#define int long long
using namespace std;
const int N=8e4+10;
ll n,m,a[N],bsz,sum,cnt[N];
ll ans1[N],ans2[N];//记录某种颜色的数量
ll gcd(ll x,ll y){
return y==0?x:gcd(y,x%y);
}
struct query{
int l,r,id;
bool operator<(const query &q){
if(l/bsz!=q.l/bsz)return l<q.l;//同一个块中按左端点排序
//否则按右端点排序
return r<q.r;
}
}q[N];
void add(int x){
sum+=cnt[x];
cnt[x]++;
}
void minor(int x){
cnt[x]--;
sum-=cnt[x];
}
signed main(){
cin>>n>>m;
bsz=sqrt(n);
for(int i=1;i<=n;i++){
cin>>a[i];
}
for(int i=1;i<=m;i++){
int l,r;
cin>>l>>r;
q[i].l=l;
q[i].r=r;
q[i].id=i;//记录下左右区间
}
//排序
sort(q+1,q+m+1);
int l=1,r=0;//初始为空区间
for(int i=1;i<=m;i++){
while(l>q[i].l)add(a[--l]);
while(r<q[i].r)add(a[++r]);
while(l<q[i].l)minor(a[l++]);
while(r>q[i].r)minor(a[r--]);
ans1[q[i].id]=sum;//分子
int len=r-l+1;
ans2[q[i].id]=len*(len-1)/2;
}
for(int i=1;i<=m;i++){
ll k=gcd(ans1[i],ans2[i]);
if(k==0||ans1[i]==0){
cout<<"0/1"<<endl;
continue;
}else {
ans1[i]/=k;
ans2[i]/=k;
cout<<ans1[i]<<"/"<<ans2[i]<<endl;
}
}
return 0;
}
1.5洛谷P5268
一个简单的询问,但这题可不简单啊。
题意:给定一个区间[l1,r1]和另一个区间[l2,r2]。计算。get的含义见题目。乘法不好处理,我们在莫队问题处理加法比较多。考虑类似前缀和的思想,把区间[l,r]拆分成[1,r],[1,l-1]。这样我们可以对公式进行如下处理。记g(r)=get(1,r,x)。
至此我们将一个区间拆成了四个区间(实际上不是区间啦),假如只考虑其中一组的情况,开两个数组,cnt1[i]与cnt2[i],分别记录两个数组各元素的数量。当cnt1[i]增加时,答案累加cnt2[i],因为:(x+1)*y-x*y=y。如果是减少的情况也是一样的,至此这道题已经解决了。
#include<iostream>
#include<algorithm>
#include<cstring>
#include<cmath>
#define ll long long
using namespace std;
const int N=5e4+10;
int n,m,a[N],bsz;
int cnt1[N],cnt2[N];
int l=0,r=-1;
ll sum,ans[N<<2];
struct query{
int l,r,id;
bool operator<(const query &t)const{
if(l/bsz!=t.l/bsz)return l<t.l;
return r<t.r;//同一块内按左端点排序否则按右端点排序
}
}q[N<<2];//注意询问开4倍空间
void addl(int x){
++cnt1[a[x]];
sum+=cnt2[a[x]];
}
void dell(int x){
--cnt1[a[x]];
sum-=cnt2[a[x]];
}
void addr(int x){
++cnt2[a[x]];
sum+=cnt1[a[x]];
}
void delr(int x){
--cnt2[a[x]];
sum-=cnt1[a[x]];
}
int main(){
cin>>n;
bsz=sqrt(n);
for(int i=1;i<=n;i++)cin>>a[i];
cin>>m;
for(int i=1;i<=m;i++){
//cout<<i;
int l1,r1,l2,r2;
cin>>l1>>r1>>l2>>r2;//读入数据
q[i]={r1,r2,i};
q[i+m]={r1,l2-1,i+m};
q[i+m*2]={r2,l1-1,i+2*m};
q[i+m*3]={l1-1,l2-1,i+3*m};
}
//cout<<1;
for(int i=1;i<=4*m;i++){
if(q[i].l>q[i].r)swap(q[i].l,q[i].r);
}
sort(q+1,q+4*m+1);//排序
//cout<<1;
for(int i=1;i<=m*4;i++){
while(l<q[i].l)addl(++l);
while(r<q[i].r)addr(++r);
while(l>q[i].l)dell(l--);
while(r>q[i].r)delr(r--);
ans[q[i].id]=sum;//记录答案
}
for(int i=1;i<=m;i++){
ll res=ans[i]-ans[i+m]-ans[i+2*m]+ans[i+3*m];
cout<<res<<endl;
}
return 0;
}
至此莫队水题就切完啦
2.带修莫队
2.1问题引入
现在我们要处理的序列不再是静止的了,它需要支持一系列的修改操作。
我们知道莫队是一个离线算法,对于修改操作,我们也可以离线下来,到用到的时间再改。
由于加了一维,我们需要修改我们的询问数组。
struct query{
int id, l, r, t;//id:第几个询问 t:第几个版本
int posl,posr;//记录l所在的块和r所在的块的位置
bool operator<(const query sec)const{
if(posl!=sec.posl)return l<sec.l;
if(posr!=sec.posr)return r<sec.r;
return t<sec.t;
}
}q[N];
我们还需要加入两个数组pos记录当前版本修改的位置,nxt数组暂存当前版本需要修改的值,等需要用到的时候进行交换。
带修莫队的块大小是取的,具体证明我看了很多遍没看懂,先记个结论吧,如果有大佬会的话可以教教本蒟蒻啊。
我们需要加一个work函数用来记录版本的移动操作。这里做的事情就是删掉原来的颜色,加入暂存的颜色,将原来的颜色暂存,可以通过代码更直观的感受。
void work(int x,int l,int r){//x是记录的版本号所对应修改的位置
if(l<=pos[x]&&pos[x]<=r){
if(--cnt[a[pos[x]]]==0)sum--;//删掉原版本的信息
if(++cnt[nxt[x]]==1)sum++;//插入新版本的信息
}
swap(nxt[x],a[pos[x]]);//将原来版本的信息先暂存起来
}
接下来看完整代码
2.2例题代码(洛谷1903)
#include<iostream>
#include<cmath>
#include<algorithm>
using namespace std;
const int N=133343;
const int M=1e6+10;
int n,m,a[N],bsz,sum,cnt[M],ans[N];
int pos[N],nxt[N],cnt1,cnt2;
inline int read(){
char ch = getchar();
int x = 0, f = 1;
while(ch < '0' || ch > '9'){
if(ch == '-') f = -1;
ch = getchar();
}
while('0' <= ch && ch <= '9'){
x = x * 10 + ch - '0';
ch = getchar();
}
return x * f;
}
inline void write(int x){
if (x < 0) x = ~x + 1, putchar('-');
if (x > 9) write(x / 10);
putchar(x % 10 + '0');
}
struct query{
int id, l, r, t;
int posl,posr;
bool operator<(const query sec)const{
if(posl!=sec.posl)return l<sec.l;
if(posr!=sec.posr)return r<sec.r;
return t<sec.t;
}
}q[N];
void add(int x){
if(cnt[a[x]]++==0)sum++;
}
void del(int x){
if(--cnt[a[x]]==0)sum--;
}
void work(int x,int l,int r){//x是记录的版本号所对应修改的位置
if(l<=pos[x]&&pos[x]<=r){
if(--cnt[a[pos[x]]]==0)sum--;
if(++cnt[nxt[x]]==1)sum++;
}
swap(nxt[x],a[pos[x]]);
}
int main(){
n=read();
m=read();
for(int i=1;i<=n;i++)cin>>a[i];
bsz=pow(n,0.666);
for(int i=1;i<=m;i++){
char op;
cin>>op;
if(op=='Q'){
int l,r;
l=read();
r=read();
q[++cnt1]={cnt1,l,r,cnt2};//因为一次修改增加一个版本
q[cnt1].posl=l/bsz;
q[cnt1].posr=r/bsz;
}else{
int x,k;
x=read();
k=read();
pos[++cnt2]=x;
nxt[cnt2]=k;//暂存当前版本信息
}
}
sort(q+1,q+cnt1+1);
int l=1,r=0,t=-1;
for(int i=1;i<=cnt1;i++){
while(l>q[i].l)add(--l);
while(r<q[i].r)add(++r);
while(l<q[i].l)del(l++);
while(r>q[i].r)del(r--);
while(t<q[i].t)work(++t,l,r);
while(t>q[i].t)work(t--,l,r);
ans[q[i].id]=sum;
//cout<<sum<<endl;
}
for(int i=1;i<=cnt1;i++){
write(ans[i]);
puts("");
}
return 0;
}
3.杂题
3.1CodeTon round 7 D
这题是一个思维题,但是我不喜欢动脑子,所以我决定用暴力的方法写。
本题我不是赛时出的,补题写的。
题意:给定一个序列,序列中每个数只能是1或者2。有两种操作,有修改,有查询。查询的是是否存在一个区间[l,r]使得区间[l,r]的元素和为k。
一读题是不是很有线段树的味道了?没错!但是我的线段树一直T,调不来,我就把一颗线段树换成了树状数组。第一颗线段树维护原数组,没啥好说的,第二颗线段树维护当前区间是否存在1。关键是如何查询呢?我们考虑二分答案。如果当前维护的区间恰好为k,则找到了这样的区间。如果为k+1,我们查询[1,n-mid+1]是否有1,如果有,则可以通过移动区间把这个1去掉。至于为什么是n-mid+1,我们只能往后移动n-mid+1位了嘛。或者查询[mid,n]是否存在1,如果有1,我们可以移动区间加上这个1,左边删掉若干个2即可。线段树上套二分的复杂度(),是可以卡过去的。
#include<iostream>
using namespace std;
const int N=2e6+10;
int n,m,a[N],b[N],T;//b用来记录某个数是不是1或0
int t[N];
int sumv[N<<2];
int lowbit(int x){
return x&-x;
}
void add(int x,int k){
for(int i=x;i<=n;i+=lowbit(i))t[i]+=k;
}
int ask(int x){
int res=0;
for(int i=x;i;i-=lowbit(i))res+=t[i];
return res;
}
void pushup(int id){
sumv[id]=sumv[id<<1]+sumv[id<<1|1];
}
void build(int id,int l,int r){
if(l==r){
sumv[id]=b[l];
return;
}
int mid=l+r>>1;
build(id<<1,l,mid);
build(id<<1|1,mid+1,r);
pushup(id);
}
void modify(int id,int l,int r,int x,int k){
if(l==r){
sumv[id]=k;
return;
}
int mid=l+r>>1;
if(x<=mid)modify(id<<1,l,mid,x,k);
else modify(id<<1|1,mid+1,r,x,k);
pushup(id);
}
int query(int id,int l,int r,int x,int y){
if(x<=l&&y>=r)return sumv[id];
int ans=0,mid=l+r>>1;
if(x<=mid)ans+=query(id<<1,l,mid,x,y);
if(y>mid)ans+=query(id<<1|1,mid+1,r,x,y);
return ans;
}
int cal(int l,int r,int s){
while(l<=r){
int mid=l+r>>1;
int check=ask(mid);//访问当前下标的元素
if(check<s)l=mid+1;
else if(check>s+1)r=mid-1;
else if(check==s)return true;
else if(check==s+1){
int lfind=query(1,1,n,1,n-mid+1);
int rfind=query(1,1,n,mid,n);
if(lfind>0||rfind>0)return true;
return false;
}
//cout<<l<<' '<<r<<endl;
}
return false;
}
void solve(){
cin>>n>>m;
for(int i=1;i<=n;i++)t[i]=0;
for(int i=1;i<=n;i++){
cin>>a[i];
b[i]=a[i]%2;
add(i,a[i]);
}
build(1,1,n);//建树
while(m--){
int op;
cin>>op;
if(op==1){
int s;
cin>>s;
if(cal(1,n,s))cout<<"Yes"<<endl;
else cout<<"No"<<endl;
}else{
int x,k;
cin>>x>>k;
add(x,k-a[x]);
a[x]=k;
modify(1,1,n,x,k%2);
}
}
}
int main(){
cin>>T;
while(T--){
solve();
}
return 0;
}
3.2洛谷P4135
这题和总结(上)中的蒲公英很类似。
题意:一个序列,若干组询问,每次询问区间内出现的数的数量是偶数的数量,强制在线。
看见强制在线不用担心,分块是强制在线的。
下面我将从建块,修改,查询操作分别讲起。本题没有修改操作
建块:多开两个数组,cnt[i][j]表示从第i个块开始数字j出现的次数,实际上是一个类似与后缀和一样的东西,因为我们肯定是难以先求出每个块的元素再累加的,复杂度太高了。f[i][j]表示从第i个块到第j个块中数字出现次数是偶数的个数。
查询:如果区间[l,r]在同一块或者在相邻块中,我们只需要暴力枚举,如果当前数的出现次数变成偶数了,计数增加。如果当前出现次数变回大于1的奇数了,计数减少。
否则,答案初始化为f[id[l]+1][id[r]-1]。然后扫两边的元素,计数增加或减少,记得加上中间的元素出现的次数,这时候就得用到后缀和了。第i块到第j块出现k的次数实际上是cnt[id[l]+1][k]-cnt[id[r]][k],其他的处理就和之前在同一块一样了。
写完啦!
#include<iostream>
#include<cmath>
using namespace std;
const int N=1e5+10;
const int SQ=345;
int n,m,c,a[N],sq;
int ed[SQ],st[SQ],id[N],cnt[SQ][N],f[SQ][SQ],ans;//cnt表示某一块某个数字的个数
int t[N];//用来存放可能的数据
void build(){
sq=sqrt(n);
for(int i=1;i<=sq;i++){
st[i]=n/sq*(i-1)+1;
ed[i]=n/sq*i;
}
ed[sq]=n;
for(int i=1;i<=sq;i++){
for(int j=st[i];j<=ed[i];j++){
id[j]=i;
}
}
for(int i=1;i<=sq;i++){
int s=0;
for(int j=st[i];j<=n;j++){
++cnt[i][a[j]];//空间换时间
if(cnt[i][a[j]]%2==0)++s;
else if(cnt[i][a[j]]>=3)s--;//cnt是从第i块开始到n的数量 实际上应该是后缀和?3
if(id[j]!=id[j+1])f[i][id[j]]=s;
//预处理从x~y块的情况
}
}
}
int query(int l,int r){
int res=0;
if(id[l]==id[r]||id[r]==id[l]+1){//暴力扫
//暴力扫一遍
for(int i=l;i<=r;i++){
t[a[i]]++;
if(t[a[i]]%2==0)res++;
else if(t[a[i]]>=3)res--;
}
for(int i=l;i<=r;i++){
t[a[i]]--;
}
}else{
res=f[id[l]+1][id[r]-1];//初始化res 取中间的数
for(int i=l;i<=ed[id[l]];i++){
t[a[i]]++;
//从id[l]+1~id[r]-1
int temp=cnt[id[l]+1][a[i]]-cnt[id[r]][a[i]];//记录中间某个数出现的次数
if((temp+t[a[i]])%2==0)res++;
else if(temp+t[a[i]]>=3)res--;
}
for(int i=st[id[r]];i<=r;i++){
t[a[i]]++;
int temp=cnt[id[l]+1][a[i]]-cnt[id[r]][a[i]];//记录中间某个数出现的次数
if((temp+t[a[i]])%2==0)res++;
else if(temp+t[a[i]]>=3)res--;
}
for(int i=l;i<=ed[id[l]];i++)t[a[i]]--;
for(int i=st[id[r]];i<=r;i++)t[a[i]]--;//清空桶
}
return res;
}
void check(){
// for(int i=1;i<=sq;i++){
// for(int j=1;j<=i;j++){
// cout<<j<<' '<<i<<' '<<f[j][i]<<endl;
// }
// }
cout<<"后缀和\n";
for(int i=1;i<=sq;i++){
for(int j=1;j<=c;j++){
cout<<i<<' '<<j<<' '<<cnt[i][j]<<endl;
}
}
}
int main(){
cin>>n>>c>>m;
for(int i=1;i<=n;i++)cin>>a[i];
build();
//check();
for(int i=1;i<=m;i++){
int l,r;
cin>>l>>r;
l=((l+ans)%n)+1;
r=((r+ans)%n)+1;
if(l>r)swap(l,r);
//cout<<l<<' '<<r<<endl;
ans=query(l,r);
cout<<ans<<endl;
}
return 0;
}
今天就写到这里吧,明天还得去补CF的D和E题