摩尔投票的板子题。
为什么会记录这么简单的题捏(因为今年 NOI 考到了这个知识点)。
绝对众数
众数的定义是:在一个数列里出现次数最多的数。
绝对众数的定义在于,这个数出现的次数必须严格大于序列的一半。
举几个例子:
$A=\{0,1,2,3,3,3,3,3,3,3,3,4\}$ 中,绝对众数为 $3$ ;
$B=\{0,1,2,3,4,5,6,7,8,9\}$ 中,不存在绝对众数;
$C=\{1,1,1,1,1,2,2,2,2,2\}$ 中,同样不存在绝对众数。
题意
给定一个序列,求出该序列的绝对众数。
时间复杂度 $\mathcal O(n)$ ,空间复杂度 $\mathcal O(1)$ ,保证有解。
题解
有一种方法叫摩尔投票法,这个方法可以在时间复杂度 $\mathcal O(n)$ ,空间复杂度 $\mathcal O(1)$ 内解决保证有解的绝对众数问题,并在时空复杂度 $\mathcal O(n)$ 内解决不保证有解的绝对众数问题。
思路在于:同增异减。也称作对拼消耗或者相互攻击。
记录一个二元组 $(cnt,val)$ ,表示在当前 $1\sim i$ 中,投给 $val$ 的票数还剩下 $cnt$ 票。
我们在小的时候,可能都有一种解决追及问题的思路:
假如说一辆车甲的速度为 $v_1$ ,乙的速度为 $v_2$ ,且 $v_1>v_2$ ,那么,我们可以假设把两辆车的速度同时减去 $v_2$ ,那么,我们可以视作是乙车是不动的,甲车以一个很慢的速度向前行驶,然后就能轻松理解追及时间为 $\frac{d}{v_1-v_2}$ 了。
那么同样的,如果甲有 $x_1$ 票,乙有 $x_2$ 票。我们也可视作是没有人投给乙,而甲只有 $x_1-x_2$ 票,这就是摩尔投票算法的精髓所在。
摩尔投票算法的流程:
- 遍历一遍数列;
- 当遍历点为 $i$ 时,如果 $x_i\neq val$ ,则 $cnt$ 减 $1$ ;
- 否则 $cnt$ 加 $1$ 。
如果在某一个位置 $cnt=0$ 了,则更新 $val$ 为 $x_i$ 。
结束,需要做的就是遍历一遍数列,时间复杂度 $\mathcal O(n)$ ,空间复杂度仅仅需要二元组 $(cnt,val)$ ,为 $\mathcal O(1)$ 。
然后你就会马上把这个算法 $\text{hack}$ 掉。
$Hack=\{3,3,3,2,2,2,1\}$ ,最后的答案是 $1$ ,那就寄寄了。
然后你会发现,这样的数据还有很多很多,但都有一个共同点,那就是无解的情况。
上述过程的确无法处理无解的情况,所以我们需要特判一下:
- 在值域较小的情况下,可以开值域哈希表来存储 $Hash[val]$ 是否大于 $\frac{n}{2}$ 从而判断无解。
- 在无法使用哈希的情况下,可以选择再次遍历序列,然后统计 $val$ 出现的次数。
这样的话,空间复杂度就会达到 $\mathcal O(n)$ 。
那么我们深度理解摩尔投票,最后得到的 $val$ 只是一个备选答案,而非序列的绝对众数,因为无法判断是否有解。
这样为什么是正确的呢?如果一个序列存在绝对众数 $d$ ,那么 $d$ 出现的次数一定大于了一半。我们假设只有 $d,!d$ 两个参赛选手,因为满足了 $cnt[d]\geq\frac{n}{2}$ ,也就满足了 $cnt[d]>cnt[!d]$ ,所以无论如何,$d$ 的票数都是最高的,即使在前半段的时候可能会被一些其他数字代替,但最后 $val$ 一定会被换回 $d$ 。
当然,这道题还有一个 $\mathcal O(n\log n)$ 的不算智慧的智慧法,直接将整个序列排序,则 $x_{\frac{n}{2}}$ 就是该序列的绝对众数。
此题保证有解,且空间限制为 $\text{5.00MB}$ ,所以不开数组。
AC Code
1 | int N,x,cnt,val; |
后话
关于摩尔投票,它满足结合律。
就比如说两个序列 $(cnt_1,val_1)$ ,$(cnt_2,val_2)$ ,那么两个序列合并过后得到的绝对众数就是 $cnt$ 值较大的那个。
然后又可以被轻松 $\text{Hack}$ 掉。
$A_1=\{3,3,3,2,2,2,3\},val=3,cnt=1$
$A_2=\{1,1,1\},val=1,cnt=3$
这样得到的 $val=1$ ,然后我们会发现,$A_1,A_2$ 合并过后,根本就没有绝对众数。
然后就会发现,这个 $\text{Hack}$ 的解决方案和上述一致,绝对众数一定是摩尔投票的结果,但是摩尔投票的结果不一定是绝对众数。
所以需要特判。