让菜鸡讲一讲莫队(非修改)

本文介绍了一种优雅的暴力算法——莫队算法,用于解决区间处理问题。通过实例讲解了算法的基本原理,包括指针移动、计数更新及复杂度分析,并提供了一个具体的代码示例。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

传说中,莫队算法能解决一切区间处理问题

这是一个优雅的暴力

那么我们先看一道题


蛤蛤的项链

HH有一串由各种漂亮的贝壳组成的项链。

HH相信不同的贝壳会带来好运,所以每次散步完后,他都会随意取出一段贝壳,思考它们所表达的含义。

HH不断地收集新的贝壳,因此,他的项链变得越来越长。

有一天,他突然提出了一个问题:

某一段贝壳中,包含了多少种不同的贝壳?

这个问题很难回答……因为项链实在是太长了。

于是,他只好求助睿智的你,来解决这个问题。

这一题有多组询问。


之前我们解决区间问题的时候常用的一种方法是种一棵线段树

但是现在这东西不满足加和性质,线段树不能用了!

其实要用也是可以的,只不过有很大的几率即使不TLE也会MLE

怎么办?

那么我们祭出我们的莫队算法


莫队的组成部分

首先,莫队有一对指针,现在我们把它们分别叫做l,r

一开始,它们在序列中像这样摆放:

 l
 1234352435142435123   <贝壳颜色v>
r

然后,莫队有一排计数君cnt[]

它们统计在l~r区间中每种颜色出现了多少次

如果一开始不把l放在r前面的话,那么cnt[]将不能很好的反映出l~r区间的颜色情况

具体为什么请自行脑补

当l,r移动的时候,

cnt[]的值可以很方便的作出修改

比如现在

l=2
r=5
cnt={0,0,1,2,1,0}
  l
 1234352435142435123   <v>
     r

如果我们要得到l=1,r=6的情况

那么我们可以把l向左移动,把r向右移动

l--,cnt[v[l]]++;r++,cnt[v[r]]++;
//简写为cnt[v[--l]]++,cnt[v[++r]]++;
"因为它们是扩张,所以要先移动再计数"
l=1
r=6
cnt={0,1,1,2,1,1}
 l
 1234352435142435123   <v>
      r

又如果我们要得到l=3,r=4的情况

那么我们可以把l向右移动,把r向左移动

cnt[v[l]]--,l++;cnt[v[r]]--,r--;
//简写为--cnt[v[l++]],--cnt[v[r--]];
"因为它们是缩小,所以要先计数再移动"
l=3
r=4
cnt={0,0,1,1,0,0}
   l
 1234352435142435123   <v>
    r

莫队还有一个统计答案sum

可以在区间修改的时候顺便修改它

比如现在

l=2
r=3
sum=2
cnt={0,0,1,1,0,0}
  l
 1234352435142435123   <v>
   r

当我们要把l向右移1格的时候,

颜色2的出现次数减少了1,

而由于减少后2的出现次数变成了0,那么sum就要减少1

if(--cnt[v[l++]]==0)sum--;
if(--cnt[v[r--]]==0)sum--;//同理

而当我们要把r向右移1格的时候,

颜色4的出现次数增加了1,

而由于增加前4还没有出现过,那么sum就要增加1

if(cnt[v[++r]]++==0)sum++;
if(cnt[v[--l]]++==0)sum++;//同理

有没有想过,l和r这么移来移去是为了什么?

如果这道题只有1次询问,我们大可以暴力。

而莫队这样做,是为了处理多组询问。

每一次询问都可以在上次询问的基础上通过移动l,r来回答。

辣么可见这个算法的复杂度就由它们的移动次数来决定。

如何减少l,r的移动次数

显然对于两次询问L,R和L′,R′,

知道了L,R的答案,

就可以暴力计算|L−L′|+|R−R′|次得出L′,R′的答案。

Fa♂现|L−L′|+|R−R′|是曼哈顿距离

把每个询问看作是二维平面上的点,那么我们的最小总时间,

就是这些点的最小曼哈顿距离生成树,

按照这个树的顺序做,复杂度变成了\(O(n\sqrt n)\)

不过这样是不是有点繁琐?

分块大法好!

把整个序列分块,把L所在的块为第一关键字,R为第二关键字排序。

为什么要分块不能直接排呢?

分块很好的减少了一种情况的影响:

L<L′<L′′,并且距离很近,但是R′<R<R′′,并且R′与另外两个离得很远,会导致r指针一顿乱跳。

如果直接按L排序就会浪费非常多的时间。

由于每个块的大小是\(\sqrt n\)

分块使得两个询问之间差异平均到了L,R上。

因此,理论复杂度大约还是是\(O(n\sqrt n)\)

给出蛤蛤的项链的代码,

这也可以作为非修改莫队的模板。

#include<bits/stdc++.h>
using namespace std;
inline int gotcha()
{
    register int a=0,b=1,c=getchar();
    while(!isdigit(c))b^=c=='-',c=getchar();
    while(isdigit(c))a=(a<<3)+(a<<1)+c-48,c=getchar();
    return b?a:-a;
}
const int _ = 50002 , __ = 2000002 , sect = 667;
struct trouble
{
    int l,r,sc,num;
    const int operator < (const trouble &b)const{return sc==b.sc?r<b.r:sc<b.sc;}
}q[__];
int n,m,sum=0,cnt[__]={0},col[__],ans[__];
void add(int a){if(++cnt[a]==1)sum++;}void del(int a){if(--cnt[a]==0)sum--;}
int main()
{
    register int i,l=1,r=0;
    for(n=gotcha(),i=1;i<=n;i++)col[i]=gotcha();
    for(m=gotcha(),i=1;i<=m;i++)q[i].l=gotcha(),q[i].r=gotcha(),q[i].sc=q[i].l/sect,q[i].num=i;
    sort(q+1,q+m+1);
    for(i=1;i<=m;i++)
    {
        while(r<q[i].r)add(col[++r]);while(r>q[i].r)del(col[r--]);
        while(l<q[i].l)del(col[l++]);while(l>q[i].l)add(col[--l]);
        ans[q[i].num]=sum;
    }
    for(i=1;i<=m;i++)printf("%d\n",ans[i]);
    return 0;
}

转载于:https://www.cnblogs.com/finder-iot/p/8409570.html

内容概要:本文档主要展示了C语言中关于字符串处理、指针操作以及动态内存分配的相关代码示例。首先介绍了如何实现键值对(“key=value”)字符串的解析,包括去除多余空格和根据键获取对应值的功能,并提供了相应的测试用例。接着演示了从给定字符串中分离出奇偶位置字符的方法,并将结果分别存储到两个不同的缓冲区中。此外,还探讨了常量(const)修饰符在变量和指针中的应用规则,解释了不同类型指针的区别及其使用场景。最后,详细解了如何动态分配二维字符数组,并实现了对这类数组的排序与释放操作。 适合人群:具有C语言基础的程序员或计算机科学相关专业的学生,尤其是那些希望深入理解字符串处理、指针操作以及动态内存管理机制的学习者。 使用场景及目标:①掌握如何高效地解析键值对字符串并去除其中的空白字符;②学会编写能够正确处理奇偶索引字符的函数;③理解const修饰符的作用范围及其对程序逻辑的影响;④熟悉动态分配二维字符数组的技术,并能对其进行有效的排序和清理。 阅读建议:由于本资源涉及较多底层概念和技术细节,建议读者先复习C语言基础知识,特别是指针和内存管理部分。在学习过程中,可以尝试动手编写类似的代码片段,以便更好地理解和掌握文中所介绍的各种技巧。同时,注意观察代码注释,它们对于理解复杂逻辑常有帮助。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值