区间摩尔投票
摩尔投票
寻找绝对众数
给定一个长度为 nnn 的数组 aaa,已知数组 aaa 内有一种数出现次数比其他数出现次数之和还要大,请求出这个数。
解
假如存在一个数字,出现次数比其他所有的数出现次数之和还要大。那么我们可以不断地选择 两个不同的数 并将这两个数删去,直到数组中只剩下一种数。
此时剩下的数就是答案。
这是因为对于真正的答案 xxx,即使考虑最坏的情况,即每次选择一个 xxx 和非 xxx 的数,由于 xxx 的出现次数大于其他所有数的出现次数之和,故最终剩下的仍是 xxx 。
可以用两个变量 num
、cnt
分别记录 当前最有可能成为答案的数 以及 这个数目前的优势。
不妨假设 num
、cnt
表示的是 1∼i−11\sim i-11∼i−1 内的信息,那么要转移到 1∼i1\sim i1∼i,则需要
- 判断是否
num
与a[i]
相同,若相同则cnt
增加 111,表示优势增加 111,反正cnt
减少 111 表示优势减少 111。 - 若
cnt
是负数,说明num
相对于其他数而言不再具备优势,此时我们将num
换成新加进来的数a[i]
,并令cnt
为 111。
根据上面的结论,不难得出 局部最优最终会发展为全局最优。
#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-1k−1 个,可以参考摩尔投票,每次选 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-1k−1 个数的出现次数大于 nk\frac{n}{k}kn,所以这 xxx 种数至多减少 nk\frac{n}{k}kn,也就是最少会剩下 111。
我们可以维护一个大小不超过 k−1k-1k−1 的集合 AAA,集合 AAA 内的每个元素是一组 (num, cnt)
,表示当前数 num
目前的优势 cnt
。
不妨假设集合 AAA 内的元素表示的是 1∼i−11\sim i-11∼i−1 内最有可能成为答案的数的集合,那么要转移到 1∼i1\sim i1∼i,则
- 如果
a[i]
是集合 AAA 内的元素,则将其cnt
增加 111。 - 如果
a[i]
不是集合 AAA 内的元素,但集合 AAA 不满 k−1k-1k−1 个元素,则将其加入至 AAA 内,创建(a[i], 1)
。 - 如果
a[i]
不是集合 AAA 内的元素,但集合 AAA 已满,则将 AAA 内的所有元素的优势减一,若有元素的优势为 000,则将其从 AAA 内删去。
最终集合 AAA 内剩下来的元素均可能作为答案,所以我们要对它们进行最后一次检查,即遍历数组检查它们的出现次数是否大于 nk\frac{n}{k}kn。
这样我们就不需要去管其他的非候选数的出现次数,需要被专门记录的数从原来的 nnn 变成了 k−1k-1k−1。
#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_2cnt1−cnt2。
求区间[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}kr−l+1 的数,如果你没有掌握 全局出现次数大于 nk\frac{n}{k}kn 的数、摩尔投票的结合性,不要看下面内容。
如何求解求区间 [l,r][l, r][l,r] 出现次数大于 r−l+1k\frac{r-l+1}{k}kr−l+1 的所有数?
缩减目标范围
区间 [l,r][l, r][l,r] 出现次数大于 r−l+1k\frac{r-l+1}{k}kr−l+1 的数不会超过 k−1k-1k−1 个。
故我们可以用摩尔投票来忽略那些不可能成为候选数的数字进而减少时间复杂度。
因为一般 kkk 都不会给很大,所以就变成了对区间的 k−1k-1k−1 个数字进行检查,检查它们出现次数是否大于 nk\frac{n}{k}kn。
因为摩尔投票具有结合性,故我们可以 预处理若干个区间信息(当前区间候选数及其优势),然后对于每次要查询的区间,用 若干个区间信息拼凑成目标区间。
根据上面所说的需求,我们可以使用 线段树 来维护,线段树的每个结点都表示一个区间,结点信息应该是若干个候选数及其优势,也就是若干个二元组。
当要进行合并区间的时候,我们先将所有的候选数及其优势合并到一个集合,如果候选数不超过 k−1k - 1k−1 个,那么父亲结点所代表区间的候选数集合就是当前这个集合,直接上传即可。
如果候选数超过了 k−1k-1k−1 个,那么选择优势最小的候选数删去,并将其他的候选数的优势减少同等数值,直到候选数不超过 k−1k-1k−1 个。
我们维护一个 vector<PII> tmp
,把左右儿子的候选放入里面,再维护答案 vector<PII> candidates
数组,遍历 tmp
内所有候选元素,对于当前候选元素 candidate, val
- 如果它没在
candidates
内出现,那么直接加入candidates
数组内。 - 如果它在
candidates
内出现,那么把val
值更新一下。
当 candidates
内的元素超过 k−1k-1k−1 个,我们就遍历找到 val
值最小的元素,将其删去,然后再把其他元素的优势都减去 val
,如果优势为 000 则也需要被删去。
值得注意的是,由于 kkk 较小,所以我们尽量用暴力遍历和 vector
来代替其他的带 log
的数据结构。
检查候选目标
当我们从线段树中获得了目标区间的候选数集合时,仍需要检查这些候选数的合规性,即出现次数大于 r−l+12\frac{r-l+1}{2}2r−l+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();
}