当前位置: 首页 > news >正文

区间摩尔投票 - 教程

摩尔投票

寻找绝对众数

给定一个长度为 nnn 的数组 aaa,已知数组 aaa 内有一种数出现次数比其他数出现次数之和还要大,请求出这个数。

假如存在一个数字,出现次数比其他所有的数出现次数之和还要大。那么我们可以不断地选择 两个不同的数 并将这两个数删去,直到数组中只剩下一种数。

此时剩下的数就是答案

这是因为对于真正的答案 xxx,即使考虑最坏的情况,即每次选择一个 xxx 和非 xxx 的数,由于 xxx 的出现次数大于其他所有数的出现次数之和,故最终剩下的仍是 xxx

可以用两个变量 numcnt 分别记录 当前最有可能成为答案的数 以及 这个数目前的优势

不妨假设 numcnt 表示的是 1∼i−11\sim i-11i1 内的信息,那么要转移到 1∼i1\sim i1i,则需要

  • 判断是否 numa[i] 相同,若相同则 cnt 增加 111,表示优势增加 111,反正 cnt 减少 111 表示优势减少 111
  • cnt 是负数,说明 num 相对于其他数而言不再具备优势,此时我们将 num 换成新加进来的数 a[i],并令 cnt111

根据上面的结论,不难得出 局部最优最终会发展为全局最优

#include <bits/stdc++.h>using namespace std;//#pragma GCC optimize(2)#define int long long#define endl '\n'#define PII pair<int,int>#define INF 1e18void slove () {int n;cin >> n;int num = INF, cnt = 0;for (int i = 1; i <= n; i++) {int a;cin >> a;if (num == a) cnt ++;else {if (cnt == 0) {num = a;cnt = 1;} else {cnt --;}}}cout << num << endl;}signed main () {ios::sync_with_stdio(0),cin.tie(0),cout.tie(0);slove();}

寻找出现次数大于 n/k 的所有数

给定一个长度为 nnn 的数组 aaa,请找出 aaa 内出现次数大于 nk\frac{n}{k}kn 的所有数,kkk 至多不超过 101010

一共 nnn 个数,出现次数大于 nk\frac{n}{k}kn 的数显然不超过 k−1k-1k1 个,可以参考摩尔投票,每次选 kkk 不同的个数删去,直到数组内只剩下少于 kkk 种数,则剩下的数 可能是 我们要找的出现次数大于 nk\frac{n}{k}kn 的数。

这里要注意我的用词,可能是,因为我们只能证明出现次数大于 nk\frac{n}{k}kn 的数必然会留下来,但是不能推出留下来的就一定是出现次数大于 nk\frac{n}{k}kn 的数。

证明很简单,假设存在 xxx 种出现次数大于 nk\frac{n}{k}kn 的数,显然除此之外的所有数的出现次数均不大于 nk\frac{n}{k}kn,而最坏的情况是我们令这 xxx 种数都包含在每次选择的 kkk 种数中,由于不超过 k−1k-1k1 个数的出现次数大于 nk\frac{n}{k}kn,所以这 xxx 种数至多减少 nk\frac{n}{k}kn,也就是最少会剩下 111

我们可以维护一个大小不超过 k−1k-1k1 的集合 AAA,集合 AAA 内的每个元素是一组 (num, cnt),表示当前数 num 目前的优势 cnt

不妨假设集合 AAA 内的元素表示的是 1∼i−11\sim i-11i1 内最有可能成为答案的数的集合,那么要转移到 1∼i1\sim i1i,则

最终集合 AAA 内剩下来的元素均可能作为答案,所以我们要对它们进行最后一次检查,即遍历数组检查它们的出现次数是否大于 nk\frac{n}{k}kn

这样我们就不需要去管其他的非候选数的出现次数,需要被专门记录的数从原来的 nnn 变成了 k−1k-1k1

#include <bits/stdc++.h>using namespace std;//#pragma GCC optimize(2)#define int long long#define endl '\n'#define PII pair<int,int>#define INF 1e18void slove () {int n, k;cin >> n >> k;map <int, int> mp;for (int i = 1; i <= n; i++) {int x;cin >> x;if (mp.count(x)) mp[x] ++;else {if (mp.size() < k - 1) {mp[x] = 1;} else {vector <int> del;for (auto &j : mp) {j.second --;if (j.second == 0) del.push_back(j.first);}for (auto j : del) mp.erase(j);}}}}signed main () {ios::sync_with_stdio(0),cin.tie(0),cout.tie(0);slove();}

摩尔投票的结合性

在摩尔投票与线段树结合起来之前,先证明一下 摩尔投票的结合性

假设我们要求区间 [1,n][1,n][1,n] 内的出现次数大于 n2\frac{n}{2}2n 的数。

那么我是否可以先将 [1,n][1,n][1,n] 分成若干个更小的互不重叠的区间,分别计算答案后结合起来变成最终答案呢?

显然是可以的,因为我们可以把 拆分为若干个区间 看做对这些区间内的数进行“每次选择两个不同的数删去”的操作,那么区间剩下来的一个数,就表示这个区间内最有可能成为答案的数,即 候选数

由摩尔投票可知,若存在出现次数超过 n/2n/2n/2 的数字,则不管按照什么顺序进行操作,最终得到的答案必然是唯一的,所以这若干个互不重叠的小区间合并后得到的候选数就一定是最终的答案。

所以我们可以得出一个结论,摩尔投票是具有结合性的

具体怎么结合呢,下面给出一个简单的例子:

[1,n/2][1,n/2][1,n/2] 所得到的候选数是 x1x_1x1,其优势是 cnt1cnt_1cnt1[n/2+1,n][n/2+1,n][n/2+1,n] 所得到的候选数是 x2x_2x2,其优势是 cnt2cnt_2cnt2

  • x1=x2x_1=x_2x1=x2,则最终的候选数是 x1x_1x1
  • x1≠x2x_1\neq x_2x1=x2,则最终的候选数是 cntcntcnt 值更大的。

不妨设 cnt1>cnt2cnt_1>cnt_2cnt1>cnt2,则最终 [1,n][1,n][1,n] 内的答案是 x1x_1x1,其优势是 cnt1−cnt2cnt_1-cnt_2cnt1cnt2

求区间[l,r]内出现次数大于 (r-l+1)/k 的所有数

这是摩尔投票的最终版本,从原来的求全局出现次数大于 n2\frac{n}{2}2n 的数,到求区间 [l,r][l, r][l,r] 出现次数大于 r−l+1k\frac{r-l+1}{k}krl+1 的数,如果你没有掌握 全局出现次数大于 nk\frac{n}{k}kn 的数摩尔投票的结合性,不要看下面内容。

如何求解求区间 [l,r][l, r][l,r] 出现次数大于 r−l+1k\frac{r-l+1}{k}krl+1 的所有数?

缩减目标范围

区间 [l,r][l, r][l,r] 出现次数大于 r−l+1k\frac{r-l+1}{k}krl+1 的数不会超过 k−1k-1k1 个。

故我们可以用摩尔投票来忽略那些不可能成为候选数的数字进而减少时间复杂度。

因为一般 kkk 都不会给很大,所以就变成了对区间的 k−1k-1k1 个数字进行检查,检查它们出现次数是否大于 nk\frac{n}{k}kn

因为摩尔投票具有结合性,故我们可以 预处理若干个区间信息(当前区间候选数及其优势),然后对于每次要查询的区间,用 若干个区间信息拼凑成目标区间

根据上面所说的需求,我们可以使用 线段树 来维护,线段树的每个结点都表示一个区间,结点信息应该是若干个候选数及其优势,也就是若干个二元组。

当要进行合并区间的时候,我们先将所有的候选数及其优势合并到一个集合,如果候选数不超过 k−1k - 1k1 个,那么父亲结点所代表区间的候选数集合就是当前这个集合,直接上传即可。

如果候选数超过了 k−1k-1k1 个,那么选择优势最小的候选数删去,并将其他的候选数的优势减少同等数值,直到候选数不超过 k−1k-1k1 个。

我们维护一个 vector<PII> tmp,把左右儿子的候选放入里面,再维护答案 vector<PII> candidates 数组,遍历 tmp 内所有候选元素,对于当前候选元素 candidate, val

  • 如果它没在 candidates 内出现,那么直接加入 candidates 数组内。
  • 如果它在 candidates 内出现,那么把 val 值更新一下。

candidates 内的元素超过 k−1k-1k1 个,我们就遍历找到 val 值最小的元素,将其删去,然后再把其他元素的优势都减去 val,如果优势为 000 则也需要被删去。

值得注意的是,由于 kkk 较小,所以我们尽量用暴力遍历和 vector 来代替其他的带 log 的数据结构。

检查候选目标

当我们从线段树中获得了目标区间的候选数集合时,仍需要检查这些候选数的合规性,即出现次数大于 r−l+12\frac{r-l+1}{2}2rl+1

对于一个区间 [l,r][l,r][l,r] 内的数(且这个数并不能预处理,具有一定的普遍性),如何检查其出现次数?

可以对每个数 xxx 开一个 vector,里面 xxx 的所有可能出现的位置,并将其排好序。

然后对这个 vector 进行二分有多少位置出现在 [l,r][l,r][l,r] 之间即可,由于 kkk 很小,所以我们每次二分的时间复杂度至多 O(logn)O(logn)O(logn)

而线段树的时间复杂度也是 lognlognlogn,故总时间复杂度会是 O(qlogn)O(qlogn)O(qlogn)

最终代码

例题

https://codeforces.com/contest/2149/problem/G

#include <bits/stdc++.h>using namespace std;//#pragma GCC optimize(2)#define int long long#define endl '\n'#define PII pair<int,int>#define INF 1e18struct SegmentTreeNode {vector <PII> candidates; // 候选人SegmentTreeNode() {}};struct SegmentTree {vector <SegmentTreeNode> tr;int n, k;SegmentTree(int size, int _k) : n(size), tr(size << 2), k(_k) {}SegmentTreeNode merge(SegmentTreeNode a, SegmentTreeNode b) {SegmentTreeNode now;vector<PII> tmp = a.candidates;for (auto x : b.candidates) tmp.push_back(x);// 合并所有候选for (auto [cnt, val] : tmp) {bool found = false;for (auto &it : now.candidates) {// 当前候选人在之前出现过,那么直接累加if (it.second == val) {it.first += cnt;found = true;break;}}// 如果当前if (!found) now.candidates.push_back({cnt, val});if (now.candidates.size() > k - 1) {// 抵消最小次数int mn = now.candidates[0].first;for (auto &it : now.candidates) mn = min(mn, it.first);for (auto &it : now.candidates) it.first -= mn;vector<PII> t;for (auto &it : now.candidates) if (it.first > 0) t.push_back(it);now.candidates = t;}}return now;}void pushup(int node) {tr[node] = merge(tr[node << 1], tr[node << 1 | 1]);}void build(int node, int l, int r, const vector<int>& a) {if (l == r) {tr[node].candidates.push_back({1, a[l]});return;}int mid = (l + r) >> 1;build(node << 1, l, mid, a);build(node << 1 | 1, mid + 1, r, a);pushup(node);}SegmentTreeNode query (int node, int L, int R, int l, int r) {if (l > r || l > R || r < L) return SegmentTreeNode(); // 区间非法//opt1:区间覆盖if (l <= L && r >= R) {return tr[node]; // 返回该结点信息。}int mid = (L + R) >> 1;SegmentTreeNode q1 = query (node << 1, L, mid, l, r);SegmentTreeNode q2 = query (node << 1 | 1, mid + 1, R, l, r);return merge(q1, q2); // 返回合并起来的信息,不一定是求和}};void slove () {int n, q;cin >> n >> q;SegmentTree tt(n, 3);vector <int> a(n + 1);map <int, vector<int>> mp;for (int i = 1; i <= n; i++) {cin >> a[i];mp[a[i]].push_back(i);}tt.build(1, 1, n, a);while (q--) {int l, r;cin >> l >> r;SegmentTreeNode now = tt.query(1, 1, n, l, r);sort (now.candidates.begin(), now.candidates.end(), [](PII A, PII B) {return A.second < B.second;});bool is_ok = 0;for (auto [val, candidate] : now.candidates) {int L = lower_bound(mp[candidate].begin(), mp[candidate].end(), l) - mp[candidate].begin();int R = lower_bound(mp[candidate].begin(), mp[candidate].end(), r + 1) - mp[candidate].begin();if ((R - L) > (r - l + 1)/3) {cout << candidate << ' ';is_ok = 1;}}if (is_ok == 0) cout << -1;cout << endl;}}signed main () {ios::sync_with_stdio(0),cin.tie(0),cout.tie(0);int T;cin >> T;while (T--) slove();}
http://www.hskmm.com/?act=detail&tid=37899

相关文章:

  • SpringBoot整合SpringDoc
  • GEO靠谱推荐:GEO技术开启精准农业与资源管理新纪元 - 勤懒调和者
  • 2025中国DevOps平台选型全景洞察:本土化与安全可控成关键考量
  • 增强AI股票预测分析报告 - 2025年10月24日 - 10:18:59
  • NACOS 2.4.1 数据库表详解
  • win10开始安装vs2022时闪退问题记录
  • 领取快手的3个月的 KAT-Coder-Pro V1 编程 Tokens 资源包
  • (WebSocket)心理咨询管理系统开发ing......
  • 2025 年硅砂模块实力厂家最新推荐排行榜:涵盖新型 / 第三代承插型等多类型产品,多维度解析优质企业优势
  • 1基础的UActorComponent基类实现功能模块化
  • 2025 泳池设备厂家专业解决方案与设备优势,推荐 Firsle 法思乐,全产业链服务解析
  • 2025年10月杭州模拟人开发公司对比榜:服务链路深度拆解
  • 医用制氧机哪家好?2025医用制氧机厂家权威排行榜
  • NativeMessaging通信失败问题
  • 2025年10月中国电缆品牌评价榜:十强参数与口碑全解析
  • Java反射
  • 2025年10月杭州获客教育培训公司实力榜:六维对比看清谁更适合你
  • Python 装饰器
  • 2025年10月中国电线电缆厂家推荐榜:五强性能评价
  • R-高性能编程-全-
  • oracle NVL和NVL2
  • SSH 端口转发与跳板机
  • 99.5%制氧机生产厂家盘点!2025高原制氧机厂家排行榜
  • 2025年中国国际健康营养博览会(NHNE):深度解析亚洲旗舰展的供需对接效能
  • 利用 ProxyJump 来安全访问内网主机
  • 2025年10月上海装修公司实力榜:排名与口碑全解析
  • sqlserver 字符串转成日期、日期转字符串、字符串转数字、数字转字符串
  • 2025年10月上海装修公司对比榜:十家真实评价排行
  • 2025年10月上海装修公司服务榜:口碑排行十强