莫队算法详解和c实现

解析和实现

摘要:

       莫队算法是一个对于区间、树或其他结构离线(在线)维护的算法,此算法基于一些基本算法,例如暴力维护,树状数组,分块,最小曼哈顿距离生成树,对其进行揉合从而产生的一个简单易懂且短小好写的算法。此算法在很多情况下可以很轻松的切掉一些复杂而且难写的数据结构问题。

关键词:

程序设计、算法、算法优化,暴力算法,分块算法,最小曼哈顿距离生成树。

背景:

众所周知,在OI竞赛、软件的设计中都会要求我们去处理各种各样的棘手的问题,而这些问题之中,有一大类就是维护问题:比如说对于一个序列的维护,对于棵二叉或者多叉树的维护……这些问题往往会需要我们去使用一个或多个高端的数据结构复合来完美解决,通常题目的代码十分冗长而且出错可能性十分大,是广大OIer、Acmer、Coder所害怕的题目。那么有没有一种方法可以既简单又快捷的解决这类问题(这类问题中的一大部分)呢?莫队算法就诞生辣!

理论1:

序列莫队:我们现在有一个长为n的静态的序列,对于序列,我们有m次查询,我们要动态查询l到r之间大于a小于b的数的个数以及种类。遇到了这个问题我们通常需要使用书套树的数据结构,即一颗以自平衡二叉查找树为节点的线段树(时间复杂度大约是O(mlognlogn)),而且由于空间限制,我们还必须动态创建线段树的节点,这样一来十分难写,一些大约要个400-500行,调试起来也很困难。这时候我们来考虑暴力算法,如果暴力的处理题目中的问题那么复杂度是多少呢?这个不难计算,对于每个询问我们都要O(n)的时间处理,一共有m个询问,那么暴力处理的复杂度就是O(nm)的,明显处理问题花费的时间我们是不能接受的。这是我们想到可以交换询问和询问之间的先后次序,这样每次询问在前一次询问的基础上转移就可以节省一些时间了。

但是如何重新排列询问之间的顺序是一个问题。我们需要进行一些理论分析。我们再上一个询问的基础上暴力地维护一个询问(假设上一个询问询问区间为[l0,r0],这个询问区间为[l,r]),那么我们所谓的暴力维护就是先把现有答案的右边界从r0移动到r,再把左边界从l0移动到l,那么我们的总花费是O(|l-l0|+|r-r0|)。仔细看一看,没错,这就是我们的曼哈顿距离的计算公式,有了这个思路,我们就可以从图形的角度来思考了,对于一个询问[l,r]我们可以将它映射为平面上在(l,r)位置的点,那么两个询问之间转移的代价就是询问所对应的点之间的曼哈顿距离。有了这一个结论,我们便想到可以用最小曼哈顿生成树来处理询问的顺序。由此莫队算法便诞生啦!莫队算法就是先将询问抽象成平面上的点,然后进行一边最小曼哈顿距离生成树,然后按照生成树的顺序来处理询问,这样的算法复杂度大约是O(mSqrt(n))的。如此,问题便简单了许多。

但是由于最小曼哈顿距离生成树也不是那么的好写,所以莫队算法还能再简单一点么?我们思考是否可以用一个简单而暴力的算法代替莫队算法呢。很快便能想到分块算法。我们可以使用分块算法来处理询问之间的次序问题。再去看那个询问对应的点所在的平面,我们找到它的X轴,我们把X轴平均分割成r分,然后我们把在一个块内的询问统一先处理,不在一个块内的询问我们按照左端点升序右端点升序排序依次处理。这样做有什么好处呢?对于m干个询问,如果在一个块里面,那么处理这些询问花费的复杂度是O(n/r*n*m),如果有两个询问不在一个同一个块里面,按照我们之前的排序规则,我们把左区间和右区间在块之间移动的次数最多为r*(n/r)*r次,那么我们的复杂度就是O(r*(n/r)*r)次,经过简单的数学分析,我们可以发现r=Sqrt(n)是时间复杂度最低为O(nSqrt(n))次,是可以接受的时间复杂度。这样我们的莫队算法就又简单有强大了。但是在另一些情况下,题目会无耻的限定我们可以使用的空间(一般不会,因为这样高级数据结构的复合也难以解决这样的问题了)。那么如果空间被限定了,我们应该如何解决问题呢?其实很简单, 还记得我们之前的r么?我们为了求的时间复杂度最小令r=Sqrt(n),如果我们令r=n ^ (2 / 3),那么便是一个时间复杂度和空间复杂度较为平衡的情况,这样可以很好的解决问题。

例题:

输入数据首先输入两个整数N,Q,分别代表序列的长度和询问的个数。这两个数字将单独占据一行并用一个空格分开。输入数据的第二行包含了N个由一个空格分开的正整数,代表了整个序列,从左向右依次编号为A1, A2……An。接下来Q行,每行两个整数i,j表示了一个询问区间。输入数据保证1≤i <j<=N

例题:

2038: [2009国家集训队]小Z的袜子(hose)

Description

作为一个生活散漫的人,小Z每天早上都要耗费很久从一堆五颜六色的袜子中找出一双来穿。终于有一天,小Z再也无法忍受这恼人的找袜子过程,于是他决定听天由命……
       具体来说,小Z把这N只袜子从1到N编号,然后从编号L到R(L 尽管小Z并不在意两只袜子是不是完整的一双,甚至不在意两只袜子是否一左一右,他却很在意袜子的颜色,毕竟穿两只不同色的袜子会很尴尬。
你的任务便是告诉小Z,他有多大的概率抽到两只颜色相同的袜子。当然,小Z希望这个概率尽量高,所以他可能会询问多个(L,R)以方便自己选择。

Input

输入文件第一行包含两个正整数N和M。N为袜子的数量,M为小Z所提的询问的数量。接下来一行包含N个正整数Ci,其中Ci表示第i只袜子的颜色,相同的颜色用相同的数字表示。再接下来M行,每行两个正整数L,R表示一个询问。

Output

包含M行,对于每个询问在一行中输出分数A/B表示从该询问的区间[L,R]中随机抽出两只袜子颜色相同的概率。若该概率为0则输出0/1,否则输出的A/B必须为最简分数。(详见样例)

【样例解释】

询问1:共C(5,2)=10种可能,其中抽出两个2有1种可能,抽出两个3有3种可能,概率为(1+3)/10=4/10=2/5。

询问2:共C(3,2)=3种可能,无法抽到颜色相同的袜子,概率为0/3=0/1。

询问3:共C(3,2)=3种可能,均为抽出两个3,概率为3/3=1/1。

注:上述C(a, b)表示组合数,组合数C(a, b)等价于在a个不同的物品中选取b个的选取方案数。

【数据规模和约定】

30%的数据中 N,M ≤ 5000;

60%的数据中 N,M ≤ 25000;

100%的数据中 N,M ≤ 50000,1 ≤ L < R ≤ N,Ci ≤ N。

单个测试点时限2S

对于上述这道题,30%的数据我们可以对于每个询问都扫描询问区间中所存在的数然后计算,这样单次复杂度是O(N)的,但有M的询问,总复杂度是O(MN)。这就显得有点不太能接受了。

但是当我们知道一个询问[l,r]的答案后,[l+1,r],[l-1,r],[l,r+1],[l,r-1]这四个区间的答案可以通过计算做到O(1)的时间内得到

所以我们可以考虑莫队算法,分为如下三步。

1、分块

2、把所有询问左端点排序

3、对于左端点在同一块内的询问按右端点排序,然后分三种情况统计。

而复杂度正如理论部分所说的一样,

一、i与i+1在同一块内,r单调递增,所以r是O(N)的。由于有sqrt(N)块,所以这一部分时间复杂度是Nsqrt(N)。
二、i与i+1跨越一块,r最多变化n,由于有sqrt(N)块,所以这一部分时间复杂度是Nsqrt(N)
三、i与i+1在同一块内时变化不超过sqrt(N),跨越一块也不会超过2* sqrt(N),不妨看作是sqrt(N)。由于有N个数,所以时间复杂度是O(Nsqrt(N))
可以证明复杂度是O(Nsqrt(N))了

理论2:

可现在有很多问题都设置了修改操作,对于这类我们我们又该如何处理呢?

问题:

我们现在有一个长为n的,对于序列,我们有m次操作,操作分为两种

1、询问在[l,r]中抽到两个数字相同的概率

2、把某个位置的数ai改成x

100%的数据中 N,M ≤100000,1 ≤ L < R ≤ N,Ci ≤ N。

单个测试点时限10S

我们会发现,加上了修改操作后。就没办法直接按照分块来处理解决询问的顺序。

定义B为分块的大小。

首先考虑没有修改操作,那么就和理论1中小Z的袜子一样,令B = sqrt(n) 。把所有询问左端点排序,对于左端点在同一块内的询问按右端点排序,然后写莫队算法,按顺序扫询问,这样是O(n sqrt(n))。如果现在加上修改操作考虑一个询问(l,r),这样是肯定不够的。

于是变成:(l,r,ti),ti是询问时的时间,即这次询问是第几次操作。把所有询问左端点l排序,对于左端点在同一块内的询问按右端点r所在的块排序,对右端点r所在块相同的我们再按照时间ti排序。

然后做莫队算法,按顺序扫询问,时间有时向前有时倒流。这样令B = n ^ (2 / 3),因为在每一块中时间最多从1到T改变一次,设询问操作p1次,修改操作p2次,则在最差情况下的时间复杂度是O(p1 n^(2 / 3)+p2 *n^(1 / 3)* n^(1 / 3))=O(n^(5 / 3))【n与m等价】,这在时限下基本是可以得到答案的。

那么还有个遗留的问题,如何处理时间。我们只需要记录修改前和修改后该点的值就可以了。

至此这个问题完美解决。

C语言实现:

#include <bits/stdc++.h>
#define PB push_back
#define MP make_pair
using namespace std;
typedef long long LL;
typedef pair<int,int> PII;
#define PI acos((double)-1)
#define E exp(double(1))
#define K 100000+9
int a[K],vis[K],pos[K*10];
int sz,n,m;
double ans[K*10],sum,num;
map<int,int>p;
struct node
{
    int l,r,id;
}q[K*10];
int cmp(node ta,node tb)
{
    if(pos[ta.l]==pos[tb.l])return ta.r<tb.r;
    return pos[ta.l]<pos[tb.l];
}
void add(int x)
{
    if(!vis[p[a[x]]])sum+=a[x],num++;
    vis[p[a[x]]]++;
}
void del(int x)
{
    if(vis[p[a[x]]]==1)sum-=a[x],num--;
    vis[p[a[x]]]--;
}
void slove(void)
{
    int l,r;
    l=1;r=0;
    sum=num=0;
    memset(vis,0,sizeof(vis));
    for(int i=1;i<=m;i++)
    {
        while(l<q[i].l)del(l++);
        while(l>q[i].l)add(--l);
        while(r<q[i].r)add(++r);
        while(r>q[i].r)del(r--);
        ans[q[i].id]=sum*1.0/num;
    }
}

int main(void)
{
    int t,cs=1;
    cin>>t;
    while(t--)
    {
        scanf("%d",&n);
        p.clear();
        for(int i=1,cnt=1,x;i<=n;i++)
        {
            scanf("%d",&x);
            if(p[x]==0)p[x]=cnt++;
            a[i]=x;
        }
        scanf("%d",&m);
        sz=sqrt(n);
        for(int i=1;i<=m;i++)
        {
            scanf("%d%d",&q[i].l,&q[i].r);
            pos[q[i].l]=(q[i].l-1)/sz+1;
            q[i].id=i;
        }
        sort(q+1,q+m+1,cmp);
        slove();
        printf("Case %d:\n",cs++);
        for(int i=1;i<=m;i++)
            printf("%.6f\n",ans[i]);
    }

    return 0;
}

总结:

也许莫队是一种看起来复杂度非常高的算法,但如果合理地处理好分块的大小和询问的顺序,,它便可以变成一个极其有效的工具。

辞谢

Vfleaking、莫涛

参考文献

国家集训队命题《小z的袜子》

时间: 2024-08-03 06:46:44

莫队算法详解和c实现的相关文章

CodeForces - 86D 莫队算法

http://codeforces.com/problemset/problem/86/D 莫队算法就是调整查询的顺序,然后暴力求解. 每回可以通过现有区间解ans(l,r)得到区间(l+1,r),(l-1,r),(l,r+1),(l,r-1)的区间解. 调整方式http://blog.csdn.net/bossup/article/details/39236275 这题比那个还要简单,查询的是K^2*Z,很清楚就是莫队算法,然而做的时候没有学过,回来补题补到 关键是我一直没明白为什么重载小于号

B 洛谷 P3604 美好的每一天 [莫队算法]

题目背景 时间限制3s,空间限制162MB 素晴らしき日々 我们的情人,不过是随便借个名字,用幻想吹出来的肥皂泡,把信拿去吧,你可以使假戏成真.我本来是无病呻吟,漫无目的的吐露爱情---现在这些漂泊不定的鸟儿有地方栖息了,你可以从信里看出来.拿去吧---由于不是出自真心,话就说得格外动听,拿去吧,就这么办吧... 由于世界会在7月20日完结,作为救世主,间宫卓司要在19日让所有人回归天空 现在已经是19日傍晚,大家集合在C栋的天台上,一共n个人 在他们面前,便是终之空,那终结的天空 题目描述 回

Hdu5145NPY and girls莫队算法

Problem Description NPY's girlfriend blew him out!His honey doesn't love him any more!However, he has so many girlfriend candidates.Because there are too many girls and for the convenience of management, NPY numbered the girls from 1 to n.These girls

BZOJ 1878 SDOI2009 HH的项链 树状数组/莫队算法

题目大意:给定一个序列.求一个区间内有多少个不同的数 正解是树状数组 将全部区间依照左端点排序 然后每次仅仅统计左端点開始的每种颜色的第一个数即可了 用树状数组维护 我写的是莫队算法 莫队明显能搞 m√m明显慢了点可是还是能接受的一个复杂度 一開始离散化数组开小了各种秒RE-- 跪了 #include<cmath> #include<cstdio> #include<cstring> #include<iostream> #include<algori

bzoj 2038 [2009国家集训队]小Z的袜子(hose) 莫队算法

2038: [2009国家集训队]小Z的袜子(hose) Time Limit: 20 Sec  Memory Limit: 259 MBSubmit: 10239  Solved: 4659[Submit][Status][Discuss] Description 作为一个生活散漫的人,小Z每天早上都要耗费很久从一堆五颜六色的袜子中找出一双来穿.终于有一天,小Z再也无法忍受这恼人的找袜子过程,于是他决定听天由命--具体来说,小Z把这N只袜子从1到N编号,然后从编号L到R(L 尽管小Z并不在意两

BZOJ 3236 AHOI 2013 作业 莫队算法

题目大意:给出一些数,问在一个区间中不同的数值有多少种,和在一个区间中不同的数值有多少个. 思路:由于没有修改,所以就想到了莫队算法.然后我写了5K+的曼哈顿距离最小生成树,然后果断T了.(100s的时限啊,刷status都要刷疯了..,结果最后加了手写读入也没能A).后来果断放弃,写了分块版的莫队算法.84sAC...这题卡的..貌似莫队并不是正解. 其实用分块来写莫队就很简单了.只需要将所有询问的区间排序,左端点所在块作为第一键值,右端点作为第二季键值排序,之后就可以转移了.理论上来时还是曼

BZOJ 2589 Spoj 10707 Count on a tree II 强制在线莫队算法(TLE)

题目大意:给定一棵树,每个节点有一个颜色,多次询问某条路径上颜色数量,强制在线 正解是块状数组,强制在线莫队会TLE到死,想AC这道题的不用看了 如果朴素的跑树上莫队其实并不难- - 但是强制在线 因此我们可以考虑强制在线莫队算法 将树分成O(n^1/3)块,每块大小O(n^2/3) 记录每两块之间的答案.每种颜色的出现次数和哪些点被记录到了答案中 每次查询先找到两端点所在块的端点的答案,然后暴力用莫队转移即可 空间复杂度O(n^1/3)*O(n^1/3)*O(n)=O(n^5/3) 预处理时间

最短路SPFA 算法详解

最短路SPFA 算法详解 适用范围:给定的图存在负权边,这时类似Dijkstra等算法便没有了用武之地,而Bellman-Ford算法的复杂度又过高,SPFA算法便派上用场了. 我们约定有向加权图G不存在负权回路,即最短路径一定存在.当然,我们可以在执行该算法前做一次拓扑排序,以判断是否存在负权回路,但这不是我们讨论的重点. 算法思想:我们用数组d记录每个结点的最短路径估计值,用邻接表来存储图G.我们采取的方法是动态逼近法:设立一个先进先出的队列用来保存待优化的结点,优化时每次取出队首结点u,并

最短路算法 :Bellman-ford算法 &amp; Dijkstra算法 &amp; floyd算法 &amp; SPFA算法 详解

 本人QQ :2319411771   邮箱 : [email protected] 若您发现本文有什么错误,请联系我,我会及时改正的,谢谢您的合作! 本文为原创文章,转载请注明出处 本文链接   :http://www.cnblogs.com/Yan-C/p/3916281.html . 很早就想写一下最短路的总结了,但是一直懒,就没有写,这几天又在看最短路,岁没什么长进,但还是加深了点理解. 于是就想写一个大点的总结,要写一个全的. 在本文中因为邻接表在比赛中不如前向星好写,而且前向星效率并