莫队入门 | P1972 HH的项链
博客食用更佳
前置知识:分块。
莫队
OI-wiki
由莫涛神仙老爷发明的算法。这是一种离线分治算法,即先收集到所有的询问,然后自己通过排序来处理回答询问的顺序。
模板题:
SP3267 D-query
P1972 HH的项链
内容完全一样,不过第二题在2021年被lxl加了数据卡了莫队,各种乱搞优化似乎都过不了了。
问题描述:
现有共有
数据规模:
方法:
1.暴力,时间复杂度
2.莫队
开个桶,记录每个数在当前区间(不是目标区间)的出现次数,变量 now 记录当前区间内出现的不同的数的种数。
我们每次不用求出冗余的部分,可以从上次询问开始。对询问排序的方法我们后续讨论。
int l=0,r=-1;//避免莫队玄学bug,一般习惯这样起手
void add(int p)//当前区间加上这个位置的值
{
if(!cnt[a[p]]) now++;
cnt[a[p]]++;
}
void del(int p)//当前区间减去这个位置的值
{
if(cnt[a[p]]==1) now--;
cnt[a[p]]--;
}
struct Node
{
int l,r,id;
//l和r是查询范围,id是查询序号,这样在排序、计算之后合起来回答的时候可以知道哪个结果是第几次查询的。
}q[Maxn];//询问
for(int i=1;i<=m;i++)//main函数中的部分
{
while(q[i].r>r) add(++r);//当前右边界边向目标右边界右移
while(q[i].r<r) del(r--);//当前右边界边向目标右边界左移
while(q[i].l>l) del(l++);//当前左边界边向目标左边界右移
while(q[i].l<l) add(--l);//当前左边界边向目标左边界左移
ans[q[i].id]=now;
}
这也是最基本的莫队模板。
3.我们再来优化。
想想之前的方法哪里有疏漏?
顺序。如果给的询问是这样的:
1 2
999999 1000000
3 4
999997 999998
......
死因:反复拉扯。所以我们必须要排序。
如果按照左端点排序,右边反复拉扯还是可以把复杂度打回
找到了一种合理一些的排序方法:分块后,按照左端点所在块递增排序,左端在同块的时候右端递增。这样的话就不会因为左边一小点差距然后被拉来拉去,可以放过左端点一小点的差距,在每次都微调一下,右端点放开跑也不会折返太多。
bool operator<(Node n1,Node n2)
{
return (k[n1.l]^k[n2.l])?k[n1.l]<k[n2.l]:n1.r<n2.r;
}
另外,在明白了莫队的基本原理之后,也可以试试这种排序方法:
bool operator<(Node n1,Node n2)
{
return (k[n1.l]^k[n2.l])?k[n1.l]<k[n2.l]:((k[n1.l]&1)?n1.r < n2.r : n1.r > n2.r);
}
左端点在奇数块的时候右端递增排序,左端点在偶数块的时候右端点递减排序。这样可以在左端点换块的时候右端点换向,来回的时候都可以计算区间结果,不用右端点无谓地回来再向右来浪费时间。
开桶。开桶导致时间复杂度很高,因为我们要频繁地改
有没有方法能优化这一步:有。
摒弃桶的方法,预处理记录每一个位置的前一个和后一个和它数一样的位置。不用每次移动的时候记录和判断cnt的数量,我们可以用这种方法(以左端点向右从
记
若
反之若 now-- 。
参考代码:(为了调掉玄学 bug ,我冒着被大家喷的风险做了0打头处理)
memset(last,0xff,sizeof(last));
for(int i=0;i<n;i++)
{
pre[i]=last[a[i]];
last[a[i]]=i;
if(pre[i]>=0) nxt[pre[i]]=i;
nxt[i]=n;
}//预处理部分
//脑袋转不过来的可以打开excel表格,对着样例模拟数据
//我打了半个小时,我比较蒻
for(int i=1;i<=m;i++)
{
while(l<q[i].l) now-=(r<nxt[l++]);
while(l>q[i].l) now+=(r<nxt[--l]);
while(r<q[i].r) now+=(l>pre[++r]);
while(r>q[i].r) now-=(l>pre[r--]);
ans[q[i].id]=now;
}//相应地,后面的挪左右指针也做了处理
再带上 register inline IO优化 之类的基础优化操作,我将之前总用时26s调到了8.30s,那些卡莫队的数据最大的点我也就 686ms ,顺利 AC 。
AC记录
祝你好运qwq~。