数据结构与算法:摩尔投票算法

前言

这玩意儿感觉挺特殊的,因为写到现在都没见过要用这个的题()

一、多数元素

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里就需要返回范围内的候选数和血量,那么若被全包或只存在一侧就直接返回。否则就去左右收集候选数和血量,然后用类似的方法碰出候选数和血量即可。

总结

还是比较简单的,加油!

END

相关推荐
小羊学伽瓦4 小时前
【Java数据结构】——常见力扣题综合
java·数据结构·leetcode·1024程序员节
柯一梦4 小时前
深入解析C++ String类的实现奥秘
c++
文火冰糖的硅基工坊4 小时前
[人工智能-大模型-66]:模型层技术 - 两种编程范式:数学函数式编程与逻辑推理式编程,构建起截然不同的智能系统。
人工智能·神经网络·算法·1024程序员节
蜗牛沐雨5 小时前
详解c++中的文件流
c++·1024程序员节
im_AMBER5 小时前
Leetcode 34
算法·leetcode
im_AMBER5 小时前
Leetcode 31
学习·算法·leetcode
无聊的小坏坏5 小时前
从零开始:C++ TCP 服务器实战教程
服务器·c++·tcp/ip
吃着火锅x唱着歌5 小时前
LeetCode 74.搜索二维矩阵
算法·leetcode·矩阵
老王熬夜敲代码5 小时前
C++继承回顾
c++·笔记