前言
这玩意儿感觉挺特殊的,因为写到现在都没见过要用这个的题()
一、多数元素
cpp
class Solution {
public:
int majorityElement(vector<int>& nums) {
int n=nums.size();
//水王数:词频大于n/2的数
//如果有,那么一次删掉两个不同的数,最终剩下的一定就是水王数
//如果没有,那么最终剩下的不一定是水王数
//如果不是,那么说明没有水王数
//筛选
//如果无候选,那么当前数就成为候选,血量=1
//若有候选,若当前数不是候选,血量-1,否则血量+1
int cand=0;
int hp=0;
for(int i=0;i<n;i++)
{
//没有候选
if(hp==0)
{
cand=nums[i];
hp=1;
}
else if(nums[i]==cand)
{
hp++;
}
else
{
hp--;
}
}
//没有剩下
if(hp==0)
{
return -1;
}
//判断
//复用hp
hp=0;
for(int i=0;i<n;i++)
{
if(nums[i]==cand)
{
hp++;
}
}
if(hp>n/2)
{
return cand;
}
return -1;
}
};
首先先说一下水王数的定义,水王数就是词频大于n/2,即一半的数。如果有水王数,那么一次删掉两个不同的数,剩下的一定就是水王数。如果没有,那么最终剩下的不一定是水王数。如果不是,说明不存在水王数。
所以表现在代码上,就是设置一个cand变量表示当前候选的数,hp表示"血量"。若无候选,即hp等于0,那么就设置当前数为候选,让hp为1。之后若当前数不是候选,就让hp-1,否则就让hp+1。之后若最后有候选,那么就考虑复用hp变量,再遍历一遍统计词频,检查是否大于一半。
二、摩尔投票智力题
给定一堆选票,一开始均背面朝上放在桌子上,正面写人名,背面无差异。你有一台机器,可以验证任意两张选票上写的人名是否一致。若有一个人的得票数超过一半,那么该人当选,投票成功;否则无人当选,投票失败。你只能利用机器,在全程不看正面的情况下,如何知道结果是成功还是失败?若失败,需要宣布这一点;否则需要找出任意一张写着当选人名字的选票,并在最后时刻才翻开这张选票。
因为摩尔投票不需要知道每个数具体是什么,只关心两张票一不一样。所以就可以先给每张票编个号,再设置候选和血量跑一遍,每次去看当前票和候选票一不一样。最后再跑一遍看看候选票的词频有没有超过一半即可。
三、摩尔投票多线程并发执行
对于词频大于一半的数,因为总血量很高,所以不管怎么"针对",这个数都能活到最后。所以可以考虑用多线程划分数组,每个线程只进行自己负责的那一段的摩尔投票,那么最终每个线程都会产生一个负责区域的水王数和血量。所以之后再整合所有线程,由于真正的水王数的血量一定是比其他数加起来还高的,所以这个数一定会活到最后。那么有了这个候选数,就可以让这几个线程统计自己负责区间中这个数的词频,最后整合一下看是否大于一半即可。
四、合法分割的最小下标
cpp
class Solution {
public:
int minimumIndex(vector<int>& nums) {
int n=nums.size();
//若两边的水王数一样,说明这个就是真正的水王数
//所以考虑先求出真正的水王数及词频
//然后枚举划分点,统计词频即可
int cand=0;
int hp=0;
for(auto &x:nums)
{
if(hp==0)
{
cand=x;
hp=1;
}
else if(cand==x)
{
hp++;
}
else
{
hp--;
}
}
hp=0;
for(auto &x:nums)
{
if(x==cand)
{
hp++;
}
}
//左侧和右侧的词频
int lc=0;
int rc=hp;
for(int i=0;i<n-1;i++)
{
if(nums[i]==cand)
{
lc++;
rc--;
}
//找到了
if(lc>(i+1)/2&&rc>(n-i-1)/2)
{
return i;
}
}
return -1;
}
};
由上述例子可以得出,若两边的水王数一样,那么就说明这个数就是水王数。所以考虑先求出这个水王数,再枚举划分点统计词频即可。
五、多数元素 II
cpp
class Solution {
public:
vector<int> majorityElement(vector<int>& nums) {
//对于某个确定的k,水王数最多只会有k-1个
//所以只需要建立k-1个候选,每次同时让血量减少即可
return majority(nums,3);
}
vector<int> majority(vector<int>&nums,int k)
{
vector<vector<int>>cands(--k,vector<int>(2));
for(int &x:nums)
{
update(cands,k,x);
}
vector<int>ans;
collect(cands,k,nums,ans);
return ans;
}
//更新cands
void update(vector<vector<int>>&cands,int k,int x)
{
for(int i=0;i<k;i++)
{
//之前有过这个数
if(cands[i][0]==x&&cands[i][1]>0)
{
cands[i][1]++;
return ;
}
}
for(int i=0;i<k;i++)
{
//候选没满
if(cands[i][1]==0)
{
cands[i][0]=x;
cands[i][1]=1;
return ;
}
}
for(int i=0;i<k;i++)
{
if(cands[i][1]>0)
{
cands[i][1]--;
}
}
}
//验证候选
void collect(vector<vector<int>>&cands,int k,vector<int>&nums,vector<int>&ans)
{
int n=nums.size();
for(int i=0,cur,real;i<k;i++)
{
//候选
if(cands[i][1]>0)
{
cur=cands[i][0];
real=0;
for(int &x:nums)
{
if(cur==x)
{
real++;
}
}
if(real>n/(k+1))
{
ans.push_back(cur);
}
}
}
}
};
另一个结论就是,若要求词频大于n分之k,那么最多存在k-1个水王数。那么就可以直接暴力维护k-1个候选数,然后正常跑摩尔投票即可。所以对于每个数x,都遍历一遍k-1个候选数,看能否新增或更新候选数。最后验证时也是对所有候选数,都遍历数组检查是否符合即可。
六、子数组中占绝大多数的元素
摩尔投票因为可以并发执行,所以父范围上的候选数可以通过子范围在O(1)的时间里整合出,所以也可以通过线段树维护区间水王数。
cpp
class MajorityChecker {
public:
const int MAXN=2e4+5;
//有序数组
vector<vector<int>>nums;
//候选 -> 线段树
vector<int>cand;
//血量 -> 线段树
vector<int>hp;
int n;
MajorityChecker(vector<int>& arr) {
n=arr.size();
nums.resize(MAXN,vector<int>(2));
cand.resize(MAXN<<2);
hp.resize(MAXN<<2);
buildCnt(arr);
buildTree(arr,1,n,1);
}
int query(int left, int right, int threshold) {
//线段树上的下标要+1
auto ch=findCandidate(left+1,right+1,1,n,1);
int c=ch[0];
return findCnt(left,right,c)>=threshold?c:-1;
}
//初始化有序数组
void buildCnt(vector<int>&arr)
{
for(int i=0;i<n;i++)
{
nums[i][0]=arr[i];
nums[i][1]=i;
}
//值不同按值从小到大排序,值相同按下标从小到大排序
sort(nums.begin(),nums.begin()+n,[](vector<int>&x,vector<int>&y)
{
return x[0]!=y[0]?x[0]<y[0]:x[1]<y[1];
});
}
//查v在[l,r]上的词频
int findCnt(int l,int r,int v)
{
return bs(v,r)-bs(v,l-1);
}
//[0,i]范围上,小于v及等于v且下标小于等于i的数的个数
int bs(int v,int i)
{
int l=0;
int r=n-1;
int m;
int ans=-1;
while(l<=r)
{
m=(l+r)>>1;
if(nums[m][0]<v||(nums[m][0]==v&&nums[m][1]<=i))
{
ans=m;
l=m+1;
}
else
{
r=m-1;
}
}
//返回个数
return ans+1;
}
void buildTree(vector<int>&arr,int l,int r,int i)
{
if(l==r)
{
cand[i]=arr[l-1];
hp[i]=1;
}
else
{
int m=(l+r)>>1;
buildTree(arr,l,m,i<<1);
buildTree(arr,m+1,r,i<<1|1);
up(i);
}
}
void up(int i)
{
int lc=cand[i<<1];
int lh=hp[i<<1];
int rc=cand[i<<1|1];
int rh=hp[i<<1|1];
//两个候选对碰
cand[i]=lc==rc||lh>=rh?lc:rc;
hp[i]=lc==rc?(lh+rh):abs(lh-rh);
}
//找出[l,r]上的候选
//根据摩尔投票并发执行的原理
//父范围上的水王数可以由子范围的水王数在O(1)的时间内得到
vector<int> findCandidate(int jobl,int jobr,int l,int r,int i)
{
if(jobl<=l&&r<=jobr)
{
return {cand[i],hp[i]};
}
int m=(l+r)>>1;
//只在左侧
if(jobr<=m)
{
return findCandidate(jobl,jobr,l,m,i<<1);
}
//只在右侧
if(jobl>=m+1)
{
return findCandidate(jobl,jobr,m+1,r,i<<1|1);
}
auto lch=findCandidate(jobl,jobr,l,m,i<<1);
auto rch=findCandidate(jobl,jobr,m+1,r,i<<1|1);
int lc=lch[0];
int lh=lch[1];
int rc=rch[0];
int rh=rch[1];
int c=lc==rc||lh>=rh?lc:rc;
int h=lc==rc?(lh+rh):abs(lh-rh);
return {c,h};
}
};
首先查询范围内某个数的词频这件事,可以考虑先构建一个有序数组,按值从小到大,下标从小到大排序,那么每次查询时就可以考虑去二分搜索左右边界。所以定义一个二分函数,返回[0,i]范围内,小于v和等于v且下标小于等于i的数的个数。那么查询[l,r]范围上某个数v的词频,就可以用[0,r]范围的个数减去[0,l-1]范围的个数。
之后就需要考虑如何快速查询区间内的水王数,那么就可以考虑用线段树维护区间水王数及血量。那么首先build时叶节点的水王数就是自己,血量为1。之后区间合并up时,若左右的候选数一样,或左侧候选数的血量比右侧大,那么整体的候选数就是左侧数,否则就是右侧数。所以若左右候选数相同,那么血量就是直接相加,否则就是取绝对差即可。所以query里就需要返回范围内的候选数和血量,那么若被全包或只存在一侧就直接返回。否则就去左右收集候选数和血量,然后用类似的方法碰出候选数和血量即可。
总结
还是比较简单的,加油!