[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;
        }
    }
}
posted @ 2019-08-20 10:42  slowbird  阅读(565)  评论(0编辑  收藏  举报