找出唯一性数组的中位数

2024-08-27

题目:

给你一个整数数组 nums 。数组 nums唯一性数组 是一个按元素从小到大排序的数组,包含了 nums 的所有非空子数组中不同元素的个数。

换句话说,这是由所有 0 <= i <= j < nums.lengthdistinct(nums[i..j]) 组成的递增数组。

其中,distinct(nums[i..j]) 表示从下标 i 到下标 j 的子数组中不同元素的数量。

返回 nums 唯一性数组中位数

注意,数组的 中位数 定义为有序数组的中间元素。如果有两个中间元素,则取值较小的那个。

示例 1:

输入:nums = [1,2,3]

输出:1

解释:

nums 的唯一性数组为 [distinct(nums[0..0]), distinct(nums[1..1]), distinct(nums[2..2]), distinct(nums[0..1]), distinct(nums[1..2]), distinct(nums[0..2])],即 [1, 1, 1, 2, 2, 3] 。唯一性数组的中位数为 1 ,因此答案是 1 。

示例 2:

输入:nums = [3,4,3,4,5]

输出:2

解释:

nums 的唯一性数组为 [1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 3, 3, 3] 。唯一性数组的中位数为 2 ,因此答案是 2 。

示例 3:

输入:nums = [4,3,5,4]

输出:2

解释:

nums 的唯一性数组为 [1, 1, 1, 1, 2, 2, 2, 3, 3, 3] 。唯一性数组的中位数为 2 ,因此答案是 2 。

提示:

  • 1 <= nums.length <= 105
  • 1 <= nums[i] <= 105

暴力枚举所有子数组

思路:

暴力滑动窗口枚举元素,无须多盐

代码:

class Solution {
public:
    int medianOfUniquenessArray(vector<int>& nums) {
        // 思路1:滑动窗口,枚举所有子数组,统计子数组中不同元素的个数时间复杂度O(n^2 + nlogn)
        int n = nums.size();
        vector<int> distinct(n * (n + 1) /  2, 1);
        int idx = n;
        for (int i = 2; i <= n; i++) {  // 滑动窗口长度
            unordered_map<int, int> window;
            for (int j = 0; j < n; j++) {
                if (j < i) {
                    window[nums[j]]++;
                    continue;
                }
                distinct[idx++] = window.size();
                if (window[nums[j - i]] == 1) window.erase(nums[j - i]);
                else window[nums[j - i]]--;
                window[nums[j]]++;
                if (j == n - 1) distinct[idx++] = window.size();
            }
            if (i == n) distinct[idx++] = window.size();  // 统计原数组的不同元素个数
        }
        sort(distinct.begin(), distinct.end());
        if (n * (n + 1) / 2 % 2) return distinct[n * (n + 1) / 2 / 2];  // 奇数个
        return distinct[n * (n + 1) / 2 / 2 - 1]; 
    }
};

二分搜索优化

思路:

提示 1:二分答案

nums 的唯一性数组有多少个?也就是 nums 的非空连续子数组的个数。
长为 n 的子数组有 1 个,长为 n − 1 的子数组有 2 个,……,长为 1 的子数组有 n 个。
所有一共有 m = 1 + 2 + ⋯ + n = n * (n + 1) / 2 个非空连续子数组。
这 m 个子数组,对应着 m 个 distinct 值。
中位数是这 m 个数中的第 k=⌈m / 2⌉ 小元素。例如 m = 4 时,中位数是其中第 2 小元素。
考虑这 m 个数中,小于等于某个定值 upper 的数有多少个。
由于 upper 越大,小于等于 upper 的数越多,有单调性,故可以二分中位数为 upper,问题变成:
	distinct 值 ≤ upper 的子数组有多少个?
设子数组的个数为 cnt,如果 cnt<k 说明二分的 upper 小了,更新二分左边界 left,否则更新二分右边界 right。

提示 2:滑动窗口

怎么计算 distinct 值 ≤ upper 的子数组个数?
由于子数组越长,不同元素个数(distinct 值)不会变小,有单调性,故可以用滑动窗口计算子数组个数。
用一个哈希表 freq 统计窗口(子数组)内的元素及其出现次数。
枚举窗口右端点 r,把 nums[r] 加入 freq(出现次数加一)。如果发现 freq 的大小超过 upper,说明窗口内的元素过多,那么不断移出窗口左端点元素 nums[l](出现次数减一,如果出现次数等于 0 就从 freq 中移除),直到 freq 的大小 ≤ upper 为止。
此时右端点为 r,左端点为 l, l + 1, l + 2, ⋯, r 的子数组都是满足要求的(distinct 值 ≤ upper),一共有 r − l + 1 个,加到子数组个数 cnt 中。

其它细节

开区间二分左边界:0,一定不满足要求,因为没有子数组的 distinct 值是 0。
开区间二分右边界:n,或者 nums 中的不同元素个数,一定满足要求,因为所有子数组的 distinct 值都不超过 n。
用开区间计算二分仅仅是个人喜好,你也可以使用闭区间或半闭半开区间计算二分,并无本质区别。

答疑
问:为什么二分出来的答案,一定是某个子数组的 distinct 值?有没有可能,二分出来的答案不是任何子数组的 distinct 值?
答:反证法。如果答案 d 不是任何子数组的 distinct 值,那么 distinct 值 ≤d 和 ≤d−1 算出来的子数组个数是一样的。也就是说 d−1 同样满足要求,即 check(d - 1) == true,这与循环不变量相矛盾。

代码:

class Solution {
public:
    int medianOfUniquenessArray(vector<int>& nums) {
        int n = nums.size();
        long long k = ((long long) n * (n + 1) / 2 + 1) / 2;

        auto check = [&](int upper) {
            long long cnt = 0;
            int l = 0;
            unordered_map<int, int> freq;
            for (int r = 0; r < n; r++) {
                freq[nums[r]]++; // 移入右端点
                while (freq.size() > upper) { // 窗口内元素过多
                    int out = nums[l++];
                    if (--freq[out] == 0) { // 移出左端点
                        freq.erase(out);
                    }
                }
                cnt += r - l + 1; // 右端点固定为 r 时,有 r-l+1 个合法左端点
                if (cnt >= k) {
                    return true;
                }
            }
            return false;
        };

        int left = 0, right = n;
        while (left + 1 < right) {
            int mid = (left + right) / 2;
            (check(mid) ? right : left) = mid;
        }
        return right;
    }
};