最近沉迷 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+1−ai,i+1),分别表示在继续选择 a i + 1 a_{i+1} ai+1 的基础上,携带和放弃 a i a_i ai 的情况,一个特别简单的 DP。然后直接全丢进队列里,一个个拿出来更新就可以得到所有的和了(记搜跟dp不是一个东西嘛),不会出现状态重复的情况。
然后就是跟 dijkstra 差不多一样的思想,把队列直接换成一个优先队列,让最小的在最上面,这样就能保证取出来的和是从小到大的,那么第 k − 1 k - 1 k−1 次取出来的就是第 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 tot−sum=sum2−sum3,我们就得到了没被选中的那些原本就是正数的元素和被选中原本是负数的元素他们全部变成正数之前和值之和。
例如: [ − 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 tot−sum=4−8=−4=(−5)+(−2)+3。
结合上面的那个小问题,我们可以得到 s u m sum sum 也就是数组里面的元素全部变成正数之后所有的和,并且得到的顺序是从小到大的,那么 t o t − s u m tot - sum tot−sum 对应的则是数组里面的元素全部变成正数之前所有和的情况(可以拿上面的例子手动模拟一下),并且 s u m sum sum 和 t o t − s u m tot - sum tot−sum 全部都是不重不漏的,每一个和都对应一种选择 。我们之前已经实现了 s u m sum sum 从小到大的获取,又因为 t o t tot tot 是个常数,所以 t o t − s u m tot - sum tot−sum 也可以实现从大到小的获取,第 k − 1 k - 1 k−1 个获取到的便是第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
}
};
作者能力有限,如果有任何错误之处,还请各位指教。(~ ̄▽ ̄)~