关键字:贪心算法,优先队列,multiset
归航return:LeetCode 1610—可见点的最大数目zhuanlan.zhihu.comProblem
Given an array of integers target
. From a starting array, A
consisting of all 1's, you may perform the following procedure :
- let
x
be the sum of all elements currently in your array. - choose index
i
, such that0 <= i < target.size
and set the value ofA
at indexi
tox
. - You may repeat this procedure as many times as needed.
Return True if it is possible to construct the target
array from A
otherwise return False.
Example 1:
Input: target = [9,3,5]
Output: true
Explanation: Start with [1, 1, 1]
[1, 1, 1], sum = 3 choose index 1
[1, 3, 1], sum = 5 choose index 2
[1, 3, 5], sum = 9 choose index 0
[9, 3, 5] Done
Example 2:
Input: target = [1,1,1,2]
Output: false
Explanation: Impossible to create target array from [1,1,1,1].
Example 3:
Input: target = [8,5]
Output: true
Constraints:
N == target.length
1 <= target.length <= 5 * 10^4
1 <= target[i] <= 10^9
Solution
这道题目的例子就给了我们提示:每次选出 target 中数组最大的元素当作加出来以后的结果,然后反过来执行步骤,直到得到初始数组或者遇到了错误。具体来说,反过来执行应该这么做:首先找到最大的元素,然后将这个元素减去其他元素的和,就得到了上一步中这个位置的元素。遇到了错误的情况也是很好想到的——我们要保证最大的元素减去其他元素的和之后仍然是正整数,因此最大的元素必须大于所有其他元素的和,否则这说明上一步的状态根本不合理。
因此,上述要求就需要我们维护一个有序的数据结构,保证能很快得到最大元素,同时维护求和。显然求和是容易维护的,只需要一个额外变量,并在每次修改之后做减法即可,因此实际上就是要一个有序的数据结构。那么答案也就呼之欲出了,两种可能:优先队列,自平衡二叉搜索树。由于题目中的数组元素可能出现重复元素,因此这里要求我们的自平衡二叉搜索树允许重复元素,这里需要使用 C++ 的专属数据结构:std::multiset
。最后的代码中,C++ 将使用 std::multiset
,Java 将使用优先队列 PriorityQueue
。
好的,我们直接按照这个思路写出 C++ 代码:
typedef unsigned long long ull;
class Solution {
public:
bool isPossible(vector<int>& target) {
if (target.size() == 1){
return target[0] == 1;
}
multiset<ull> targetSorted(target.begin(), target.end());
ull sum = accumulate(target.begin(), target.end(), static_cast<ull>(0));
while (true){
ull maxNum = *targetSorted.rbegin();
ull otherNumSum = sum - maxNum;
if (maxNum <= otherNumSum){
return false;
}
sum -= otherNumSum;
maxNum -= otherNumSum;
targetSorted.erase(--targetSorted.end());
targetSorted.insert(maxNum);
if (*targetSorted.rbegin() == 1){
return true;
}
}
}
};
注:这里用了 C++ 的反向迭代器,方便地访问 multiset
中的最大值。Java 类似做法则是 set.last()
。
这里的 accumulate
库函数来源于标准库 <numeric>
,文档在这里。然而很不幸,在 [1,1000000000] 这个 test case 上超时了。究其原因,是因为我们在这里每次都要重复减去最大的元素,而实际上,如果最大的元素减去其他元素之和后还是最大的元素,那么我们仍然用这个元素减掉。为此我们可以定义一个 rate
变量,代表最大元素除以其他元素的和的比例,如果比值大于 2,我们直接减去其他元素的和的比值减一倍,保证每次修改之后,最大元素的和都不超过其他元素的和太多,这样就可以大幅节约时间了。修改后的代码如下:
typedef unsigned long long ull;
class Solution {
public:
bool isPossible(vector<int>& target) {
if (target.size() == 1){
return target[0] == 1;
}
multiset<ull> targetSorted(target.begin(), target.end());
ull sum = accumulate(target.begin(), target.end(), static_cast<ull>(0));
while (true){
ull maxNum = *targetSorted.rbegin();
ull otherNumSum = sum - maxNum;
if (maxNum <= otherNumSum){
return false;
}
ull rate = maxNum / otherNumSum;
if (rate > 2){
sum -= (rate - 1) * otherNumSum;
maxNum -= (rate - 1) * otherNumSum;
}
else{
sum -= otherNumSum;
maxNum -= otherNumSum;
}
targetSorted.erase(--targetSorted.end());
targetSorted.insert(maxNum);
if (*targetSorted.rbegin() == 1){
return true;
}
}
}
};
Java 中没有类似于 C++ 中这样的特殊数据结构 std::multiset
,但我们只关心最大值,因此可以使用优先队列来完成这一事项,由于 Java 中的优先队列是默认小根堆,因此这里还需要设定排序顺序,将比较顺序改成相反的顺序。代码如下:
class Solution {
public boolean isPossible(int[] target) {
if (target.length == 1){
return target[0] == 1;
}
long sum = 0;
PriorityQueue<Long> pq = new PriorityQueue<>(Comparator.reverseOrder());
for (long x : target){
pq.offer(x);
sum += x;
}
while (true){
long maxNum = pq.poll();
long otherNumSum = sum - maxNum;
if (maxNum <= otherNumSum){
return false;
}
long rate = maxNum / otherNumSum;
maxNum -= Math.max(rate - 1, 1L) * otherNumSum;
sum -= Math.max(rate - 1, 1L) * otherNumSum;
pq.offer(maxNum);
if (pq.peek() == 1){
return true;
}
}
}
}
时间复杂度上,每次求和都会减少一半的求和结果,因此时间复杂度是
EOF。