洛谷 P3901:数列找不同 ← 基础莫队算法

​​【题目来源】
https://www.luogu.com.cn/problem/P3901

【题目描述】
现有数列 A1,A2,…,AN,Q 个询问 (Li,Ri),询问 A_Li,A_{Li+1},…,A_Ri 是否互不相同。

【输入格式】
第一行,两个整数 N,Q。
第二行,N 个整数 A1,A2,…,AN。
接下来 Q 行,每行两个整数 Li,Ri。

【输出格式】
对每个询问输出一行,Yes 或 No。

【输入样例】
4 2
1 2 3 2
1 3
2 4​​​​​​​

【输出样例】
Yes
No​​​​​​​

【数据范围】
对于 50% 的数据,N,Q≤10^3。
对于 100% 的数据,1≤N,Q≤10^5,1≤Ai≤N,1≤Li≤Ri≤N。

【算法分析】
● 基础莫队算法(Mo's Algorithm)是一种经分块排序优化的增量暴力算法。它不依赖线段树、树状数组等高级数据结构,而是将分块思想与暴力枚举巧妙融合,填补了传统暴力算法在处理大规模离线区间统计问题时的效率空白。
显然,基础莫队算法是一种暴力算法,但它不是 O(n²) 的原始暴力,而是用分块排序把原始暴力摊成 O(n√n) 的暴力,即所谓“优雅的暴力”。其绝大部分时间开销,来源于指针移动所触发的单点增删与答案更新操作。

● 基础莫队算法中的「指针移动」,就是维护当前活跃区间 [L, R],按离线排序后的询问顺序,每次将 L、R 分别向目标询问的 le、ri 靠拢。移动过程中:
(1)R<ri 时,右指针右移(R++),再加入新右端元素 a[R];
(2)L>le 时,左指针左移(L--),再加入新左端元素 a[L];
(3)R>ri 时,右指针左移,删除原右端元素 a[R],然后 R--。
(4)L<le 时,左指针右移,删除原左端元素 a[L],然后 L++;
每步操作均以 O(1) 增量公式更新答案,直至当前区间与目标查询区间重合。

● 基础莫队算法的区间迁移与增量维护机制
基础莫队算法维护的两个指针 L、R,代表当前已维护的活跃区间 [L, R]。在处理经过分块排序的查询序列时:
(1)对于每条查询 [le, ri],算法不会对每个查询独立重新计算,而是通过(L←L±1、R←R±1)不断移动左右指针,将当前区间逐步调整为目标区间 [le, ri],利用相邻查询的重叠部分避免重复计算。
(2)每移动一步指针,只执行一次单点增/删操作:将一个新元素加入区间,或从区间移除一个旧元素,并同步更新当前答案。加入和删除互为逆运算,这是莫队能够高效维护答案的关键。
(3)指针移动习惯上遵循先扩后缩的稳健原则:优先移动右指针 R 向右扩展,再移动左指针 L 调整左边界,避免指针交错导致区间无效。

while(ri<q[i].ri) add(++ri);
while(le>q[i].le) add(--le);
while(ri>q[i].ri) del(ri--);
while(le<q[i].le) del(le++);

● 基础莫队算法与分块的联系
基础莫队算法里的分块是调度策略,不是数据存储结构。基础莫队算法是‌基于分块思想‌构建的离线区间查询优化算法,分块为其提供‌排序依据与复杂度保障‌,两者关系可概括为"‌基础莫队算法=离线处理+分块排序+暴力转移‌"。
(1)离线处理:预先载入所有查询,不支持在线即时回答。
(2)分块排序:将原序列按 √n 大小分块,对查询进行多级排序(多级排序:以查询左端点所属块编号作为第一关键字,同一块内部以区间右端点作为第二关键字排序)。
(3)暴力转移:移动左右指针、单点增删元素、增量更新区间答案的过程。

● 奇偶性排序
基础莫队算法的核心思想是借助分块策略重排查询,规范指针移动次序,把总移动代价限制在 O(n√n)。朴素分块排序难以规避跨块时指针长途折返,奇偶排序是重要的常数优化技巧,用以减少这类无效移动。具体策略如下:
(1)首先,将长度为 n 的序列分成 sqrt(n) 个块;
(2)然后,将所有询问按左端点 L 所在块的编号为第一关键字排序。对于左端点位于同一块内的询问,采用奇偶性排序决定右端点 R 的次序。即,若该块编号为奇数,则右端点 R 从小到大排序;若该块编号为偶数,则右端点 R 从大到小排序

● 为什么不同块内无需奇偶优化?
(1)基础莫队算法的核心逻辑是“先按块定序,再按右端点调序”。不同块之间的排序仅依赖左端点所在块的编号这一关键字,不引入任何翻转策略。
(2)在莫队算法的标准实现中,奇偶优化(或称翻转排序)的作用域严格限定在同一块内,作为第二关键字的生成规则:奇数块右端点升序,偶数块右端点降序。跨块层级不应也不能应用奇偶优化。

● 基础莫队算法的核心几何模型
(1)点映射:每个询问区间 [L,R] 对应平面直角坐标系中一点(L,R),且满足 L≤R(点分布在对角线上方区域)。
(2)代价定义:从当前区间 [L1,R1] 转移到目标区间 [L2,R2] 的移动代价等于两点间的曼哈顿距离:|L1-L2|+|R1-R2|,即指针 L 和 R 移动的总步数。
(3)问题本质:莫队算法的几何解释是将每个区间询问 [L,R] 映射为二维平面上的点 (x,y)=(L,R),回答所有询问等价于在点集之间移动指针,移动开销等于两点间曼哈顿距离。想要得到全局最小移动总代价是 NP 难问题,故莫队使用分块贪心排序。

● 分块排序的几何意义
(一)空间划分‌:将横轴 L 按块大小 B≈sqrt(n) 划分成若干垂直条带(方格列),同一条带内的点 L 坐标相近。
(二)遍历策略
(1)先按 L 所在块编号升序遍历(从左向右扫描条带);
(2)同一块内按 R 升序排列(在条带内自下而上单调移动);
(3)结合奇偶优化:偶数块 R 升序、奇数块 R 降序,形成“蛇形”扫描,减少块间切换时 R 的回跳距离。

【算法代码】

#include <bits/stdc++.h>
using namespace std;

typedef long long LL;
const int N=1e5+5;
int a[N],cnt[N];
bool ans[N];
LL cur;
int block,n,m;

struct Node {
    int le,ri,idx;
} q[N];

bool cmp(Node a,Node b) {
    if(a.le/block != b.le/block) {
        return a.le<b.le;
    }
    //odd-even optimization
    if(a.le/block & 1) return a.ri>b.ri;
    else return a.ri<b.ri;
}

void add(int x) {
    int val=a[x];
    cur+=2*cnt[val]+1;
    cnt[val]++;
}

void del(int x) {
    int val=a[x];
    cnt[val]--;
    cur-=2*cnt[val]+1;
}

int main() {
    ios::sync_with_stdio(0);
    cin.tie(0);

    cin>>n>>m;
    for(int i=1; i<=n; i++) {
        cin>>a[i];
    }

    block=sqrt(n);
    for(int i=0; i<m; i++) {
        cin>>q[i].le>>q[i].ri;
        q[i].idx=i;
    }
    sort(q,q+m,cmp);

    int le=1,ri=0;
    for(int i=0; i<m; i++) {
        while(ri<q[i].ri) add(++ri);
        while(le>q[i].le) add(--le);
        while(ri>q[i].ri) del(ri--);
        while(le<q[i].le) del(le++);

        int len=q[i].ri-q[i].le+1;
        ans[q[i].idx]=(cur==len);
    }

    for(int i=0; i<m; i++) {
        if(ans[i]) cout<<"Yes\n";
        else cout<<"No\n";
    }

    return 0;
}

/*
in:
4 2
1 2 3 2
1 3
2 4

out:
Yes
No
*/



【参考文献】
https://blog.csdn.net/hnjzsyjyj/article/details/163114366
https://blog.csdn.net/hnjzsyjyj/article/details/163143122
https://blog.csdn.net/hnjzsyjyj/article/details/138976338
https://blog.csdn.net/Tudou_Pika/article/details/129225941

posted @ 2026-07-26 08:05  Triwa  阅读(5)  评论(0)    收藏  举报