Misra–Gries 算法/扩展摩尔投票
Misra-Gries 算法是对摩尔投票的拓展。
摩尔投票只能找出绝对众数,即占比严格大于
为了更好的理解 Misra-Gries 算法,我们首先回忆一下摩尔投票(为了方便类比做了一定修改):
- 输出:如果存在绝对众数,输出绝对众数,否则输出任意值。
- 关键理论:原先占比严格大于
\dfrac12 的数在删去两个不同的数后占比依然严格大于\dfrac12 。
模仿摩尔投票,我们推测 Misra-Gries 算法的过程如下:
- 输出:输出
k-1 个占比严格大于\dfrac1k 的数,如果这种数不足k-1 个剩余的返回值会任意填充。 - 关键理论:原先占比严格大于
\dfrac1k 的数在删去k 个互不相同的数后占比依然严格大于\dfrac1k 。
感性理解就是删除
稍微证明一下顺便方便后面的一些推论:
定义一个数组
Lemma 1. 将
定义一个元素
Theorem 1. 所有
证明:
不妨假设存在一个
又因为每次均会删除
故假设不成立,即所有
称一个数组的
由 Lemma 1 有
(均可由 Lemma 1 证明)
根据这个性质我们得到了可以用线段树维护 summary。
证明和性质时间结束,现在是幻想时间代码时间
int k;
//插入一个数 x
void ins_one(summary &a,int x)
{
for(int i=0;i<k-1;i++) if(a.val[i]==x)
{a.cnt[i]++; return;}
for(int i=0;i<k-1;i++) if(!a.cnt[i])
{a.val[i]=x; a.cnt[i]=1; return;}
for(int i=0;i<k-1;i++) a.cnt[j]--;
}
summary reduce(vector<int> a)
{
summary ans;
for(int i=0;i<k-1;i++) ans.val[i]=-1,ans.cnt[i]=0;
for(int i:a) ins_one(a,i);
return ans;
}
该代码能以
一般 OI 里很少用到只求一次 reduce 的,因为
int k;
//插入 v 个 x,脑子里模拟一下跑 k 次 ins_one 代码应该很显然
void ins(summary &a,int x,int v)
{
for(int i=0;i<k-1;i++) if(a.val[i]==x)
{a.cnt[i]+=v; return;}
for(int i=0;i<k-1;i++) if(!a.cnt[i])
{a.val[i]=x; a.cnt[i]=v; return;}
int mn=v;
for(int i=0;i<k-1;i++) if(a.cnt[i]<mn) mn=a.cnt[i];
for(int i=0;i<k-1;i++) a.cnt[i]-=mn;
if(v>mn) for(int i=0;i<k-1;i++) if(!a.cnt[i])
{a.val[i]=x; a.cnt[i]=v-mn; return;}
}
summary merge(summary a,summary b)
{
for(int i=0;i<k-1;i++) if(b.cnt[i])
ins(a,b.val[i],b.cnt[i]);
return a;
}
例题1:Choosing Ads
看到区间查询占比大于某个阈值的数直接无脑上线段树维护 summary。
区间赋值直接清空 summary 然后插入一条
这里要求的是出现频率
题意等于出现频率
初始化
代码(半年前写的有点丑)
例题2:Destiny
也是线段树维护 summary,注意此题要求必须严格大于
vector<int> pos[N];
void init() {for(int i=1;i<=n;i++) pos[a[i]].push_back(i);}
int count(int l,int r,int x)
{
auto L=pos[x].begin(),R=pos[x].end();
return upper_bound(L,R,r)-lower_bound(L,R,l);
}
代码(注意
后记:优化
猫树优化:
若不修改数组(如例 2)就可以用猫树来优化初始化和查询。
如果朴素猫树的话初始化复杂度为
参考猫树维护线性基的做法不合并求值而是逐个插入可以把初始化复杂度降低至
插入优化:
朴素做法为
注意到 ins 函数内只有单点修改,求最小值,全局减这三个操作,那么可以用平衡树维护做到
ins 伪代码:
ins(a,x,v)
{
p=a.findx(x)
if(p.val!=-1) {a.set(p,{p.val,p.v+v}); return;}
mn=a.findmin()
if(mn.cnt==0) a.set(mn,{x,v})
else if(mn.cnt<v)
a.subtract(mn.cnt),a.set(mn,{x,v-mn.cnt});
else a.subtract(v);
}
具体维护方式为维护两棵互相同步的平衡树,一棵以val为第一关键字以cnt为第二关键字,一棵以cnt为第一关键字val为第二关键字。
查询x对应的二元组在第一棵树上查,查cnt最小的二元组在第二棵树上查,拿到完整的二元组之后在另一个树上定位就是 trivial 的了。
至于全局减打一个全局减法标记就行了,记得返回 cnt 的时候减去标记和插入的时候加上标记即可。
把两个拼起来猫树优化+插入优化:
询问复杂度降低到
由于猫树需要执行
考虑 summary 前/后缀和相邻两项的平衡树之间最多只有一个二元组不同,那么我们可以使用可持久化平衡树,这样只需要把根的编号和减法 tag 保存进前后缀数组里即可,这样预处理就只要
其实平时做题可能用上的也就猫树优化了毕竟出题人不太可能毒瘤到给你几千的