莫队算法

转载自https://www.cnblogs.com/Paul-Guderian/p/6933799.html

原文更为清晰仔细,并同时解释带修莫队与树上莫队。

自己学习。

目前的题型概括为三种:普通莫队,树形莫队以及带修莫队。

若谈及入门,那么BZOJ2038的美妙袜子一题堪称顶尖。

【例题一】袜子

·述大意:

     进行区间询问[l,r],输出该区间内随机抽两次抽到相同颜色袜子的概率。

·分析:

     首先考虑对于一个长度为n区间内的答案如何求解。题目要求Ans使用最简分数表示:那么分母就是n*n(表示两两袜子之间的随机组合),分子是一个累加和,累加的内容是该区间内每种颜色i出现次数sum[i]的平方。

     将莫队算法抬上议程。莫队算法的思路是,离线情况下对所有的询问进行一个美妙的SORT(),然后两个指针l,r(本题是两个,其他的题可能会更多)不断以看似暴力的方式在区间内跳来跳去,最终输出答案。

     掌握一个思想基础:两个询问之间的状态跳转。如图,当前完成的询问的区间为[a,b],下一个询问的区间为[p,q],现在保存[a,b]区间内的每个颜色出现次数的sum[]数组已经准备好,[a,b]区间询问的答案Ans1已经准备好,怎样用这些条件求出[p,q]区间询问的Ans2?

image

考虑指针向左或向右移动一个单位,我们要付出多大的代价才能维护sum[]和Ans(即使得sum[],Ans保存的是当前[l,r]的正确信息)。我们美妙地对图中l,r的向右移动一格进行分析:

                                    image

如图啦。l指针向右移动一个单位,所造成的后果就是:我们损失了一个绿色方块。那么怎样维护?美妙地,sum[绿色]减去1。那Ans如何维护?先看分母,分母从n2变成(n-1)2,分子中的其他颜色对应的部分是不会变的,绿色却从sum[绿色]2变成(sum[绿色]-1)2 ,为了方便计算我们可以直接向给Ans减去以前该颜色的答案贡献(即sum[绿色]2)再加上现在的答案贡献(即(sum[绿色]-1)2 )。同理,观赏下面的r指针移动,将是差不多的。

                                      image

·如图r指针的移动带来的后果是,我们多了一个橙色方块。所以操作和上文相似,只不过是sum[橙色]++。

·回归正题地,我们美妙的发现,知道一个区间的信息,要求出旁边区间的信息(旁边区间指的是当前区间的一个指针通过加一减一得到的区间),竟只需要O(1)的时间。

·就算是这样,到这里为止的话莫队算法依旧无法焕发其光彩,原因是:如果我们以读入的顺序来枚举每个询问,每个询问到下一个询问时都用上述方法维护信息,那么在你脑海中会浮现出l,r跳来跳去的疯狂景象,疯狂之处在于最坏情况下时间复杂度为:O(n2)————如果要这样玩,那不如写一个暴力程序。

·“莫队算法巧妙地将询问离线排序,使得其复杂度无比美妙……”在一般做题时我们时常遇到使用排序来优化枚举时间消耗的例子。莫队的优化基于分块思想:对于两个询问,若在其l在同块,那么将其r作为排序关键字,若l不在同块,就将l作为关键字排序(这就是双关键字)。大米饼使用Be[i]数组表示i所属的块是谁。排序如:

image

·值得强调的是,我们是在对询问进行操作。

·时间复杂度分析(分类讨论思想):

首先,枚举m个答案,就一个m了。设分块大小为unit。

分类讨论:

①l的移动:若下一个询问与当前询问的l所在的块不同,那么只需要经过最多2*unit步可以使得l成功到达目标.复杂度为:O(m*unit)

②r的移动:r只有在Be[l]相同时才会有序(其余时候还是疯狂地乱跳,你知道,一提到乱跳,那么每一次最坏就要跳n次!),Be[l]什么时候相同?在同一块里面l就Be[]相同。对于每一个块,排序执行了第二关键字:r。所以这里面的r是单调递增的,所以枚举完一个块,r最多移动n次。总共有n/unit个块:复杂度为:O(n*n/unit)

总结:O(n*unit+n*n/unit)(n,m同级,就统一使用n)

根据基本不等式得:当n为sqrt(n)时,得到莫队算法的真正复杂度:

O(n*sqrt(n))

·代码上来了(莫队喜欢while):

 

复制代码

 1 #include<stdio.h>
 2 #include<algorithm>
 3 #include<iostream>
 4 #include<math.h>
 5 #include<cstring>
 6 #define go(i,a,b) for(int i=a;i<=b;i++)
 7 #define mem(a,b) memset(a,b,sizeof(a))
 8 #define ll long long 
 9 using namespace std;const int N=50003;
10 struct Mo{int l,r,ID;ll A,B;}q[N];ll S(ll x){return x*x;}
11 ll GCD(ll a,ll b){while(b^=a^=b^=a%=b);return a;}
12 int n,m,col[N],unit,Be[N];ll sum[N],ans;
13 bool cmp(Mo a,Mo b){return Be[a.l]==Be[b.l]?a.r<b.r:a.l<b.l;}
14 bool CMP(Mo a,Mo b){return a.ID<b.ID;};
15 void revise(int x,int add){ans-=S(sum[col[x]]),sum[col[x]]+=add,ans+=S(sum[col[x]]);}
16 int main()
17 {
18     scanf("%d%d",&n,&m);unit=sqrt(n);
19     go(i,1,n)scanf("%d",&col[i]),Be[i]=i/unit+1;;
20     go(i,1,m)scanf("%d%d",&q[i].l,&q[i].r),q[i].ID=i;
21     
22     sort(q+1,q+m+1,cmp);
23     
24     int l=1,r=0;
25     go(i,1,m)
26     {
27         while(l<q[i].l)revise(l,-1),l++;
28         while(l>q[i].l)revise(l-1,1),l--;
29         while(r<q[i].r)revise(r+1,1),r++;
30         while(r>q[i].r)revise(r,-1),r--;
31         
32         if(q[i].l==q[i].r){q[i].A=0;q[i].B=1;continue;}
33         q[i].A=ans-(q[i].r-q[i].l+1);
34         q[i].B=1LL*(q[i].r-q[i].l+1)*(q[i].r-q[i].l);
35         ll gcd=GCD(q[i].A,q[i].B);q[i].A/=gcd;q[i].B/=gcd;
36     }
37     
38     sort(q+1,q+m+1,CMP);
39     go(i,1,m)printf("%lld/%lld\n",q[i].A,q[i].B);
40     return 0;
41 }//Paul_Guderian

复制代码

莫队算法是一种基于分块的法,用于解决一些静态区间查询问题,时间复杂度为 $O(n\sqrt{n})$。以下是一个基于Python的莫队算法的示例代码: ```python import math # 定义块的大小 BLOCK_SIZE = 0 # 初始化块的大小 def init_block_size(n): global BLOCK_SIZE BLOCK_SIZE = int(math.sqrt(n)) # 定义查询操作 def query(left, right): pass # 在这里写查询操作的代码 # 定义添加操作 def add(x): pass # 在这里写添加操作的代码 # 定义删除操作 def remove(x): pass # 在这里写删除操作的代码 # 定义莫队算法 def mo_algorithm(n, q, queries): init_block_size(n) queries.sort(key=lambda x: (x[0] // BLOCK_SIZE, x[1])) left, right = 0, -1 for query in queries: while left > query[0]: left -= 1 add(left) while right < query[1]: right += 1 add(right) while left < query[0]: remove(left) left += 1 while right > query[1]: remove(right) right -= 1 query(query[0], query[1]) ``` 在这段代码中,我们首先定义了一个全局变量 `BLOCK_SIZE`,用于表示块的大小。接着,我们定义了三个操作函数 `query()`、`add()` 和 `remove()`,分别用于查询、添加和删除元素。在 `mo_algorithm()` 函数中,我们首先调用 `init_block_size()` 函数初始化块的大小,然后将查询操作按照块的大小和右端点排序,接着使用双指针维护当前查询区间的左右端点,每次移动指针时调用 `add()` 和 `remove()` 函数更新块的状态,最后调用 `query()` 函数进行查询操作。 请注意,这段代码只是一个示例,具体的 `query()`、`add()` 和 `remove()` 函数的实现取决于具体的问题。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值