【笔记篇】莫队算法(一)
P.S.:这个星期写了一个星期的莫队,现在也差不多理解了,下周该学点别的了(其实是被long long卡得生活不能自理......快要写吐了).
在本文开始之前,先orz莫涛......
莫队算法(Mo's algorithm),是一种离线解决区间问题的算法.
据说,只要不强制在线,莫队算法能解决所有区间查询问题......
如何判断一个问题可以使用莫队?
如果我们知道[L,R]的答案,便可以O(1)推出[L-1,R] [L,R-1] [L+1,R] [L,R+1]的答案,就可以用莫队做...
莫队算法的基本思想就是,把所有询问离线下来,得到一堆[L,R],我们已经知道上面的东西都可以O(1)求出了,那么我们对于[\(L_2,R_2\)]的答案就可以通过[\(L_1,R_1\)] ,花费\(|L_2-L_1|+|R_2-R_1|\)的时间求解....
哦 那么我们是不是就可以通过改变询问的顺序来少些重复的转移?
——嗯,没错,二维平面MST(曼哈顿距离最小生成树)!!!
莫队告诉我们,只要按这颗树做,复杂度就不会太高......而我们能在\(O(nlog_2n)\)时间内求出MST...
但这样的编程复杂度岂不是太高了?(MST对我等蒟蒻来说实在是……)
但是不要紧,我们有偷懒的办法——分块的\(O(n\sqrt n)\)还是没问题的嘛= =(当然我们假设n,q同级)
(orz 其实用MST做到最后的复杂度也是\(O(n\sqrt n)\),只是常数小了点(别问我为啥,我不会证!!))
我们把左端点的块编号作为第一关键字,把右端点的编号作为第二关键字排序...
然后按排序好的序列推过去就行了orz....
Q:为什么不是按左端点序号为第一关键字,右端点序号为第二关键字排序呢?
A:是为了避免\(L\)略小于\(L_1\)略小于\(L_2\),但\(R_1\)远小于R远小于\(R_2\)的情况啊......
关于复杂度的证明:
右端点:由于排过序,左端点跨块时变得多,最多变n,有\(\sqrt n\)个块所以不超过\(O(n\sqrt n)\)
左端点:由于按左端点排序,跨两块最多也不超过\(2\sqrt n\),有q个询问,q,n同级所以不超过\(O(n\sqrt n)\)
而实际上的复杂度应该不到这个最坏复杂度,大概就O(玄学)了...
嗯 差不多就是这样,下面我们看代码......
我们做莫队的时候就是要分析:
我们当前的区间维护到了[L,R],现在遇到了x点...
-如果x∈[L,R]中,肯定要删除(不然就不会用到x点了)
-如果x∉[L,R],我们就要添加
所以我们可以用一个bool数组来维护每个点是不是在区间中(当然这份代码没有这么写)
然后我们就需要一个fix函数来维护……
void fix(int p,int &res) //res用于维护结果,p表示更新p点
{
if(ex[p])
{
//TODO:删掉p点要维护什么信息
}
else
{
//TODO:添加p点要维护什么信息
}
ex[p]^=1; //处理完p点要将p点存在性取反...
}
处理询问是怎么推过去的呢?
//我们需要定义一个询问的结构体
struct query
{
int l,r,id; //第id个问题询问[l,r]
};
//我们之前需要一个cmp函数(sort用)
bool cmp(const query &a,const query &b)
{
if(a.l/blk==b.l/blk) return a.r<b.r; //blk表示分块的大小
return a.l<b.l;
}
void solve()
{
sort(q+1,q+m,cmp); //将询问排序
int l=q[1].l,r=q[1].r-1;int res=0; //[l,r]表示当前处理到的区间,res表示结果
//开始的时候肯定不会想更新一堆信息所以把区间设为空
for(int i=1;i<=m;i++)
{
int L=q[i].l,R=q[i].r;
while(l>L) fix(--l,res);
while(l<L) fix(l++,res);
while(r>R) fix(r--,res);
while(r<R) fix(++r,res);
//这一串记住--l,其他都能推出来,至于为什么,也是很好理解的~
ans[q[i].id]=res;
}
}
//基本就是这样咯~
例题? 是莫队在集训队论文中提出的.. bzoj上莫涛版权所有的一道题...
bzoj2038-小z的袜子
化式子什么的我本不想说,但是鉴于实在不是很懂orz...
所以还是要化一下的...
$ans=\frac{\sum C(cur[color_i],2)}{C(r-l+1,2)} \( \)= \frac{\sum \frac{cur[color_i]!}{(cur-2)!2!}}{\frac{(r-l+1)!}{(r-l-1)!2!}} \( \)= \frac{\sum curcolor_i}{(r-l)(r-l+1)}\( \)= \frac{\sum cur[color_i]^2-\sum color_i}{(r-l)(r-l+1)}\( \)= \frac{\sum cur[color_i]^2+(r-l+1)}{(r-l)*(r-l+1)}$
应该能看懂吧= = \(color_i\)表示i的颜色,cur[i]表示当前颜色为i的节点有多少...
然后根据上面,我们就能得出这样代码:
//此题极限数据50000*50000 一定要开long long
//开long long的时候每一处乘法都要记得强转long long(WA惨的教训)
#include <cmath>
#include <cstdio>
#include <algorithm>
using namespace std;
const int N=50005;
typedef long long int64;
int c[N],ex[N];
int64 cur[N];
int n,m,blk;
struct _ans{
int64 a,b;
}ans[N]; //这题答案是个分数...
struct query{
int l,r,id;
}q[N]; int ttt;
bool cmp(const query &a,const query &b){
if(a.l/blk==b.l/blk) return a.r<b.r;
return a.l<b.l;
}
inline void buildquery(int l,int r,int id){
q[id].l=l; q[id].r=r; q[id].id=id;
}
inline int getnum(){
int a=0;char c=getchar();bool f=0;
for(;(c<'0'||c>'9')&&c!='-';c=getchar());
if(c=='-') c=getchar(),f=1;
for(;c>='0'&&c<='9';c=getchar()) a=(a<<1)+(a<<3)+c-'0';
if(f) return -a; return a;
}
int64 gcd(int64 a,int64 b){
if(!b) return a;
return gcd(b,a%b);
}
void fix(int p,int64 &res){
if(ex[p]){
res-=(cur[c[p]]<<1)-1;
//这里是化了一下式子之后简便的位运算版(利用完全平方公式,可以自己推一下)
cur[c[p]]--;
}
else{
res+=(cur[c[p]]<<1|1); //同上
cur[c[p]]++;
}
ex[p]^=1;
}
void solve(){
sort(q+1,q+m+1,cmp);
int l=q[1].l,r=q[1].l-1; int64 res=0;
for(int i=1;i<=m;i++){
int L=q[i].l,R=q[i].r,id=q[i].id;
while(l>L) fix(--l,res);
while(l<L) fix(l++,res);
while(r<R) fix(++r,res);
while(r>R) fix(r--,res);
if(L==R) ans[id].a=0,ans[id].b=1; //特殊情况特殊处理
else{
int64 a=res-(r-l+1),b=(int64)(r-l+1)*(r-l),k=gcd(a,b);
ans[id].a=a/k,ans[id].b=b/k;
}
}
}
int main(){
n=getnum(),m=getnum(); blk=sqrt(n);
for(int i=1;i<=n;i++) c[i]=getnum();
for(int i=1;i<=m;i++){
int x=getnum(),y=getnum();
buildquery(x,y,i);
} solve();
for(int i=1;i<=m;i++) printf("%lld/%lld\n",ans[i].a,ans[i].b);
}
就这样= = 完结撒花= =