Medium
题目描述
给你一个正整数数组 nums 。在一次操作中,你可以从 nums 中选择任意一个数并将它减小到 恰好 一半。(注意,在后续操作中你可以对减半过的数继续减半。)
返回将 nums 的数组和 至少 减少一半所需的最少操作数。
示例 1:
输入:nums = [5,19,8,1]
输出:3
解释:初始 nums 的和为 5 + 19 + 8 + 1 = 33 。
下面是将和减少至少一半的一种方法:
选择数字 19 并减小为 9.5 。
选择数字 9.5 并减小为 4.75 。
选择数字 8 并减小为 4 。
最终数组为 [5, 4.75, 4, 1] ,和为 5 + 4.75 + 4 + 1 = 14.75 。
nums 的和减小了 33 - 14.75 = 18.25 ,减小的部分超过了初始和的一半,18.25 >= 33/2 = 16.5 。
总共执行了 3 次操作,所以返回 3 。
可以证明,要想减半 nums 的和,3 是最少操作数。
示例 2:
输入:nums = [3,8,20]
输出:3
解释:初始 nums 的和为 3 + 8 + 20 = 31 。
下面是将和减少至少一半的一种方法:
选择数字 20 并减小为 10 。
选择数字 10 并减小为 5 。
选择数字 3 并减小为 1.5 。
最终数组为 [1.5, 8, 5] ,和为 1.5 + 8 + 5 = 14.5 。
nums 的和减小了 31 - 14.5 = 16.5 ,减小的部分超过了初始和的一半,16.5 >= 31/2 = 15.5 。
总共执行了 3 次操作,所以返回 3 。
可以证明,要想减半 nums 的和,3 是最少操作数。
提示:
1 <= nums.length <= 10^51 <= nums[i] <= 10^7
解题思路
这道题需要我们通过最少的操作次数,使数组和减少至少一半。每次操作可以将任意一个数减半。
核心思路:贪心策略 + 最大堆
要使操作次数最少,我们应该每次都选择当前最大的数进行减半操作。这样能够最大化每次操作对总和的减少量。
具体步骤:
- 计算数组的初始总和,目标减少量为总和的一半
- 使用最大堆(优先队列)维护所有数值
- 重复以下操作直到减少量达到目标:
- 取出堆顶最大值
- 将其减半后重新加入堆中
- 累计减少量和操作次数
时间优化: 由于涉及浮点运算,为避免精度问题,实际实现中可以直接计算减少的总量而不是维护当前总和。
这种贪心策略是最优的,因为每次选择最大值减半能够使每步操作的收益最大化。
推荐解法: 使用优先队列实现最大堆的贪心算法。
代码实现
class Solution {
public:
int halveArray(vector<int>& nums) {
priority_queue<double> pq;
double total = 0;
for (int num : nums) {
pq.push(num);
total += num;
}
double target = total / 2;
double reduced = 0;
int operations = 0;
while (reduced < target) {
double largest = pq.top();
pq.pop();
double half = largest / 2;
reduced += half;
pq.push(half);
operations++;
}
return operations;
}
};
class Solution:
def halveArray(self, nums: List[int]) -> int:
import heapq
# Python's heapq is min heap, so use negative values for max heap
heap = [-num for num in nums]
heapq.heapify(heap)
target = sum(nums) / 2
reduced = 0
operations = 0
while reduced < target:
largest = -heapq.heappop(heap)
half = largest / 2
reduced += half
heapq.heappush(heap, -half)
operations += 1
return operations
public class Solution {
public int HalveArray(int[] nums) {
var pq = new PriorityQueue<double, double>(Comparer<double>.Create((x, y) => y.CompareTo(x)));
double total = 0;
foreach (int num in nums) {
pq.Enqueue(num, num);
total += num;
}
double target = total / 2;
double reduced = 0;
int operations = 0;
while (reduced < target) {
double largest = pq.Dequeue();
double half = largest / 2;
reduced += half;
pq.Enqueue(half, half);
operations++;
}
return operations;
}
}
var halveArray = function(nums) {
// Use array to simulate max heap
const heap = [...nums];
heap.sort((a, b) => b - a);
const target = nums.reduce((sum, num) => sum + num, 0) / 2;
let reduced = 0;
let operations = 0;
while (reduced < target) {
heap.sort((a, b) => b - a); // Re-sort to maintain max heap property
const largest = heap.shift();
const half = largest / 2;
reduced += half;
heap.push(half);
operations++;
}
return operations;
};
复杂度分析
| 复杂度 | 分析 |
|---|---|
| 时间复杂度 | O(n log n) |
| 空间复杂度 | O(n) |
详细分析:
- 时间复杂度:初始化堆需要 O(n),最坏情况下需要进行 O(n log n) 次堆操作,每次操作时间复杂度为 O(log n)
- 空间复杂度:需要 O(n) 的空间存储优先队列