csp信奥赛C++之摩尔投票算法详解

csp信奥赛C++之摩尔投票算法详解

原题说明:洛谷P2397 yyy loves Maths VI (mode)/摩尔投票

题目描述

一共有 n n n 个正整数 a i a_i ai,需要找出其中的众数。特别提醒:这个众数出现次数超过了一半。

输入格式

第一行一个整数 n n n,表示数的个数。

第二行 n n n 个正整数 a i a_i ai。

输出格式

一行一个整数,表示众数。

输入输出样例 1
输入 1
复制代码
5
2 3 3 3 3
输出 1
复制代码
3
说明/提示

【数据范围】

对于 100 % 100\% 100% 的数据, 1 ≤ n ≤ 2 × 10 6 1\le n \le 2\times 10^6 1≤n≤2×106, a i ∈ [ 1 , 2 31 ) a_i \in [1,2^{31}) ai∈[1,231)。

【内存限制】

5MB

思路分析

题目要求在 n n n 个数中找出出现次数超过一半的众数,且空间限制严格(不能使用数组存储所有数)。

本题可以使用摩尔投票算法(Boyer--Moore majority vote algorithm)实现,其核心思想是:

  • 维护一个候选众数 cand 和一个计数器 cnt,初始 cnt = 0
  • 遍历每个数:
    • cnt == 0,将当前数设为候选众数,计数器置为 1。
    • 否则,若当前数等于候选众数,计数器加 1;
    • 否则计数器减 1。
  • 由于众数出现次数超过一半,最终 cand 中保存的一定是众数。

摩尔投票算法(Boyer-Moore Majority Vote Algorithm)实现原理详解

摩尔投票算法是一种用于在线性时间常数空间 内寻找数组中出现次数超过一半的元素的算法。它由 Robert S. Boyer 和 J Strother Moore 于 1981 年提出,特别适合内存受限的场景(如本题)。

1. 算法核心思想

"配对抵消"

将数组中任意两个不同的元素视为一对"敌人",它们相互抵消(删除)。如果某个元素出现的次数超过总数的一半,那么无论怎么抵消,最后剩下的那个元素一定是该众数。

更形象的理解:

  • 想象你有一堆球,每个球上标有一个数字。
  • 你随机拿出两个球:如果数字不同,就把它们都扔掉(抵消);如果数字相同,就保留一个(实际上相当于计数增加)。
  • 重复这个过程,最后剩下的球上的数字就是出现次数超过一半的众数。
2. 算法步骤

算法维护两个变量:

  • candidate:当前候选的众数。
  • count:当前候选的"净支持数"(即当前候选比其它数多出现的次数)。

遍历数组的每个元素 x

  1. 如果 count == 0,说明之前的候选已经被完全抵消,将 x 设为新的 candidate,并将 count 置为 1。
  2. 否则,如果 x == candidate,说明当前元素与候选相同,支持度增加:count++
  3. 否则(x != candidate),说明当前元素与候选不同,产生一次抵消:count--

遍历结束后,candidate 中保存的就是可能的众数。

由于题目保证众数存在且出现次数超过一半,因此 candidate 一定是正确答案。

3. 算法举例

以输入 2 3 3 3 3 为例:

  • 初始:candidate = 0, count = 0
  • 读入 2count == 0 → 设 candidate = 2, count = 1
  • 读入 33 != 2 → 抵消,count--count = 0
  • 读入 3count == 0 → 设 candidate = 3, count = 1
  • 读入 33 == 3count++count = 2
  • 读入 33 == 3count++count = 3
  • 输出 candidate = 3,正确。
4. 为什么算法正确?

我们需要证明:如果存在一个元素出现次数超过一半,那么遍历结束后 candidate 一定是该元素。

证明思路

设数组长度为 n,众数为 m,出现次数为 k > n/2。在遍历过程中,每次遇到与 candidate 不同的元素时,count 减 1,相当于将当前 candidate 与另一个不同元素配对抵消。由于 m 出现的次数超过一半,它不可能被完全抵消(最多只能和它不同的元素一一抵消)。

更严谨地,考虑整个过程中抵消的总次数。每次抵消(count 减 1)都会消耗一个非 m 元素和一个当前候选(可能是 m 也可能不是)。但无论当前候选是谁,最终 m 的"净胜"次数至少为 k - (n - k) > 0,所以当遍历结束时,count 必定大于 0 且 candidate 就是 m

另一种数学证明:

定义"净票数"为当前候选的出现次数减去其他元素的出现次数。在遍历过程中,每次遇到相同元素,净票数 +1;遇到不同元素,净票数 -1。由于众数的出现次数超过一半,它的最终净票数一定为正,因此最后剩下的候选必为众数。

5. 算法特点
  • 时间复杂度:O(n),只需一次遍历。
  • 空间复杂度:O(1),仅用两个变量。
  • 局限性 :该算法只能用于确认存在 且出现次数超过一半的元素。如果题目没有保证,则需要在第一遍找出候选后,再进行第二遍扫描验证该候选的出现次数是否真的超过一半。本题明确保证众数存在且超过一半,因此无需验证。
6. 为什么不能直接排序或哈希?
  • 排序:时间复杂度 O(n log n),且可能需额外空间(如归并排序)或改变原数组。
  • 哈希表:空间复杂度 O(n),对于 n=2×10^6 且内存限制严格的情况可能不可行。
  • 摩尔投票算法完美平衡了时间和空间,是解决此类问题的最优方法。
7. 总结

摩尔投票算法通过巧妙的"配对抵消"策略,在只遍历一次且几乎不占用额外内存的情况下,找出了出现次数超过一半的元素。它的实现简单,逻辑清晰,是算法竞赛和面试中的经典问题。


AC代码

cpp 复制代码
#include <bits/stdc++.h>
using namespace std;

int n; // 数字个数

int main() {
    cin >> n;                // 输入个数
    int cand = 0, cnt = 0;    // cand:当前候选众数;cnt:当前候选的"净支持数"
    for (int i = 1; i <= n; i++) {
        int x;
        cin >> x;             // 读入一个数
        if (cnt == 0) {       // 若计数器为0,则当前数成为新的候选众数
            cand = x;
            cnt = 1;
        } else if (x == cand) { // 若当前数等于候选众数,支持度+1
            cnt++;
        } else {                // 否则,支持度-1
            cnt--;
        }
    }
    cout << cand << endl;      // 最终cand一定是众数(题目保证存在且超过一半)
    return 0;
}

【文末福利:一等奖秘籍汇总】(完整csp信奥赛C++学习资料):

1、csp/信奥赛C++,完整信奥赛系列课程(永久学习):

https://edu.csdn.net/lecturer/7901 点击跳转

2、CSP信奥赛C++竞赛拿奖视频课:

https://edu.csdn.net/course/detail/40437 点击跳转

3、csp信奥赛高频考点知识详解及案例实践:

CSP信奥赛C++动态规划:
https://blog.csdn.net/weixin_66461496/category_13096895.html点击跳转

CSP信奥赛C++标准模板库STL:
https://blog.csdn.net/weixin_66461496/category_13108077.html 点击跳转

信奥赛C++提高组csp-s知识详解及案例实践:
https://blog.csdn.net/weixin_66461496/category_13113932.html 点击跳转

4、csp信奥赛冲刺一等奖有效刷题题解:

CSP信奥赛C++初赛及复赛高频考点真题解析(持续更新): https://blog.csdn.net/weixin_66461496/category_12808781.html 点击跳转

信奥赛C++提高组csp-s初赛&复赛真题题解(持续更新):
https://blog.csdn.net/weixin_66461496/category_13125089.html 点击跳转

5、GESP C++考级真题题解:

GESP(C++ 一级+二级+三级)真题题解(持续更新):https://blog.csdn.net/weixin_66461496/category_12858102.html 点击跳转

GESP(C++ 四级+五级+六级)真题题解(持续更新):https://blog.csdn.net/weixin_66461496/category_12869848.html 点击跳转

GESP(C++ 七级+八级)真题题解(持续更新):
https://blog.csdn.net/weixin_66461496/category_13117178.html 点击跳转

· 文末祝福 ·

cpp 复制代码
#include<bits/stdc++.h>
using namespace std;
int main(){
	cout<<"跟着王老师一起学习信奥赛C++";
	cout<<"    成就更好的自己!       ";
	cout<<"  csp信奥赛一等奖属于你!   ";
	return 0;
}
相关推荐
Purple Coder2 小时前
基于GNN的超导材料生长方法研究算法的实现-1
算法
tod1132 小时前
C++ 核心知识点全解析(六)
c++·算法·面试经验
m0_531237172 小时前
C语言-编程实例
c语言·开发语言·数据结构
紫陌涵光2 小时前
701. 二叉搜索树中的插入操作
算法·leetcode
tankeven2 小时前
HJ100 等差数列
c++·算法
waves浪游2 小时前
库制作与原理(上)
linux·运维·服务器·开发语言·c++
ADDDDDD_Trouvaille2 小时前
2026.2.22——OJ98-100题
c++·算法
闻缺陷则喜何志丹2 小时前
【差分数组】P9166 [省选联考 2023] 火车站|普及+
数据结构·c++·洛谷·差分数组
tankeven2 小时前
HJ99 自守数
c++·算法