洛谷 P1494:[国家集训队] 小 Z 的袜子 ← 基础莫队算法

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

【题目描述】
作为一个生活散漫的人,小 Z 每天早上都要耗费很久从一堆五颜六色的袜子中找出一双来穿。终于有一天,小 Z 再也无法忍受这恼人的找袜子过程,于是他决定听天由命……
具体来说,小 Z 把这 N 只袜子从 1 到 N 编号,然后从编号 L 到 R 的袜子中随机选出两只来穿。尽管小 Z 并不在意两只袜子是不是完整的一双,他却很在意袜子的颜色,毕竟穿两只不同色的袜子会很尴尬。
你的任务便是告诉小 Z,他有多大的概率抽到两只颜色相同的袜子。当然,小 Z 希望这个概率尽量高,所以他可能会询问多个 (L,R) 以方便自己选择。
然而数据中有 L=R 的情况,请特判这种情况,输出0/1。

【输入格式】
输入文件第一行包含两个正整数 N 和 M。N 为袜子的数量,M 为小 Z 所提的询问的数量。接下来一行包含 N 个正整数 Ci,其中 Ci 表示第 i 只袜子的颜色,相同的颜色用相同的数字表示。再接下来 M 行,每行两个正整数 L,R 表示一个询问。​​​​​​​

【输出格式】
包含 M 行,对于每个询问在一行中输出分数 A/B 表示从该询问的区间 [L,R] 中随机抽出两只袜子颜色相同的概率。若该概率为 0 则输出 0/1,否则输出的 A/B 必须为最简分数。(详见样例)​​​​​​​

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

【输出样例】
2/5
0/1
1/1
4/15

【数据范围】
30% 的数据中,N,M≤5000;
60% 的数据中,N,M≤25000;
100% 的数据中,N,M≤50000,1≤L≤R≤N,Ci≤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 的回跳距离。

● 本题核心数学原理
题目要求:从区间 [L,R] 中随机抽取两只袜子,颜色相同的概率。
(1)组合数表示
区间长度为 len=R-L+1,从 len 只袜子中抽 2 只的总方案数为:C(len,2)=len×(len-1)/2
设区间内颜色 i 出现了 ci 次,则抽到两只颜色 i 的方案数为:C(ci,2)=ci×(ci-1)/2    ​
(2)概率公式
所有颜色相同的方案数之和除以总方案数:P=∑C(ci,2)/C(len,2)=∑ci(ci-1)/[len(len-1)]
(3)化简分子
∑ci(ci-1)=∑(ci*ci-ci)=∑ci*ci-∑ci
注意 ∑ci=len(区间内所有颜色的出现次数之和等于区间长度),所以:∑ci(ci-1)=∑ci*ci-len    ​
(4)最终公式
P=(∑ci*ci-len)/[len×(len−1)]

● 本题采用莫队算法如何维护 ∑ci*ci
(1)核心思想
莫队算法通过移动左右指针来维护当前区间,每次移动一个位置时,更新 ∑ci*ci 的值。
(2)加入一个元素
当在位置 x 加入一个颜色值为 a[x] 的元素时:

void add(int x) {
    sum-=cnt[a[x]]*cnt[a[x]];
    cnt[a[x]]++;
    sum+=cnt[a[x]]*cnt[a[x]];
}

加入前:cnt[a[x]] 为该颜色当前出现次数,加入后:cnt[a[x]]+1。
∑ci*ci 的变化量:Δ=(cnt[a[x]]+1)^2-cnt[a[x]]^2=2⋅cnt[a[x]]+1
(3)删除一个元素
当删除位置 x 的一个颜色值为 a[x] 的元素时:

void del(int x) {
    sum-=cnt[a[x]]*cnt[a[x]];
    cnt[a[x]]--;
    sum+=cnt[a[x]]*cnt[a[x]];
}

删除前:cnt[a[x]]为该颜色当前出现次数,删除后:cnt[a[x]]-1。
∑ci*ci 的变化量:Δ=cnt[a[x]]^2-(cnt[a[x]]-1)^2=2⋅cnt[a[x]]-1

【算法代码】

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

typedef long long LL;
const int N=5e4+5;
LL a[N],cnt[N];
LL up[N],down[N];
LL sum;
int n,m,block;

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

LL gcd(LL a,LL b) {
    if(b==0) return a;
    return gcd(b,a%b);
}

bool cmp(Node a,Node b) {
    if(a.le/block!=b.le/block) {
        return a.le<b.le;
    }
    return a.ri<b.ri;
}

void add(int x) {
    sum-=cnt[a[x]]*cnt[a[x]];
    cnt[a[x]]++;
    sum+=cnt[a[x]]*cnt[a[x]];
}

void del(int x) {
    sum-=cnt[a[x]]*cnt[a[x]];
    cnt[a[x]]--;
    sum+=cnt[a[x]]*cnt[a[x]];
}

int main() {
    ios::sync_with_stdio(false);
    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++);

        LL len=q[i].ri-q[i].le+1;
        LL u=sum-len; //u-numerator
        LL d=len*(len-1); //d-denominator

        if(u==0 || len==1) {
            up[q[i].idx]=0;
            down[q[i].idx]=1;
        } else {
            LL g=gcd(u,d);
            up[q[i].idx]=u/g;
            down[q[i].idx]=d/g;
        }
    }

    for(int i=0; i<m; i++) {
        cout<<up[i]<<"/"<<down[i]<<"\n";
    }

    return 0;
}

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

out:
2/5
0/1
1/1
4/15
*/



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

 

posted @ 2026-07-24 10:57  Triwa  阅读(7)  评论(0)    收藏  举报