【笔记篇】莫队算法(一)

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);
}

就这样= = 完结撒花= =

posted @ 2018-02-04 08:41  Enzymii  阅读(326)  评论(0编辑  收藏  举报