文章目录
- [LeetCode2208. 将数组和减半的最少操作次数](#LeetCode2208. 将数组和减半的最少操作次数)
- LeetCode2406.将区间分为最少组数
LeetCode2208. 将数组和减半的最少操作次数
代码实现
python
from typing import List
import heapq
class Solution:
def halveArray(self, nums: List[int]) -> int:
"""
计算将数组和至少减少一半的最少操作数
解题思路:
使用贪心算法 + 最大堆(用负数实现)
核心思想:
1. 每次操作都选择当前最大的数进行减半,这样能最大程度减少数组和
2. 使用最大堆维护当前所有数,每次取出最大值
3. 累加每次减半的数值,直到减少的总和 >= 原数组和的一半
时间复杂度:O(n * log n)
空间复杂度:O(n)
"""
# 计算初始数组和
total_sum = sum(nums)
# 目标是减少至少一半
target = total_sum / 2
# 记录已经减少的数值
reduced_sum = 0
# 记录操作次数
operations = 0
# Python 的 heapq 是最小堆,用负数实现最大堆
# 将所有数取负后入堆,堆顶就是最大的数的负数
max_heap = [-num for num in nums]
heapq.heapify(max_heap)
# 贪心:每次选择最大的数减半
while reduced_sum < target:
# 弹出最大值(注意是负数)
max_num = -heapq.heappop(max_heap)
# 减半
half_num = max_num / 2
# 累加减少的数值
reduced_sum += half_num
# 操作次数 +1
operations += 1
# 将减半后的数重新加入堆中(后续可能继续减半)
heapq.heappush(max_heap, -half_num)
return operations
核心思路 贪心策略
每次选择当前最大的数减半
为什么?
- 假设当前有 [19, 8, 5, 1]
- 减半 19:减少 9.5 ✓ 最大
- 减半 8:减少 4
- 减半 5:减少 2.5
- 减半 1:减少 0.5
结论 :减半越大的数,减少得越多,越快达到目标!
详细执行过程
以 [5, 19, 8, 1] 为例:
初始状态:
数组:[5, 19, 8, 1]
总和:33
目标:减少至少 16.5
操作 1:
选择最大的 19,减半为 9.5
减少:9.5
累计:9.5
数组变为:[5, 9.5, 8, 1]
操作 2:
选择最大的 9.5,减半为 4.75
减少:4.75
累计:14.25
数组变为:[5, 4.75, 8, 1]
操作 3:
选择最大的 8,减半为 4
减少:4
累计:18.25 ✓ 达到目标(18.25 >= 16.5)
数组变为:[5, 4.75, 4, 1]
答案:3 次操作
代码关键点 最大堆的实现
Python 的 heapq 是最小堆,如何模拟最大堆?
技巧:所有数取负数
max_heap = [-num for num in nums]
heapq.heapify(max_heap)
弹出时再取负,得到最大值
max_num = -heapq.heappop(max_heap)
循环条件
while reduced_sum < target:
继续操作,直到减少的总和 >= 目标值
重复利用
减半后的数可以再次减半:
减半后重新加入堆中
heapq.heappush(max_heap, -half_num)
复杂度分析
-
时间复杂度 :O(n log n)
- 建堆:O(n)
- 每次堆操作:O(log n)
- 最多操作 n 次(实际远小于 n)
-
空间复杂度 :O(n)
- 堆存储所有元素
LeetCode2406.将区间分为最少组数
代码实现
python
class Solution:
def minGroups(self, intervals: List[List[int]]) -> int:
"""
计算将区间划分为最少组数
解题思路:
使用贪心算法 + 最小堆
核心思想:
1. 将所有区间按照左端点从小到大排序
2. 使用最小堆维护每个组当前最后一个区间的右端点
3. 遍历每个区间:
- 如果当前区间的左端点 > 堆顶(某个组的最右端点),说明可以接在该组后面,弹出堆顶
- 将当前区间的右端点加入堆中(可能是新开一组,也可能是更新某组的右端点)
4. 堆的大小就是所需的最少组数
注意:这是闭区间,[1,4] 和 [4,5] 在点 4 处相交,不能放在同一组
所以判断条件是 min_heap[0] < left(严格小于)
时间复杂度:O(n * log n)
空间复杂度:O(n)
"""
# 按照区间的左端点从小到大排序
# 这样可以保证我们按顺序处理每个区间,便于贪心决策
intervals.sort(key=lambda x: x[0])
# 最小堆,存储每个组当前最后一个区间的右端点
# 堆顶元素是所有组中右端点最小的值(即最早结束的组)
min_heap = []
# 遍历排序后的每个区间
for interval in intervals:
left, right = interval[0], interval[1]
# 关键判断:如果堆不为空,且堆顶(最小右端点)< 当前区间的左端点
# 说明当前区间可以接在该组后面(无重叠)
# 因为是闭区间,[a,b] 和 [b,c] 在点 b 处相交,不能放在同一组
# 所以必须是严格小于,即 min_heap[0] < left 才能接上
if min_heap and (min_heap[0] < left):
# 弹出堆顶,表示当前区间可以接在这个组后面
# 弹出后该组会被当前区间更新右端点
heapq.heappop(min_heap)
# 将当前区间的右端点加入堆中
# 两种情况:
# 1. 如果刚才弹出了堆顶,这相当于更新了该组的右端点
# 2. 如果没有弹出,这是新开一个组
heapq.heappush(min_heap, right)
# 堆的大小就是最少需要的组数
# 因为堆中每个元素代表一个组的右端点
return len(min_heap)
算法:贪心 + 最小堆
步骤 1:排序
将所有区间按左端点从小到大排序
步骤 2:遍历 + 维护最小堆
- 堆的作用:存储每个组的右端点
- 堆顶:所有组中右端点最小的(最早结束的组)
python
堆 = [组 1 的右端点,组 2 的右端点,组 3 的右端点...]
堆大小 = 当前组数
堆顶 = 最早结束的组
步骤 3:对每个区间做决策
bash
对于区间 [left, right]:
if 堆顶 < left:
# 可以接在该组后面(无重叠)
弹出堆顶,加入 right(更新该组)
else:
# 不能接在任何组后面(会重叠)
直接加入 right(新开一组)
详细示例
示例
\[5,10\],\[6,8\],\[1,5\],\[2,3\],\[1,10\]
排序后
[1,5], [1,10], [2,3], [5,10], [6,8]
执行过程
| 步骤 | 处理区间 | 堆状态(处理前) | 判断 | 操作 | 堆状态(处理后) | 组数 |
|---|---|---|---|---|---|---|
| 1 | [1,5] | [] | - | 开新组,加入 5 | [5] | 1 |
| 2 | [1,10] | [5] | 5≥1 ✗ | 开新组,加入 10 | [5,10] | 2 |
| 3 | [2,3] | [5,10] | 5≥2 ✗ | 开新组,加入 3 | [3,5,10] | 3 |
| 4 | [5,10] | [3,5,10] | 3<5 ✓ | 弹出 3,加入 10 | [5,10,10] | 3 |
| 5 | [6,8] | [5,10,10] | 5<6 ✓ | 弹出 5,加入 8 | [8,10,10] | 3 |
复杂度分析
时间复杂度:O(n log n)
- 排序:O(n log n)
- 遍历:O(n)
- 堆操作:每次 O(log n),共 n 次 → O(n log n)
- 总计:O(n log n)
空间复杂度:O(n)
- 堆存储最多 n 个元素