一、前人种树
博客:莫队算法
博客:莫队算法
博客:莫队算法详解
二、相关定义
【问题引入】
给定一个大小为N的数组arr,数组中所有元素的大小arr[i]<=N。你需要回答M个查询,每个查询的形式是L,R。你需要回答在范围[L,R]中至少重复3次的数字的数目num。
例如:数组arr为{ 1,2,3,1,1,2,1,2,3,1 }(索引从0开始)
查询:L = 0,R = 4。num= 1。在范围[L,R]中的值 = { 1,2,3,1,1 },只有1是至少重复3次的,所以至少重复3次的元素数目num=1。
查询:L = 1,R = 8。num= 2。在范围[L,R]中的值 = { 2,3,1,1,2,1,2,3 }, 1重复3次并且2重复3次,所以至少重复3次的元素数目num=2。
【暴力解法】
对于每一个查询,从L至R循环,统计元素出现频率,报告答案。考虑查询次数为M的情况,那么程序在最坏的情况的时间复杂度为O(mn):
int search_arr(int a[],int l,int r) { int num=0; for(int i=l;i<=r;i++){ count[a[i]]++; //count数组的定义为int count[N] = {0}; if(count[i] == 3){ num++; } } return num; }
对上述算法稍作修改,它时间复杂度仍然为O(mn):
add(position): count[array[position]]++ if count[array[position]] == 3: answer++ remove(position): count[array[position]]-- if count[array[position]] == 2: answer-- currentL = 0 currentR = 0 answer = 0 count[] = 0 for each query: // currentL 应当到 L ,currentR 应当到 R while currentL < L: remove(currentL) currentL++ while currentL > L: add(currentL) currentL-- while currentR < R: add(currentR) currentR++ while currentR > R: remove(currentR) currentR-- output answer
最初我们总是从L至R循环,但现在我们从上一次查询的位置调整到当前的查询的位置。例如:
如果上一次的查询是L = 3,R = 10,则我们在查询结束时有currentL=3、currentR=10。
如果下一个查询是L = 5,R = 7,则我们将currentL移动到5,currentR移动到7。
add函数意味着我们添加该位置的元素到当前集合内,并且更新相应的回答。
remove函数意味着我们从当前集合内移除该位置的元素,并且更新相应的回答。
【莫队算法】
惊奇之处:莫队算法仅仅调整我们处理查询的顺序。
- 首先,准备基础的东东:
在我们得到了M个查询之后,我们将把查询以一个特定的顺序进行重新排序,然后处理它们;(符合离线算法的特征)
每个查询都有L和R,我们称呼其为“起点”和“终点”;
让我们将给定的输入数组分为√N块,每一块的大小为 N/√N=√N;
每个“起点”落入其中的一块,每个“终点”也落入其中的一块,可以有很多的查询属于同一块;
如果某查询的“起点”落在第p块中,则该查询属于第p块。
- 接下来将打开莫队算法神奇的大门
该算法将处理第1块中的查询,然后处理第2块中的查询,等等,最后直到第N−−√块。我们已经有一个顺序、查询按照所在的块升序排列;
从现在开始,我会忽略(其它)所有的块,只关注我们如何询问和回答第1块。我们将对所有块做同样的事。(第1块中的)所有查询的“起点”属于第1块,但“终点”可以在(包括第1块在内的)任何块中。现在让我们按照R值升序的顺序重新排列这些查询。我们也在所有的块中做这个操作(指每个块块内按R升序排列)。
最终的排序是怎样的?
所有的询问首先按照所在块的编号升序排列(所在块的编号是指询问的“起点”属于的块)。如果编号相同,则按R值升序排列。
例如考虑如下的询问,假设我们会有3个大小为3的块(0-2,3-5,6-8):
{0, 3} {1, 7} {2, 8} {7, 8} {4, 8} {4, 4} {1, 2} //7个询问
让我们先根据所在块的编号重新排列它们
{0, 3} {1, 7} {2, 8} {1, 2} (|) {4, 8} {4, 4} (|) {7, 8} //先按编号,粗略地排
现在我们按照R的值重新排列
{1, 2} {0, 3} {1, 7} {2, 8} (|) {4, 4} {4, 8} (|) {7, 8}
现在我们使用与上一节所述相同的代码来解决这个问题。上述算法是正确的,因为我们没有做任何改变,只是重新排列了查询的顺序。
add(position): count[array[position]]++ if count[array[position]] == 3: answer++ remove(position): count[array[position]]-- if count[array[position]] == 2: answer-- currentL = 0 currentR = 0 answer = 0 count[] = 0 for each query: // currentL 应当到 L ,currentR 应当到 R while currentL < L: remove(currentL) currentL++ while currentL > L: add(currentL) currentL-- while currentR < R: add(currentR) currentR++ while currentR > R: remove(currentR) currentR-- output answer
【正确性证明】
我们完成了莫队算法,它只是一个重新排序。可怕的是它的运行时间分析。原来,如果我们按照我上面指定的顺序,我们所写的O(N2)的代码运行在O(N*√N)时间复杂度上。可怕,这是正确的,仅仅是重新排序查询使我们把复杂度从O(N2)降低到O(N*√N),而且也没有任何进一步的代码上的修改。好哇!我们将以O(N*√N)的复杂度AC。
看看我们上面的代码,所有查询的复杂性是由 4个while循环决定的。前2个while循环可以表述为“左指针(currentL)的移动总量”,后2个 while循环可以表述为“右指针(currentR)的移动总量”。这两者的和将是总复杂性。
最重要的。让我们先谈论右指针。对于每个块,查询是递增的顺序排序,所以右指针(currentR)按照递增的顺序移动。在下一个块的开始时,指针可能在extreme end(最右端?),将移动到下一个块中的最小的R处。这意味着对于一个给定的块,右指针移动的量是O(N)。我们有O(√N)块,所以总共是O(N*√N)。太好了!
让我们看看左指针怎样移动。对于每个块,所有查询的左指针落在同一个块中,当我们从一个查询移动到另个一查询左指针会移动,但由于前一个L与当前的L在同一块中,此移动是O(√N)(块大小)的。在每一块中左指针的移动总量是O(Q*√N),Q是落在那个块的查询的数量。对于所有的块,总的复杂度为O(M*√N)。
就是这样,总复杂度为O((N+M)*√N)=O(N*√N)。
【适用性】
如前所述,该算法是离线的,这意味着当我们被强制按照特定的顺序查询时,我们不能再使用它。这也意味着当有更新操作时我们不能用这个算法。不仅如此,一个重要的可能的局限性:我们应该能够编写add 和remove函数。会有很多的情况下,add 是平凡的 (指复杂度O(1)?),但remove不是。这样的一个例子就是我们想要求区间内最大值。当我们添加的元素,我们可以跟踪最大值。但当我们删除元素则不是平凡的。不管怎样,在这种情况下,我们可以使用一个集合来添加元素,删除元素和报告 最小值(作者想说最大值?)。在这种情况下,添加和删除操作都是O(logN)(导致了O(N*√N*logN)的算法)。
在许多情况下,我们可以使用此算法。在一些情况下,我们也可以使用其它的数据结构,如线段树,但对于一些问题使用莫队算法的是必须的。
三、一些补充
【理论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),那么便是一个时间复杂度和空间复杂度较为平衡的情况,这样可以很好的解决问题。
【理论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等价】,这在时限下基本是可以得到答案的。
那么还有个遗留的问题,如何处理时间。我们只需要记录修改前和修改后该点的值就可以了。
至此这个问题完美解决。
四、沙场练兵
题目: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必须为最简分数。(详见样例)
Sample Input
1 2 3 3 3 2
2 6
1 3
3 5
1 6
Sample Output
0/1
1/1
4/15
【样例解释】
询问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。
分析一:
对于上述这道题,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)块,所以这一部分时间复杂度是N*sqrt(N);
- i与i+1跨越一块,r最多变化n,由于有sqrt(N)块,所以这一部分时间复杂度是Nsqrt(N);
- i与i+1在同一块内时变化不超过sqrt(N),跨越一块也不会超过2* sqrt(N),不妨看作是sqrt(N)。由于有N个数,所以时间复杂度是O(N*sqrt(N)),可以证明复杂度是O(N*sqrt(N))了。
分析二:
莫队算法可以解决一类不修改、离线查询问题。
构造曼哈顿最小生成树的做法还没有写。
写了个直接分段解决的办法。
把1~n分成sqrt(n)段。
unit = sqrt(n)
m个查询先按照第几个块排序,再按照 R排序。
然后直接求解。
#include <stdio.h> #include <string.h> #include <iostream> #include <algorithm> #include <vector> #include <queue> #include <set> #include <map> #include <string> #include <math.h> #include <stdlib.h> #include <time.h> using namespace std; const int MAXN = 50010; const int MAXM = 50010; struct Query { int L,R,id; }node[MAXM]; long long gcd(long long a,long long b) { if(b == 0)return a; return gcd(b,a%b); } struct Ans { long long a,b;//分数a/b void reduce()//分数化简 { long long d = gcd(a,b); a /= d; b /= d; } }ans[MAXM]; int a[MAXN]; int num[MAXN]; int n,m,unit; bool cmp(Query a,Query b) { if(a.L/unit != b.L/unit)return a.L/unit < b.L/unit; else return a.R < b.R; } void work() { long long temp = 0; memset(num,0,sizeof(num)); int L = 1; int R = 0; for(int i = 0;i < m;i++) { while(R < node[i].R) { R++; temp -= (long long)num[a[R]]*num[a[R]]; num[a[R]]++; temp += (long long)num[a[R]]*num[a[R]]; } while(R > node[i].R) { temp -= (long long)num[a[R]]*num[a[R]]; num[a[R]]--; temp += (long long)num[a[R]]*num[a[R]]; R--; } while(L < node[i].L) { temp -= (long long)num[a[L]]*num[a[L]]; num[a[L]]--; temp += (long long)num[a[L]]*num[a[L]]; L++; } while(L > node[i].L) { L--; temp -= (long long)num[a[L]]*num[a[L]]; num[a[L]]++; temp += (long long)num[a[L]]*num[a[L]]; } ans[node[i].id].a = temp - (R-L+1); ans[node[i].id].b = (long long)(R-L+1)*(R-L); ans[node[i].id].reduce(); } } int main() { //freopen("in.txt","r",stdin); //freopen("out.txt","w",stdout); while(scanf("%d%d",&n,&m) == 2) { for(int i = 1;i <= n;i++) scanf("%d",&a[i]); for(int i = 0;i < m;i++) { node[i].id = i; scanf("%d%d",&node[i].L,&node[i].R); } unit = (int)sqrt(n); sort(node,node+m,cmp); work(); for(int i = 0;i < m;i++) printf("%lld/%lld ",ans[i].a,ans[i].b); } return 0; }