leetcode2386 找出数组第K大 题解

最近沉迷 leetcode,写写题解ψ(`∇´)ψ

题面描述

原来的题面

大致意思是给你一个包含正负整数的数组 a a a,然后你可以在里面选若干个数加到一起得到一个和(这个和的值可能会有重复,可以不取),现在问你在所有的可能中,第 k k k 大的和是多少。

元素最多 1 0 5 10 ^ 5 105 个, k k k 最大是 2000 2000 2000

题解

排序

这个不用多说,为了后面好搞。

一个小问题

在一堆正整数里面,选择若干个元素求和,在所有的和中,第 k 小的和是多少。

可以借助优先队列进行动态规划, ( s u m , i ) (sum, i) (sum,i) 表示选择的最后一个元素下标是 i i i,并且选择的元素之和是 s u m sum sum,那么借助 ( s u m , i ) (sum, i) (sum,i) 我们可以得到 ( s u m + a i + 1 , i + 1 ) ( s u m + a i + 1 − a i , i + 1 ) (sum + a_{i+1}, i + 1)(sum + a_{i + 1} - a_i, i + 1) (sum+ai+1,i+1)(sum+ai+1ai,i+1),分别表示在继续选择 a i + 1 a_{i+1} ai+1 的基础上,携带和放弃 a i a_i ai 的情况,一个特别简单的 DP。然后直接全丢进队列里,一个个拿出来更新就可以得到所有的和了(记搜跟dp不是一个东西嘛),不会出现状态重复的情况。

然后就是跟 dijkstra 差不多一样的思想,把队列直接换成一个优先队列,让最小的在最上面,这样就能保证取出来的和是从小到大的,那么第 k − 1 k - 1 k1 次取出来的就是第 k k k 大的(因为空集是最小的)。

更进一步的内容

记数组中所有的正数之和为 t o t tot tot,并且把数组中所有的负数全部变成正数。每次我们选择一些元素之后,我们将他们的和记为 s u m sum sum,被选中的原本是正数的元素和是 s u m 1 sum1 sum1,没被选中但原本是正数的元素之和为 s u m 2 sum2 sum2,被选中原本是负数的元素之和是 s u m 3 sum3 sum3,没被选中原本是负数的元素和是 s u m 4 sum4 sum4

那么 t o t = s u m 1 + s u m 2 , s u m = s u m 1 + s u m 3 tot = sum1 + sum2,sum = sum1 + sum3 tot=sum1+sum2,sum=sum1+sum3,所以 t o t − s u m = s u m 2 − s u m 3 tot - sum = sum2 - sum3 totsum=sum2sum3,我们就得到了没被选中的那些原本就是正数的元素和被选中原本是负数的元素他们全部变成正数之前和值之和。

例如: [ − 5 , − 4 , − 2 , 1 , 3 ] [-5,-4,-2,1,3] [5,4,2,1,3],我们把他们全部变成正数之后就是 [ 1 , 2 , 3 , 4 , 5 ] [1,2,3,4,5] [1,2,3,4,5] t o t = 4 tot = 4 tot=4 1 + 3 = 4 1 + 3 = 4 1+3=4),假设我们选中了 [ 1 , 2 , 5 ] [1,2,5] [1,2,5],那么 t o t − s u m = 4 − 8 = − 4 = ( − 5 ) + ( − 2 ) + 3 tot - sum = 4 - 8 = -4 = (-5) + (-2) + 3 totsum=48=4=(5)+(2)+3

结合上面的那个小问题,我们可以得到 s u m sum sum 也就是数组里面的元素全部变成正数之后所有的和,并且得到的顺序是从小到大的,那么 t o t − s u m tot - sum totsum 对应的则是数组里面的元素全部变成正数之前所有和的情况(可以拿上面的例子手动模拟一下),并且 s u m sum sum t o t − s u m tot - sum totsum 全部都是不重不漏的,每一个和都对应一种选择 。我们之前已经实现了 s u m sum sum 从小到大的获取,又因为 t o t tot tot 是个常数,所以 t o t − s u m tot - sum totsum 也可以实现从大到小的获取,第 k − 1 k - 1 k1 个获取到的便是第K大的和。

代码

class Solution
{
public:
    long long kSum(vector<int> &nums, int k)
    {
#define mp(a, b) make_pair(a, b) // 方便往优先队列里面存放状态
        long long tot = 0;
        for (int i = 0; i < nums.size(); i++) // 把所有的正数进行求和,负数变成正数
            if (nums[i] >= 0)
                tot += nums[i];
            else
                nums[i] = -nums[i];
        sort(nums.begin(), nums.end()); 
        priority_queue<pair<long long, int>, vector<pair<long long, int>>, greater<pair<long long, int>>> q;
        q.push(mp((long long)nums[0], 0)); // 对于正数求和的那个小问题来讲,空集是最小的,只包含最小值是第二小的
        int cnt = 0; // 是第几个从优先队列里面拿出来的
        while (!q.empty())
        {
            long long sum = q.top().first;
            int id = q.top().second;
            cnt++;
            if (cnt == k - 1) // 第 k - 1 个从队列里面拿出来的就是第 k 大的
                return tot - sum;
            q.pop();
            if (id < nums.size() - 1) // 状态转移一下
            {
                q.push(mp(sum + nums[id + 1], id + 1));
                q.push(mp(sum + nums[id + 1] - nums[id], id + 1));
            }
        }
#undef mp(a, b)
        return tot; // 因为有 k = 1 的情况,返回一个 tot
    }
};

提交结果

作者能力有限,如果有任何错误之处,还请各位指教。(~ ̄▽ ̄)~

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值