update in 2017.12.24:
以前写的≈shit,实在看不下去了,重写一遍
很早之前就学习了莫队算法。
老师讲课的时候就提到过带修改莫队在线莫队树上莫队树上带修改莫队……但是一直都没有做到过有关的题,
今天有幸做了一道裸的带修改莫队的题,
那就来分享一下自己的经验
首先我们要知道,普通的莫队算法是不资瓷修改操作的,
不过后人对莫队算法加以改进
发明了资瓷修改的莫队算法
在进行修改操作的时候,修改操作是会对答案产生影响的(废话)
那么我们如何避免修改操作带来的影响呢?
首先我们需要把查询操作和修改操作分别记录下来。
在记录查询操作的时候,需要增加一个变量来记录离本次查询最近的修改的位置
然后套上莫队的板子,与普通莫队不一样的是,你需要用一个变量记录当前已经进行了几次修改
对于查询操作,如果当前改的比本次查询需要改的少,就改过去
反之如果改多了就改回来
说的听绕口的
比如,我们现在已经进行了3次修改,本次查询是在第5次修改之后,那我们就执行第4,5次修改
这样就可以避免修改操作对答案产生的影响了
#include<cstdio>
#include<cmath>
#include<algorithm>
using namespace std;
const int MAXN=2*1e6+10;
inline int read()
{
char c=getchar();int x=0,f=1;
while(c<'0'||c>'9'){if(c=='-')f=-1;c=getchar();}
while(c>='0'&&c<='9'){x=x*10+c-'0',c=getchar();}
return x*f;
}
int N,M;
int a[MAXN],where[MAXN];
struct Query
{
int x,y,pre,id;
}Q[MAXN];
int Qnum=0;
struct Change
{
int pos,val;
}C[MAXN];
int Cnum=0;
int color[MAXN],ans=0,base,out[MAXN];
int comp(const Query &a,const Query &b)
{
if(a.x!=b.x) return where[a.x]<where[b.x];
if(a.y!=b.y) return where[a.y]<where[b.y];
return a.pre<b.pre;
}
void Add(int val)
{
if(++color[val]==1) ans++;
}
void Delet(int val)
{
if(--color[val]==0) ans--;
}
void Work(int now,int i)
{
if(C[now].pos>=Q[i].x&&C[now].pos<=Q[i].y)//注意:只有修改在查询的区间内才会对查询的结果产生影响
{
if( --color[a[C[now].pos]] == 0 ) ans--;
if( ++color[C[now].val] == 1) ans++;
}
swap(C[now].val,a[C[now].pos]);
//这里有个很巧妙的操作
//对于一个操作,下一次需要为的颜色是本次被改变的颜色
//比如,我把颜色3改为了7,那么再进行这次修改的时候就是把7改为3
//所以直接交换两种颜色就好
}
void MoQueue()
{
int l=1,r=0,now=0;
for(int i=1;i<=Qnum;i++)
{
while(l<Q[i].x) Delet(a[l++]);
while(l>Q[i].x) Add(a[--l]);
while(r<Q[i].y) Add(a[++r]);
while(r>Q[i].y) Delet(a[r--]);//以上四句为莫队模板
while(now<Q[i].pre) Work(++now,i);//改少了,改过去
while(now>Q[i].pre) Work(now--,i);//改多了,改回来
out[Q[i].id]=ans;//统计答案
}
for(int i=1;i<=Qnum;i++)
printf("%d\n",out[i]);
}
int main()
{
N=read();M=read();
base=sqrt(N);
for(int i=1;i<=N;i++) a[i]=read(),where[i]=(i-1)/base+1;
while(M--)
{
char opt[5];
scanf("%s",opt);
if(opt[0]=='Q')
{
Q[++Qnum].x=read();
Q[Qnum].y=read();
Q[Qnum].pre=Cnum;//别忘了记录最近的修改位置
Q[Qnum].id=Qnum;
}
else if(opt[0]=='R')
{
C[++Cnum].pos=read();
C[Cnum].val=read();
}
}
sort(Q+1,Q+Qnum+1,comp);//玄学排序
MoQueue();
return 0;
}
由于刚开始的now=0
所以需要先增后执行
撤销的时候需要将最后一次的修改撤销掉
所以先执行后减
普通莫队时间复杂度为O\left( n\times \sqrt {n}\right)
证明:
当我们第i个询问转移的第i+1个询问时
也就是说,左端点一直在块内移动的总复杂度为O(n*\sqrt{n})(因为左端点最多转移n次,减去左端点跨越块的部分,不足n)
同时由于右端点升序,那么若l,l+1,,,r-1,r的询问区间左端点所在块的编号相等,那么右端点的移动不会超过n次。有一位有\sqrt{n}个块,
所以这一部分的复杂度是O(n*\sqrt{n})的。
又在这个期间,每次左端点跨越的时候,右端点可能要移动O(n)次,一共左端点跨越\sqrt{n}个块,所以右端点复杂度是O(n*\sqrt{n})的。
综上莫队算法的排序保证时间复杂度是O(n*\sqrt{n})的
带修改莫队算法的时间复杂度证明
以下内容借鉴自洛谷题解
原版莫队是将区间(l,r)视为点(l,r),带修改的即加一维时间轴(l,r,t)
对于t轴的移动可以保存每次修改,如果修改在(l,r)间则更新
分块方法可以参照原版莫队,先将l分块,再讲r分块,同一块的按t排序
块大小为\sqrt [3] {nt}可以达到最快的理论复杂度O\left( \sqrt [3] {n^{4}t}\right),证明如下
设分块大小为a,莫队算法时间复杂度主要为t轴移动,同r块l,r移动,l块间的r移动三部分
t轴移动的复杂度为O\left( \dfrac {n^{2}t}{a^{2}}\right),同r块l,r移动复杂度为0\left( na\right),l块间的r移动复杂度为0\left( \dfrac {n}{a}\right)
三个函数max的最小值当a为\sqrt [3] {nt}取得,为O\left( \sqrt [3] {n^{4}t}\right)