摩尔投票(绝对众数)

绝对众数:数组内出现次数大于 \(\lceil \cfrac{n}{2} \rceil\) 的数。

求绝对众数的方法:
暴力做法 \(O(n \log n)\) 排序并枚举左端点。

摩尔投票:\(O(n)\) 求出。

摩尔投票

image

丢个模板。


            int now = -1; int cnt = 0;
            f(j, 1, n) {
                if(x[j] != now) cnt--;
                else cnt++;
                if(cnt < 0) {
                    now = x[j]; cntx = 1;
                }
            }
            int ccnt = 0;
            f(j, 1, n) {
                if(x[j] == now) ccnt++;
            }
            if (ccnt >= len){
                cout << cnt << endl; 
            }   

注意,序列存在区间众数时,一定是 \(cnt\)。序列不存在区间众数时,\(cnt\) 随机。所以还要再扫一遍。

拓展到 n / k

https://leetcode.cn/problems/majority-element-ii/

可以证明,出现次数超过 \(n/k\) 的数最多只有 \(k - 1\) 个。否则必然违背「数总共只有 \(n\) 个」或者「当前统计的是出现次数超过 \(n / k\) 的数」的前提条件。

当明确了符合要求的数的数量之后,我们可以使用有限变量来代表这 \(k−1\) 个候选数及其出现次数。

然后使用「摩尔投票」的标准做法,在遍历数组时同时 check 这 \(k - 1\) 个数,假设当前遍历到的元素为 \(x\)

  • 如果 \(x\) 本身是候选者的话,则对其出现次数加一;
  • 如果 \(x\) 本身不是候选者,检查是否有候选者的出现次数为 \(0\)
    若有,则让 \(x\) 代替其成为候选者,并记录出现次数为 \(1\)
    若无,则让所有候选者的出现次数减一。
    当处理完整个数组后,这 \(k - 1\) 个数可能会被填满,但不一定都是符合出现次数超过 \(n / k\) 要求的。

需要进行二次遍历,来确定候选者是否符合要求,将符合要求的数加到答案。

上述做法正确性的关键是:若存在出现次数超过 \(n / k\) 的数,最后必然会成为这 \(k - 1\) 个候选者之一。

我们可以通过「反证法」来进行证明:若出现次数超过 \(n / k\) 的数 \(x\) 最终没有成为候选者。

有两种可能会导致这个结果:

数值 \(x\) 从来没成为过候选者:

如果 \(x\) 从来没成为过候选者,那么在遍历 \(x\) 的过程中,必然有 \(k - 1\) 个候选者被减了超过 \(n / k\) 次,假设当前 \(x\) 出现次数为 \(C\),已知 \(C>n/k\),此时总个数为

\((k - 1) * C + C = C * k\)

再根据 \(C > n / k\),可知 \(C * k > n\),而我们总共就只有 \(n\) 个数,因此该情况恒不成立。

数值 \(x\) 成为过候选者,但被逐出替换了:

同理,被逐出替换,说明发生了对 \(x\) 出现次数减一的动作(减到 \(0\)),每次的减一操作,意味着有其余的 \(k - 2\) 个候选者的出现次数也发生了减一动作,加上本身被遍历到的当前数 \(num[i]\),共有 \(k - 1\) 个数字的和 \(x\) 被一同统计。
因此,根据我们摩尔投票的处理过程,如果 \(x\) 成为过候选者,并被逐出替换,那么同样能够推导出我们存在超过 \(n\) 个数。

综上,如果存在出现次数超过 \(n / k\) 的数,其必然会成为 \(k−1\) 个候选者之一。

这个算法的时间复杂度为 \(O(n/k)\)。同时暴力的 \(n \log n\) 保持不变,不失为一种可能的做法。

放在线段树内维护

需要指出的是,摩尔投票可以表示为一个二元组,是可以 \(O(1)\) 合并的信息,并且具有结合律。可以在线段树内维护。

更深刻的用法和性质

考虑区间众数的两个求法。

  1. 不确定候选众数。可以摩尔投票,得出最大数并且 \(cnt\) 确定 \(> \cfrac{len}{2}\),发现它是绝对众数。这个方法不可差分

  2. 确定候选众数 \(v\)。每个数,如果是 \(v\) 看成 \(1\),否则看成 \(-1\)。那么任意一个区间有绝对众数 \(v\) 当且仅当区间和 \(> 0\)。这个可以差分为若干个前缀和

对于第二种,我们有性质:任何一个区间只有一个绝对众数,所以如果对所有候选众数做一遍,那么得到的所有有众数的区间不重复。

还有两个深刻的性质:

  1. 区间 \([l, r]\) 有绝对众数 \(v\),那么其分裂成任意两个区间 \([l, mid]\) 以及 \([mid + 1, r]\),这两个区间满足一定存在其中一个有绝对众数 \(v\)

  2. \([l, l], [l, l + 1], ..., [l, r]\) 这些区间,最多有 \(\log_2(r - l + 1)\) 个不同的众数。
    因为某个数要做绝对众数,一定比之前的所有数都多。于是出现次数 \(1, 2, 4, 8, ...\)

这两个性质组合起来可以有如下性质:

跨过 \(mid\) 的,在 \([l, r]\) 内的所有区间的绝对众数最多有 \(\log\) 种。这正是 cdq 分治的标准处理形式!

具体地,考虑 \([mid, mid], [mid, mid + 1 ], ..., [mid, r]\)\([mid, mid], [mid - 1, mid], ..., [l, mid]\) 中所有出现过的绝对众数,就是这些区间可能有的所有绝对众数。

然后你要批量处理这些区间的信息,可以这样:对于每一个可能的绝对众数,做第 2 种判定方式(前缀和),得到 \(s_l, ..., s_r\)。然后分成两半 \(s_l, ..., s_{mid}\)\(s_{mid + 1}, ..., s_r\),排序然后双指针归并,\([l, r]\) 有绝对众数 \(v\) 当且仅当 \(\mathbf{s_r > s_{l - 1}}\)

这样处理起来,时间复杂度:

\[T(n) = 2 T(\cfrac{n}{2}) + O(n \log^2 n) \]

是三个 \(\log\) 的,但是卡的非常不满,跑起来像一个 \(\log\) 一样(笑,排序我可以改成桶,然后绝对众数卡不满这样就像 \(1 \log\) 了)

5e5 跑了 600ms(arc159_f)。

总结一下,这个方法帮助我们找到和处理所有含有绝对众数 \(v\) 的区间的方式,是很万能的,但是时间复杂度稍微难看点。

考虑最后一个过程,是给定一些 \(s\),查询 \(s_l <s_r\) 的问题。这是一维偏序,值域是 \(O(len)\) 的,你可以认为 \(r\) 是查询,查询 \(<x\) 的数有几个。可以 \(O(len)\) 预处理答案,对于每一个 \(r\) 都是询问一下的事情。这样直接降到了 \(O(n\log^2 )\)

posted @ 2022-10-10 22:02  OIer某罗  阅读(361)  评论(0编辑  收藏  举报