news 2026/10/28 12:20:14

莫队算法解决区间元素唯一性问题

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
莫队算法解决区间元素唯一性问题

1. 题目背景与需求分析

P3901 "数列找不同"是洛谷平台上的一道经典信奥练习题,主要考察选手对基础算法和数据结构的掌握程度。题目要求我们判断一个数列中是否存在重复元素,这是计算机科学中非常基础但又极其重要的问题类型。

这类问题在实际编程中有着广泛的应用场景,比如:

  • 数据库系统中检查主键唯一性
  • 网络爬虫去重处理
  • 大数据分析中的重复记录检测

题目给出的输入格式通常为:

第一行:两个整数n和q,表示数列长度和查询次数 第二行:n个整数表示数列 接下来q行:每行两个整数l和r,表示查询区间

输出要求对每个查询,判断区间[l,r]内所有元素是否互不相同,是则输出"Yes",否则输出"No"。

2. 暴力解法与复杂度分析

2.1 最直观的暴力解法

对于初学者来说,最容易想到的解法是双重循环暴力检查:

for (int i = l; i <= r; i++) { for (int j = i+1; j <= r; j++) { if (a[i] == a[j]) { return false; } } } return true;

这种解法的时间复杂度是O(q*n²),当n和q较大时(比如都达到1e5),这种解法显然会超时。

2.2 暴力解法的优化空间

我们可以通过引入哈希表来优化内层循环:

unordered_set<int> s; for (int i = l; i <= r; i++) { if (s.count(a[i])) { return false; } s.insert(a[i]); } return true;

这样时间复杂度降为O(q*n),但对于大规模数据仍然不够高效。

3. 高效解法:离线处理与莫队算法

3.1 莫队算法简介

莫队算法是一种优雅的离线处理区间查询问题的算法,特别适合解决这类"区间内元素是否唯一"的问题。其核心思想是通过合理地安排查询顺序,利用前一次查询的结果来加速当前查询的处理。

算法步骤:

  1. 对查询进行分块排序
  2. 维护一个当前区间和元素出现次数的统计
  3. 通过移动区间左右指针来处理查询

3.2 具体实现步骤

首先我们需要定义块大小和排序规则:

int block_size = sqrt(n); struct Query { int l, r, idx; bool operator<(const Query &other) const { if (l / block_size != other.l / block_size) return l < other.l; return (l / block_size) & 1 ? r < other.r : r > other.r; } };

然后实现莫队算法的核心逻辑:

vector<int> cnt(MAX_N, 0); int current_ans = 0; void add(int pos) { if (cnt[a[pos]] == 1) current_ans--; cnt[a[pos]]++; if (cnt[a[pos]] == 1) current_ans++; } void remove(int pos) { if (cnt[a[pos]] == 1) current_ans--; cnt[a[pos]]--; if (cnt[a[pos]] == 1) current_ans++; }

3.3 完整代码实现

#include <bits/stdc++.h> using namespace std; const int MAX_N = 1e5 + 5; int a[MAX_N]; int block_size; struct Query { int l, r, idx; bool operator<(const Query &other) const { if (l / block_size != other.l / block_size) return l < other.l; return (l / block_size) & 1 ? r < other.r : r > other.r; } }; vector<bool> mo_s_algorithm(vector<Query> queries) { vector<bool> answers(queries.size()); sort(queries.begin(), queries.end()); int cur_l = 0, cur_r = -1; unordered_map<int, int> freq; int distinct = 0; for (Query q : queries) { while (cur_l > q.l) { cur_l--; if (freq[a[cur_l]] == 1) distinct--; freq[a[cur_l]]++; if (freq[a[cur_l]] == 1) distinct++; } while (cur_r < q.r) { cur_r++; if (freq[a[cur_r]] == 1) distinct--; freq[a[cur_r]]++; if (freq[a[cur_r]] == 1) distinct++; } while (cur_l < q.l) { if (freq[a[cur_l]] == 1) distinct--; freq[a[cur_l]]--; if (freq[a[cur_l]] == 1) distinct++; cur_l++; } while (cur_r > q.r) { if (freq[a[cur_r]] == 1) distinct--; freq[a[cur_r]]--; if (freq[a[cur_r]] == 1) distinct++; cur_r--; } answers[q.idx] = (distinct == (q.r - q.l + 1)); } return answers; } int main() { ios::sync_with_stdio(false); cin.tie(nullptr); int n, q; cin >> n >> q; block_size = sqrt(n); for (int i = 0; i < n; i++) { cin >> a[i]; } vector<Query> queries(q); for (int i = 0; i < q; i++) { cin >> queries[i].l >> queries[i].r; queries[i].l--; queries[i].r--; queries[i].idx = i; } vector<bool> ans = mo_s_algorithm(queries); for (bool b : ans) { cout << (b ? "Yes" : "No") << "\n"; } return 0; }

4. 算法优化与性能分析

4.1 时间复杂度分析

莫队算法的时间复杂度主要取决于:

  1. 排序时间:O(q log q)
  2. 处理查询时间:O(n√n)

总时间复杂度为O(n√n + q log q),这在n和q都达到1e5时是完全可行的。

4.2 常数优化技巧

在实际编码比赛中,以下几个优化技巧可以帮助提升运行速度:

  1. 使用快读快写:
inline int read() { int x = 0, f = 1; char ch = getchar(); while (ch < '0' || ch > '9') { if (ch == '-') f = -1; ch = getchar(); } while (ch >= '0' && ch <= '9') { x = x * 10 + ch - '0'; ch = getchar(); } return x * f; }
  1. 使用数组代替unordered_map:
int freq[MAX_N] = {0};
  1. 调整块大小:
block_size = n / sqrt(q); // 根据查询次数动态调整块大小

4.3 替代方案比较

除了莫队算法,这道题还可以用以下方法解决:

  1. 线段树:维护区间内元素是否唯一的信息
  2. 前缀和+哈希:记录每个元素最后出现的位置
  3. 分块:将数列分成若干块,预处理块内信息

但综合比较,莫队算法在这类问题上实现简单且效率较高,特别适合编程竞赛场景。

5. 常见错误与调试技巧

5.1 边界条件处理

在实现莫队算法时,最容易出错的是边界条件的处理:

  1. 区间下标从0开始还是从1开始
  2. 移动指针时先移动左指针还是右指针
  3. 初始时cur_l和cur_r的设置

建议在纸上画出区间移动的示意图,确保逻辑正确。

5.2 数据范围问题

需要注意题目给出的数据范围:

  • 数列元素的大小范围
  • n和q的最大值
  • 内存限制

特别是当使用哈希表时,要注意避免超内存。

5.3 调试输出技巧

在调试时可以添加一些输出语句:

void debug_print(int l, int r) { cout << "Current [" << l << "," << r << "]: "; for (int i = l; i <= r; i++) { cout << a[i] << " "; } cout << endl; cout << "Frequency: "; for (auto &p : freq) { if (p.second > 0) { cout << p.first << ":" << p.second << " "; } } cout << endl; }

6. 扩展练习与相关题目

为了巩固对这种类型题目的理解,建议尝试以下类似题目:

  1. 洛谷P1972 [SDOI2009]HH的项链
  2. CodeForces 86D Powerful array
  3. SPOJ DQUERY - D-query
  4. 洛谷P2709 小B的询问

这些题目都可以用莫队算法解决,但各有特点,可以帮助深入理解算法的应用场景和变种。

在实际刷题过程中,我发现记录每个题目的解题思路和关键点非常重要。建议建立一个刷题笔记,记录以下内容:

  • 题目核心考察点
  • 解题思路和算法选择
  • 实现中的关键代码
  • 遇到的坑和解决方法
  • 时间复杂度和空间复杂度分析

这样系统化的刷题方式,比单纯追求数量要有效得多。对于信奥准备来说,深入理解10道经典题目,比浅尝辄止地刷100道题目更有价值。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/28 12:20:08

Linux与Windows服务器关机命令详解及运维实践

1. 服务器关机命令概述服务器作为关键基础设施&#xff0c;其关机操作绝非简单的电源按钮动作。不同操作系统平台有着截然不同的关机机制和命令体系&#xff0c;这背后涉及系统服务管理、进程调度、文件系统保护等一系列关键技术考量。以Linux为例&#xff0c;其关机流程需要经…

作者头像 李华
网站建设 2026/10/28 12:18:55

血透机维修手册拆解:4008系列板卡索引与串口调试实战

简介&#xff1a;面向血液透析设备维护工程师、临床工程技术人员的费森尤斯4008系列技术手册&#xff0c;覆盖4008 E、4008 B、4008 H、4008 S四款机型的电路设计、操作流程、数据管理与RS232串口通信设置&#xff0c;可解决设备启动、日常维护、故障诊断与校准调整中的查阅需求…

作者头像 李华
网站建设 2026/10/28 12:18:54

DAgger算法解析:模仿学习的迭代优化与实践

1. DAgger算法核心思想解析DAgger&#xff08;Dataset Aggregation&#xff09;是2011年由斯坦福大学提出的模仿学习改进算法&#xff0c;它通过迭代式数据收集和专家标注来解决传统行为克隆中的分布偏移问题。这个算法的精妙之处在于它模拟了人类学习中的"犯错-纠正-再练…

作者头像 李华
网站建设 2026/10/28 12:18:25

三种ECC技术全解读:椭圆曲线密码、内存纠错与SAP系统

ECC这个词&#xff0c;在IT圈子里撞车概率高得离谱。搞安全的看到ECC第一反应是椭圆曲线密码学&#xff08;Elliptic Curve Cryptography&#xff09;&#xff0c;搞服务器硬件的想到的是纠错内存&#xff08;Error Correcting Code&#xff09;&#xff0c;而搞企业信息化的老…

作者头像 李华
网站建设 2026/10/28 12:17:30

OpenJDK 8_322 Windows解压安装版:环境变量配置与多版本切换全攻略

简介&#xff1a;这是一份OpenJDK 8_322的Windows解压安装版资源&#xff0c;面向Java开发者和系统管理员&#xff0c;属于2022年1月发布的开源JDK 8更新版本&#xff0c;免安装向导、解压即用&#xff0c;特别适合需要快速搭建Java开发或运行环境的场景。压缩包共349个文件&am…

作者头像 李华
网站建设 2026/10/28 12:17:03

从机械臂到数字人:统一位姿表示、雅可比与MATLAB避坑

简介&#xff1a;《机器人至数字人的演变&#xff1a;数学原理与MATLAB应用》是一份面向高年级本科生、研究生及机器人研发工程师的教材型PDF&#xff0c;聚焦机器人运动学、动力学分析与数字人体建模之间的数学联系&#xff0c;并以MATLAB编程贯穿建模、优化与控制示例&#x…

作者头像 李华