莫队--------一个优雅的暴力
莫队是一个可以在O(n√n)内求出绝大部分无修改的离线的区间问题的答案(只要问题满足转移是O(1)的)即你已知区间[l,r]的解,能在O(1)的时间内求出[l-1,r][l+1,r][l,r-1][l,r+1]的解。否则时间复杂度为O(kn√n)(k为转移的时间)
以下默认转移是O(1)的
显然,我们如果得知[l,r]的解,我们便可以在O(|l2-l|+|r2-r|)的时间内求出[l2,r2]的解
那么,对于q个询问(假设q与n同数量级),我们如果能找到一个合适的顺序求解这q个询问,便能降低时间复杂度
对于最原始的暴力,我们每次从l遍历到r来求解(l,r为区间查询的左右端点),复杂度O(n^2)
试着找到一个合适的求解顺序,我们把l,r想象成一个平面直角坐标系上的点(l,r),则所有询问间转移的花费就为在沿着这个平面上最小直线斯坦纳树做的花费。
挺简单的是吧?
但是我们有个良好的替代品,分块。
我们把整个数列分成T块(T=√n),然后我们记下每个询问的左端点l所在的块block[i],按block为第一关键字,r为第二关键字从小到大排序,依次求解就能使时间复杂度降至O(n√n)
简单说明一下:
左指针移动最坏情况下是从一块的开始跳到结尾然后再跳回开始......(因为你从这块跳到下一块后就无法跳回来了)时间复杂度O(n√n)
右指针移动最坏情况下是对于每个在不同块内的左指针,右指针都要从头跳到尾(在同一块内的多个左指针,从头跳到尾的过程中肯定也一起处理了)时间复杂度O(n√n)
在有些情况下,如果莫队的删除操作时间复杂度过高,而添加操作的时间复杂度极低,那我们考虑维护询问左端点所在的块的右端点到询问右端点之间的答案(左右端点所在块相同的暴力for一遍处理) (左端点到左端点所在的块的右端点的答案暴力计算)具体参见[BZOJ4241]历史研究(回滚莫队)
此外,莫队还有一个很好的Debug的方法,就用样例,不断调整分块大小T,看看答案是否不变。
[洛谷2709] 小B的询问
题目描述
小B有一个序列,包含N个1~K之间的整数。他一共有M个询问,每个询问给定一个区间[L..R],求Sigma(c(i)^2)的值,其中i的值从1到K,其中c(i)表示数字i在[L..R]中的重复次数。小B请你帮助他回答询问。
输入输出格式
输入格式:
第一行,三个整数N、M、K。
第二行,N个整数,表示小B的序列。
接下来的M行,每行两个整数L、R。
输出格式:
M行,每行一个整数,其中第i行的整数表示第i个询问的答案。
输入输出样例
说明
对于全部的数据,1<=N、M、K<=50000
我们这道题为例讲讲莫队的实现,首先排序,注意排序是把询问排序,不是叫你把原数组排序
然后初始化双指针l=1,r=0,表示我们已经得知[l,r]的答案
我们用cnt数组记录每个数的出现次数(如果K<=10^9,那么需要离散化)
然后莫队最难的地方来了,转移。即用区间[l,r]求出区间[l-1,r][l+1,r][l,r-1][l,r+1]的值
由完全平方公式可知,区间中多出一个数字i,对答案的贡献为i之前的出现次数(即cnt[i])*2+1,区间中少一个数字i,对答案的贡献为-cnt[i]*2+1
语序!!!显然,我们是先计算答案后cnt++(--),但是l和r什么时候++(--)
对于使区间变大的操作(即l--,r++)我们应先使l--,r++后再进行后续的操作(因为这样我们操作的数才是我们真正添加进来的数)
对于使区间变小的操作(即l++,r--)我们应先进行操作后使l++,r--(因为这样我们操作的数才是我们真正删除的数)
绿色是要加入(删除的数),黄色是此时的r指针,自己对着理解一遍吧
#include<iostream> #include<cstdio> #include<algorithm> #include<cmath> using namespace std; struct xxx{ int l,r,id,block; }q[100000]; int cnt[100000],a[100000];long long ans[100000]; bool cmp(xxx a,xxx b){if(a.block!=b.block)return a.block<b.block;return a.r<b.r;} int main() { int n,m,k;scanf("%d%d%d",&n,&m,&k);int T=(int)sqrt((double)n); for(int i=1;i<=n;i++)scanf("%d",&a[i]); for(int i=1;i<=m;i++) { scanf("%d%d",&q[i].l,&q[i].r);q[i].id=i;q[i].block=(q[i].l+1)/T; } sort(q+1,q+m+1,cmp); int l=1,r=0;long long sum=0; for(int i=1;i<=m;i++) { while(r<q[i].r){sum=sum+2*cnt[a[++r]]+1;cnt[a[r]]++;} while(r>q[i].r){sum=sum-2*cnt[a[r]]+1;cnt[a[r--]]--;} while(l<q[i].l){sum=sum-2*cnt[a[l]]+1;cnt[a[l++]]--;} while(l>q[i].l){sum=sum+2*cnt[a[--l]]+1;cnt[a[l]]++;} ans[q[i].id]=sum; } for(int i=1;i<=m;i++)printf("%lld ",ans[i]); return 0; }
那如果区间操作不只有查询,还有修改呢?(以下3√n表示3次根号n)(注意,莫队的修改只能单点修改)
以下记题目给出的原始数组为a数组
我们把所有操作按发生时间假想成一个时间轴,绿色表示修改操作,红色表示查询操作
我们可以发现,对于每个查询操作,它只和在它之前发生的修改操作有关
我们考虑用x[i]表示第i个查询操作前进行了多少次修改,然后再用一个变量now记录当前进行了多少次修改,这样我们就能计算出要还原(新增)几次修改。
对于修改,我们要记录它的几个要素,pre:修改前的值(便于还原),val:修改后的值(便于修改),no:修改哪个数。
然后像暴力移动l,r指针一样,我们也暴力移动修改指针now
我们把整个数列分成T块(T=3√n),然后我们记下每个询问的左端点l所在的块blockl[i],每个询问的右端点r所在的块blockr[i],按blockl为第一关键字,blockr为第二关键字,x为第三关键字从小到大排序,依次求解就能使时间复杂度降至O(n的5/3次方)
对于每一个修改,分在当前[l,r]区间内(不是当前循环到的query的区间)和区间外考虑,区间外就直接修改a数组的值,区间内要考虑cnt的变化以及sum (当前区间内不同种类的数的个数)的加减(和l,r指针移动一样考虑)
简要说明一下带修改莫队的时间复杂度。
左右指针如上面分析,时间复杂度O(n的5/3次方)
修改指针最坏情况下对于任意两个块lblock(l在的那个块)与rblock(r在的那个块),修改指针最坏从1跑到n,而这样的情况共有3√n ^2种,所以时间复杂度O(n的5/3次方)
注意点:在读入过程中,a数组遇到修改操作也是要修改的,否则接下来的修改操作如果还有修改这一个点的话,一个pre就会记录成最初始的原数组a[]了,最后在所有操作结束后,当前被你修改的乱七八糟的数组还是要还原为a数组。
下面程序中的T是块内元素个数,不是有多少块(3√n^2)
[洛谷1903]【模板】分块/带修改莫队(数颜色)
题目描述
墨墨购买了一套N支彩色画笔(其中有些颜色可能相同),摆成一排,你需要回答墨墨的提问。墨墨会像你发布如下指令:
1、 Q L R代表询问你从第L支画笔到第R支画笔中共有几种不同颜色的画笔。
2、 R P Col 把第P支画笔替换为颜色Col。
为了满足墨墨的要求,你知道你需要干什么了吗?
输入输出格式
输入格式:
第1行两个整数N,M,分别代表初始画笔的数量以及墨墨会做的事情的个数。
第2行N个整数,分别代表初始画笔排中第i支画笔的颜色。
第3行到第2+M行,每行分别代表墨墨会做的一件事情,格式见题干部分。
输出格式:
对于每一个Query的询问,你需要在对应的行中给出一个数字,代表第L支画笔到第R支画笔中共有几种不同颜色的画笔。
输入输出样例
说明
对于100%的数据,N≤10000,M≤10000,修改操作不多于1000次,所有的输入数据中出现的所有整数均大于等于1且不超过10^6。
#include<iostream> #include<cstdio> #include<algorithm> #include<cmath> using namespace std; struct xxx{ int l,r,blockl,blockr,id,t; }data[100000]; struct xxx2{ int pre,val,no; }d[100000]; int a[100000],cnt[1000100],ans[100100]; bool cmp(xxx a,xxx b) { if(a.blockl!=b.blockl)return a.blockl<b.blockl; if(a.blockr!=b.blockr)return a.blockr<b.blockr; return a.t<b.t; } int main() { int n,m;scanf("%d%d",&n,&m);int T=(int)pow((double)n,0.66666666666); for(int i=1;i<=n;i++)scanf("%d",&a[i]); int totq=0,totr=0; for(int i=1;i<=m;i++) { char c[2];int l,r; scanf("%s%d%d",c,&l,&r); if(c[0]=='Q') { data[++totq].l=l;data[totq].r=r;data[totq].id=totq; data[totq].blockl=(l+1)/T;data[totq].blockr=(r+1)/T;data[totq].t=totr; } if(c[0]=='R') { d[++totr].no=l;d[totr].val=r;d[totr].pre=a[l];a[l]=r; } } for(int i=m;i>=1;i--)a[d[i].no]=d[i].pre; sort(data+1,data+totq+1,cmp); int l=1,r=0,now=0,sum=0; for(int i=1;i<=totq;i++) { while(now<data[i].t) { now++; if(d[now].no>=l&&d[now].no<=r) { cnt[d[now].pre]--; if(cnt[d[now].pre]==0)sum--; cnt[d[now].val]++; if(cnt[d[now].val]==1)sum++; } a[d[now].no]=d[now].val; } while(now>data[i].t) { if(d[now].no>=l&&d[now].no<=r) { cnt[d[now].val]--; if(cnt[d[now].val]==0)sum--; cnt[d[now].pre]++; if(cnt[d[now].pre]==1)sum++; } a[d[now].no]=d[now].pre;now--; } while(r<data[i].r){cnt[a[++r]]++;if(cnt[a[r]]==1)sum++;} while(r>data[i].r){cnt[a[r]]--;if(cnt[a[r--]]==0)sum--;} while(l<data[i].l){cnt[a[l]]--;if(cnt[a[l++]]==0)sum--;} while(l>data[i].l){cnt[a[--l]]++;if(cnt[a[l]]==1)sum++;} ans[data[i].id]=sum; } for(int i=1;i<=totq;i++)printf("%d ",ans[i]); return 0; }