leetcode

找出数组的第 K 大和(优先队列/二分+dfs)

题目描述
给你一个整数数组 nums 和一个 正 整数 k 。你可以选择数组的任一 子序列 并且对其全部元素求和。

数组的第 k 大和定义为:可以获得的第 k个最大子序列和(子序列和允许出现重复)
返回数组的第 k 大和 。
子序列是一个可以由其他数组删除某些或不删除元素排生而来的数组,且派生过程不改变剩余元素的顺序。
注意:空子序列的和视作 0 。

数据样例
示例 1:
输入:nums = [2,4,-2], k = 5
输出:2
解释:所有可能获得的子序列和列出如下,按递减顺序排列:
-6、4、4、2、2、0、0、-2
数组的第 5 大和是 2 。

示例 2:
输入:nums = [1,-2,3,4,-10,12], k = 16
输出:10
解释:数组的第 16 大和是 10 。

思路1:优先队列解法
思路分析:这个问题其实可以转换一下思路,因为这个题目本身并未要求序列是连续子序列,所以我们反过来想不就是,根据数列的最大子集和(一定是所有正数相加),然后在减去某些正数,或者加上某些负数嘛。
例如:例如 nums=[1,2,3,−4],其非负数的和为 1+2+3=6,我们可以从 6 中减去 2 得到 nums的子序列 [1,3]的和 1+3=4,也可以把 6和 −4 相加,得到 nums的子序列 [1,2,3,−4]的和 2。
这里大家可以发现,我们去求减去第k小的元素,针对正数和负数的处理可以统一,也就是取绝对值都进行相减,本质上我们都是在最大值的基础上去减去某一个值,那么我们本题目就可以转换为求所有子序列的第k大的值。

那么如何生成所有不重复的子序列,可以利用优先队列按照从小到大生成
如何不重不漏地生成 nums\textit{nums}nums 的所有子序列?

以有序非负数组 nums=[1,2,3]为例,有2^3=8,生成的方法如下:

从空子序列 [] 开始。
在 []末尾添加 1 得到 [1]。
在 [1]末尾添加 2得到 [1,2]。也可以把末尾的 1 替换成 2 得到 [2]。
在 [2] 末尾添加 3得到 [2,3]。也可以把末尾的 2 替换成 3得到 [3]。
在 [1,2]末尾添加 3 得到 [1,2,3]。也可以把末尾的 2 替换成 3 得到 [1,3]。
上述过程结合最小堆,就可以按照从小到大的顺序生成所有子序列了(堆中维护子序列的和以及下一个要添加/替换的元素下标)。取生成的第 k 个子序列的和,作为 sum要减去的数。
这里大家可以发现两个点:

  • 下一个节点是从上一个节点进行拓展,所以这里我们需要再进入优先队列的时候,把当前节点的下一个节点的下标一起入队列,这样才能进行所有子序列状态的枚举更新。
  • 通过优先队列每次可以保证一定是当前序列中最小的节点,这个就类似于我们的Dijkstra算法拓展边的过程,没有负权的边,那么当前堆顶的元素一定是全局最小的。

代码如下:

long long kSum(vector<int>& nums, int k) {
        long long sum = 0;
        for(auto &x : nums){
            if(x > 0){
                sum += x;
            }else{
                x = -x;
            }
        }
        ranges::sort(nums);
        priority_queue<pair<long,int>, vector<pair<long,int>>, greater<>>pq;
        pq.push(make_pair(0,0));
        while(--k){
            auto [s , i] = pq.top();
            pq.pop();
            if(i < nums.size()){
                pq.push(make_pair(s + nums[i] , i + 1));//当前元素选
                if(i){
                    pq.push(make_pair(s + nums[i] - nums[i - 1], i + 1));//当前元素选,并且去掉之前的元素,也就是从1到1,2 或者2的状态拓展
                }
            }
        }
        return sum - pq.top().first;//减去小k小的值 得到第k大的结果
        
    }

思路2:二分+dfs剪支
二分答案,设当前二分的值为 sumLimit

问题变成:判断是否有至少 k 个子序列,其元素和 s 不超过 sumLimit

注:一道题能否二分答案,得看它有没有单调性。对于本题,sumLimit越大,这样的子序列越多,有单调性,可以二分答案。

爆搜,从小到大考虑每个 nums[i]选或不选。在递归中,如果发现 cnt=k或者 s+nums[i]>sumLimits,就不再继续递归,因为前者说明我们已经找到 k 个和不超过sumLimit 的子序列,后者说明子序列的和太大。由于每个 nums[i] 都取了绝对值,没有负数,s不会减小,所以可以剪枝。

二分下界:0。

二分上界:所有元素的绝对值的和。

最后,用 sum减去二分得到的值,即为答案。

class Solution {
public:
    vector<int> nums;
    int k;
    int cnt;
    long long kSum(vector<int>& nums, int k) {
        this->nums = nums;
        this->k = k;
        long long sum = 0, sum1 = 0;
        for(auto &x : this->nums){
            if(x > 0){
                sum += x;
            }else{
                x = -x;
            }
            sum1 += x;
        }
        ranges::sort(this->nums);
        long long l = 0 ,r = sum1;
        while(l <= r){
            long long mid = (l + r) /2;
            bool res = check(mid);
            if(res) r = mid - 1;
            else l = mid + 1;
        }
        return sum - l;
        
    }
private:
    void dfs(int i, long long s, long long sum_limit) {
        if (cnt == k || i == nums.size() || s + nums[i] > sum_limit) {
            return;
        }
        cnt++; // s + nums[i] <= sum_limit
        dfs(i + 1, s + nums[i], sum_limit); // 选
        dfs(i + 1, s, sum_limit); // 不选
    }

    bool check(long long sum_limit) {
        cnt = 1; // 空子序列算一个
        dfs(0, 0, sum_limit);
        return cnt >= k; // 找到 k 个元素和不超过 sum_limit 的子序列
    }
};

注意:这里需要说明一下 为啥二分的结果最后一定是子序列的结果
这里用反证法说明一下:
假设 x 不是 nums 的子序列和,也就是没有任何子序列的和等于 x,这意味着 s≤x等价于 s≤x−1(这里因为s不是子序列,那么针对x-1也一定合法 因为其一定也不是子序列),我们能从 nums中找到 k个元素和不超过 x−1 的子序列,所以 check(x−1)=true。但二分循环结束时,有 check(x−1)=false(这里是因为二分的边界一定是l = mid + 1 也就是为False的情况 那么也就是check(x-1)=false)矛盾,所以原命题成立,x 一定是 nums的子序列和。

  • 15
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值