• [leetcode 周赛 149] 1157 子数组中占绝大多数的元素


    1157 Online Majority Element In Subarray 子数组中占绝大多数的元素

    描述

    实现一个 MajorityChecker 的类,它应该具有下述几个 API

    • MajorityChecker(int[] arr) 会用给定的数组 arr 来构造一个 MajorityChecker 的实例。

    • int query(int left, int right, int threshold) 有这么几个参数:

      • 0 <= left <= right < arr.length 表示数组 arr 的子数组的长度。
      • 2 * threshold > right - left + 1,也就是说阈值 threshold 始终比子序列长度的一半还要大。
        每次查询 query(...) 会返回在 arr[left], arr[left+1], ..., arr[right] 中至少出现阈值次数 threshold 的元素,如果不存在这样的元素,就返回 -1
    • 示例:

    MajorityChecker majorityChecker = new MajorityChecker([1,1,2,2,1,1]);
    majorityChecker.query(0,5,4); // 返回 1
    majorityChecker.query(0,3,3); // 返回 -1
    majorityChecker.query(2,3,2); // 返回 2

    • 提示:

    1 <= arr.length <= 20000
    1 <= arr[i] <= 20000
    对于每次查询,0 <= left <= right < len(arr)
    对于每次查询,2 * threshold > right - left + 1
    查询次数最多为 10000

    思路

    • 首先读题:

      • 查询query需要拥有一个出现次数超过查询长度一半的元素
        众数(出现次数超过总数一半的元素)思想: 拥有众数的数组, 数组内元素两两对消(元素不相等)或两两相融(元素相等), 剩下的必定是众数
      • 查询次数上限10000, 需要保证查询速率
      • 阈值次数threshold, 在查询范围内, 众数出现次数获取
        可以使用二分查找左右边界
    • 可以使用动态规划的思想:

      • 状态转移公式

    l查询左边界 r查询右边界 val查询区域众数 cnt众数在查询区域出现次数
    - 子问题合并

    两子问题返回众数相等, 则出现次数相加, 不等则出现次数大减小

    以此思想可以建立线段树存储数据来加速查询速率

    代码实现

    使用线段树提前计算区域众数来加速查询

    // 通过线段树来存储某些区域的众数和计数 以此来加速查询
    class MajorityChecker {
        // idx 存储数组出现元素种类 以及该元素下标索引
        // [1, 2, 2, 1, 1, 2] --> [[1 : 0, 3, 4], [2 : 1, 2, 5]]
        HashMap<Integer, ArrayList<Integer>> idx;
        // 线段树的根节点
        SegTreeNode root;
        // key 所查找的区域众数
        // count 所查找的区域众数出现次数
        // 注意: 数组元素范围不包括0
        int key=0, count=0;
    
        /** 
         * 初始化 
         * 元素索引表idx 和 线段树root
         * 
         * @param arr 被查询数组
         * */
        public MajorityChecker(int[] arr) {
            idx = new HashMap<>();
            for (int i = 0; i < arr.length; i++) {
                if (!idx.containsKey(arr[i])) 
                    idx.put(arr[i], new ArrayList<Integer>());
                idx.get(arr[i]).add(i);
            }
            
            root = buildTree(arr, 0, arr.length-1);
        }
        
        /**
         * 查询区域众数 是否超过阈值
         * 
         * @param left 查询区域左边界
         * @param right 查询区域右边界
         * @param threshold 用来判断众数的出现次数阈值  
         * @return key/-1 如果所查询众数key的查询区域出现次数超过阈值threshold则返回key, 否则返回-1
         * */
        public int query(int left, int right, int threshold) {
            // 初始化 所查询众数key 及辅助判断的计数count
            key = 0; count = 0;
            // 查询线段树
            searchSegTree(root, left, right);
            // 如果查询区域没有众数 即key没被更改
            // 或者
            // 所查询出来的众数 在原数组中根本没有超出阈值的能力
            if (key == 0 || idx.get(key).size() < threshold) return -1;
            
            // 上确界 排序数组中 第一个大于right的下标
            int r = upper_bound(idx.get(key), right);
            // 下确界 排序数组中 第一个大于等于left的下标
            int l = lower_bound(idx.get(key), left);
            count = r - l;
            
            return count >= threshold ? key : -1;
        }
        
        // 排序数组中 第一个大于tar的下标
        int upper_bound(List<Integer> list, int tar) {
            int l = 0, r = list.size();
            while (l < r) {
                int mid = l + (r-l)/2;
                if (list.get(mid) <= tar) l = mid+1;
                else r = mid;
            }
            
            return l;
        }
    
        // 排序数组中 第一个大于等于tar的下标
        int lower_bound(List<Integer> list, int tar) {
            int l = 0, r = list.size()-1;
            while (l < r) {
                int mid = l + (r-l)/2;
                if (list.get(mid) < tar) l = mid+1;
                else r = mid;
            }
            
            return l;
        }
        
        /**
         * 构建线段树
         * 
         * @param arr 被构建数组
         * @param l 构建节点的左值 表示查询区域左边界
         * @param r 构建节点的右值 表示查询区域右边界
         * @return 以构建完成的线段树节点
         * */
        private SegTreeNode buildTree(int[] arr, int l, int r) {
            if (l > r) return null;
            
            // 初始一个线段树节点
            SegTreeNode root = new SegTreeNode(l, r);
            // 叶子节点
            if (l == r) {
                // 众数就是当前值 计数为1
                root.val = arr[l]; root.count = 1;
                return root;
            }
            
            int mid = (l+r)/2;
            // 构建左子节点
            root.left = buildTree(arr, l, mid);
            // 构建右子节点
            root.right = buildTree(arr, mid+1, r);
            // 整合父节点
            makeRoot(root);
            
            return root;
        }
        
        /**
         * 整合一个父节点
         * 
         * @param root 被整合节点
         * */
        private void makeRoot(SegTreeNode root) {
            if (null == root) return;
            
            // 如果该节点有左子节点 该节点的值"先"等于左子节点
            if (root.left != null) {
                root.val = root.left.val;
                root.count = root.left.count;
            }
            // 如果该节点还有右子节点 融合父节点和子节点
            if (root.right != null) {
                if (root.val == root.right.val) 
                    root.count = root.count + root.right.count;
                else {
                    if (root.count >= root.right.count) 
                        root.count = root.count - root.right.count;
                    else {
                        root.val = root.right.val; 
                        root.count = root.right.count - root.count;
                    }
                }
            }
        }
        
        /**
         * 查询线段树
         * 
         * @param root 被查询节点
         * @param l 需要查询的范围左边界
         * @param r 需要查询的范围右边界
         * */
        private void searchSegTree(SegTreeNode root, int l, int r) {
            if (null==root | l > r) return;
            if (root.l > r | root.r < l) return;
            
            // 当查询边界覆盖节点边界 该节点就是查询区域
            if (l <= root.l && root.r <= r) {
                if (key == root.val) count += root.count;
                else if (count <= root.count) {
                    key = root.val;
                    count = root.count - count;
                } else {
                    count = count - root.count;
                }
                return;
            }
            
            int mid = (root.r + root.l)/2;
            // root.l <= l <= mid 左节点也可以是查询区域
            if (l <= mid) {
                searchSegTree(root.left, l, r);
            }
            // mid+1 <= r <= root.r 右节点也可以是查询区域
            if (r >= mid+1) {
                searchSegTree(root.right, l, r);
            }
        }
        
        /**
         * 线段树节点 类
         * 用于存储区域边界/众数和计数 并且连接下一节点
         * */
        static class SegTreeNode {
            // l 所存储区域左边界
            // r 所存储区域右边界
            // val 所存储区域众数
            // count 所存储区域众数计数(出现次数)
            int l, r, val, count;
            SegTreeNode left, right;
            
            SegTreeNode(int l, int r) {
                this.l = l; this.r = r;
                val = 0; count = 0;
                left = null; right = null;
            }
        }
    }
    
  • 相关阅读:
    一个列表如何根据另一个列表进行排序(数组的相对排序)
    汉诺塔问题
    python面向对象基础
    python爬虫
    软件开发目录规范
    python--->包
    编译python文件
    python文件的俩种用途
    python模块的搜索路径
    python 循环导入的问题
  • 原文地址:https://www.cnblogs.com/slowbirdoflsh/p/11381565.html
Copyright © 2020-2023  润新知