3362. 零数组变换 III
3362. 零数组变换 III
我们有一个长度为 n
的整数数组 nums
和一个二维数组 queries
,其中 queries[i] = [li, ri]
。每个 queries[i]
表示对 nums
的以下操作:
- 将
nums
中下标在范围[li, ri]
之间的每一个元素 最多 减少 1。- 这意味着对于
nums[j]
(其中li <= j <= ri
),可以选择减少 1 或保持不变。 - 不同元素可以减少的值是独立的。
- 这意味着对于
零数组 指的是所有元素都为 0 的数组。我们需要回答:
- 最多可以从
queries
中删除多少个元素,使得剩下的queries
仍然能将nums
变为零数组。 - 如果无法将
nums
变为零数组,返回 -1。
初步理解
首先,我们需要明确几个关键点:
-
操作的含义:每个查询
[l, r]
可以对区间[l, r]
内的元素进行最多减少 1 的操作。这意味着可以选择对某些或全部元素减少 1,也可以选择不减少任何元素。 -
目标:通过一系列这样的操作,最终将所有
nums
的元素减到 0。我们需要选择queries
的一个子集(即删除一些查询),使得剩下的查询可以完成这个目标,并且希望删除的查询尽可能多(即剩下的查询尽可能少)。 -
最大化删除的查询数量:等价于最小化剩下的查询数量。因此,我们需要找到一个最小的查询集合,能够覆盖所有
nums
的减少需求。
问题转化
这个问题可以转化为一个覆盖问题:
- 每个
nums[i]
需要被减少nums[i]
次。每次减少可以通过一个查询[l, r]
覆盖i
(如果l <= i <= r
)。 - 我们需要选择一组查询,使得对于每个
i
,至少有nums[i]
个查询覆盖i
。 - 目标是选择尽可能少的查询(即删除尽可能多的查询)。
这类似于集合覆盖问题,其中我们需要选择最少数量的集合(查询)来覆盖所有元素的需求。
贪心算法的思考
为了最小化剩下的查询数量,可以考虑贪心算法:
-
优先选择覆盖最多“需求”的查询:即选择那些能够覆盖最多尚未被满足的
nums[i]
的查询。 -
具体步骤:
- 初始化一个数组
required
,其中required[i] = nums[i]
,表示nums[i]
还需要被减少的次数。 - 对于每个查询
[l, r]
,计算它能够覆盖的i
中required[i] > 0
的数量。选择能够覆盖最多这样的i
的查询。 - 应用这个查询:对于
i
在[l, r]
且required[i] > 0
,将required[i]
减 1。 - 重复这个过程,直到所有
required[i]
都为 0 或无法继续减少。
- 初始化一个数组
可能的算法步骤
- 初始化
required = nums.copy()
。 - 初始化
selected_queries = []
。 - 当
required
不全为 0:- 对于每个查询
[l, r]
,计算其可以覆盖的i
中required[i] > 0
的数量(即sum(1 for i in range(l, r+1) if required[i] > 0)
)。 - 选择能够覆盖最多这样的
i
的查询(贪心选择)。 - 如果没有查询可以覆盖任何
required[i] > 0
的i
,返回 -1。 - 将选中的查询加入
selected_queries
,并对i
在[l, r]
且required[i] > 0
的i
,将required[i] -= 1
。 - 从
queries
中移除该查询(或标记为已使用)。
- 对于每个查询
- 返回
len(queries) - len(selected_queries)
(即删除的查询数量)。
示例验证
让我们通过一个简单的例子来验证这个算法:
例子 1:
nums = [1, 2, 1]
queries = [[0, 1], [1, 2], [0, 2]]
初始化 required = [1, 2, 1]
。
-
计算每个查询的覆盖:
[0,1]
: coversrequired[0]=1
,required[1]=2
→ count=2[1,2]
: coversrequired[1]=2
,required[2]=1
→ count=2[0,2]
: covers all → count=3- 选择
[0,2]
。 - 应用
[0,2]
:required
becomes[0, 1, 0]
。 selected_queries = [[0,2]]
。
-
required = [0, 1, 0]
:[0,1]
: coversrequired[1]=1
→ count=1[1,2]
: coversrequired[1]=1
→ count=1- 选择
[0,1]
或[1,2]
。 - 选择
[0,1]
:required
becomes[0, 0, 0]
。selected_queries = [[0,2], [0,1]]
。
所有 required
为 0,删除的查询数量 = 3 - 2 = 1
。
但这不是最优的,因为我们可以只选择 [0,1]
和 [1,2]
:
[0,1]
:required
->[0,1,1]
[1,2]
:required
->[0,0,0]
selected_queries = [[0,1], [1,2]]
,删除[0,2]
,也是删除 1。
看起来两种方式删除数量相同。但初始 nums
总和是 4,每次操作最多减少 2(两个元素),所以至少需要 2 次操作。
例子 2:
nums = [3, 0, 0]
queries = [[0,0], [0,0], [0,0]]
需要 nums[0]
被减少 3 次,只能通过 [0,0]
操作。需要至少 3 次 [0,0]
操作。
所以不能删除任何查询,删除数量 = 0。
更优的贪心策略
前面的贪心策略可能在选择查询时不够高效。可能需要更聪明的贪心选择:
- 按查询的右端点排序,然后从左到右处理
nums
,尽可能选择覆盖当前nums[i]
的最右边的查询。
类似于区间覆盖问题中的经典贪心算法。
正确的贪心算法
更准确的贪心策略:
- 将
queries
按照右端点升序排序(或按左端点降序)。 - 初始化
required = nums.copy()
。 - 初始化
selected = 0
。 - 使用一个差分数组或线段树来高效地进行区间操作。
- 对于每个
i
从 0 到 n-1:- 如果
required[i] > 0
:- 选择覆盖
i
的最右边的查询[l, r]
且l <= i <= r
。 - 应用该查询
required[i]
次(即选择该查询required[i]
次)。 - 对于
j
in[l, r]
,required[j] -= required[i]
。 selected += required[i]
。- 如果无法找到覆盖
i
的查询,返回 -1。
- 选择覆盖
- 如果
- 返回
len(queries) - selected
。
示例验证
例子 1:
nums = [1, 2, 1]
, queries = [[0,1], [1,2], [0,2]]
排序 queries
按右端点:[[0,1], [1,2], [0,2]]
(已经按右端点升序)。
初始化 required = [1, 2, 1]
.
i=0
:required[0]=1 > 0
:- 覆盖
0
的最右边的查询:[0,2]
(右端点最大)。 - 应用
[0,2]
1 次:required
becomes[0, 1, 0]
.selected = 1
.
- 覆盖
i=1
:required[1]=1 > 0
:- 覆盖
1
的最右边的查询:[1,2]
。 - 应用
[1,2]
1 次:required
becomes[0, 0, -1]
(但required[2]
已经是 0)。selected = 2
.
- 覆盖
i=2
:required[2]=0
,跳过。
selected = 2
,删除 3 - 2 = 1
。
实现细节
为了实现这个贪心策略,我们需要:
- 对
queries
按右端点升序排序。 - 对于每个
i
,找到覆盖i
的最右边的查询(即l <= i
的最大r
)。- 可以使用优先队列(最大堆)来维护当前可以覆盖
i
的查询。
- 可以使用优先队列(最大堆)来维护当前可以覆盖
- 使用差分数组或线段树来高效地进行区间减操作。
伪代码
def max_queries_to_delete(nums, queries):n = len(nums)required = nums.copy()queries_sorted = sorted(queries, key=lambda x: x[1]) # 按右端点升序heap = []ptr = 0selected = 0for i in range(n):# 将所有 l <= i 的查询加入堆(按 -l 最大堆)while ptr < len(queries_sorted) and queries_sorted[ptr][1] >= i:l, r = queries_sorted[ptr]heapq.heappush(heap, (-l, r))ptr += 1# 选择 l <= i <= r 的最大的 lwhile heap and required[i] > 0:neg_l, r = heapq.heappop(heap)l = -neg_lif l > i:continue # 不覆盖 i# 可以应用这个查询 min(required[i], ...) 次delta = min(required[i], ...)# 需要区间 [l, r] 的 required 都减 delta# 这里需要高效区间减,可能需要差分数组# 假设可以高效进行区间减for j in range(l, r + 1):required[j] -= deltaselected += deltaif any(r > 0 for r in required):return -1return len(queries) - selected
使用差分数组
为了高效进行区间减操作,可以使用差分数组:
- 初始化
diff = [0] * (n + 1)
。 required[i] = nums[i] + diff[i]
(通过差分数组表示)。- 区间
[l, r]
减delta
:diff[l] -= delta
,diff[r+1] += delta
。 - 计算
required[i]
时,required[i] = nums[i] + prefix_sum(diff[0..i])
。
完整算法
结合贪心选择和差分数组:
- 按右端点升序排序
queries
。 - 使用最大堆(按
l
降序)维护当前可覆盖i
的查询。 - 使用差分数组进行区间减操作。
- 对于每个
i
,选择覆盖i
的最左边的查询(最大l
),并应用足够的次数。
最终答案
经过以上分析,以下是解决问题的步骤:
- 将
queries
按右端点升序排序。 - 使用最大堆(按
l
降序)维护当前可覆盖i
的查询。 - 初始化
required = nums.copy()
和差分数组diff = [0] * (n + 1)
。 - 对于每个
i
从 0 到 n-1:- 计算当前
required[i]
=nums[i]
+sum(diff[0..i])
。 - 将所有
l <= i
的查询[l, r]
加入堆(按-l
)。 - 当
required[i] > 0
:- 弹出堆顶查询
[l, r]
(最大l
)。 - 应用
min(required[i], ...)
次:delta = min(required[i], ...)
。diff[l] -= delta
,diff[r+1] += delta
。selected += delta
。
- 弹出堆顶查询
- 计算当前
- 检查所有
required[i]
是否为 0。 - 返回
len(queries) - selected
。
代码实现
import heapqdef max_queries_to_delete(nums, queries):n = len(nums)queries_sorted = sorted(queries, key=lambda x: x[1]) # 按右端点升序heap = []ptr = 0selected = 0diff = [0] * (n + 1)for i in range(n):# 更新 required[i]if i > 0:diff[i] += diff[i-1]required_i = nums[i] + diff[i]# 添加所有 l <= i 的查询到堆while ptr < len(queries_sorted) and queries_sorted[ptr][1] >= i:l, r = queries_sorted[ptr]heapq.heappush(heap, (-l, r))ptr += 1# 应用查询直到 required_i <= 0while required_i > 0 and heap:neg_l, r = heapq.heappop(heap)l = -neg_lif l > i:continue # 不覆盖 i# 可以应用这个查询最多 required_i 次delta = required_i# 区间 [l, r] 减 deltadiff[l] -= deltaif r + 1 < n:diff[r+1] += deltaselected += delta# 更新 required_irequired_i = nums[i] + (diff[i] if i == 0 else diff[i] + diff[i-1])# 检查所有 required[i] <= 0total = 0for i in range(n):total += diff[i]if nums[i] + total > 0:return -1return len(queries) - selected
复杂度分析
- 排序
queries
:O(m log m),其中 m 是queries
的长度。 - 堆操作:每个查询最多被加入和弹出一次,O(m log m)。
- 差分数组操作:O(n + m)。
- 总体复杂度:O(m log m + n)。
示例验证
例子 1:
nums = [1, 2, 1]
, queries = [[0,1], [1,2], [0,2]]
排序 queries
: [[0,1], [1,2], [0,2]]
(按右端点升序)。
i=0
:required[0] = 1
:- 堆:
[(-0,1), (-0,2)]
。 - 选择
[0,2]
,delta=1
:diff[0] = -1
,diff[3] = 1
。selected = 1
。required[0] = 1 + (-1) = 0
。
- 堆:
i=1
:diff[1] += diff[0] = -1
:required[1] = 2 + (-1) = 1
:- 堆:
[(-1,2)]
。 - 选择
[1,2]
,delta=1
:diff[1] = -1 -1 = -2
,diff[3] = 1 +1 = 2
。selected = 2
。required[1] = 2 + (-2) = 0
。
- 堆:
i=2
:diff[2] += diff[1] = -2
:required[2] = 1 + (-2) = -1
(<=0)。
- 检查
required
:i=0
:1 + (-1) = 0
。i=1
:2 + (-2) = 0
。i=2
:1 + (-2) = -1
。
- 返回
3 - 2 = 1
。
例子 2:
nums = [3, 0, 0]
, queries = [[0,0], [0,0], [0,0]]
排序 queries
: [[0,0], [0,0], [0,0]]
。
i=0
:required[0] = 3
:- 堆:
[(-0,0), (-0,0), (-0,0)]
。 - 选择
[0,0]
,delta=3
:- 但每个查询只能应用一次(因为最多减少 1),所以需要应用 3 次
[0,0]
。 selected = 3
。required[0] = 0
。
- 但每个查询只能应用一次(因为最多减少 1),所以需要应用 3 次
- 堆:
- 返回
3 - 3 = 0
。
修正贪心策略
之前的实现中,假设一个查询可以应用多次(如 delta
次),但实际上每个查询只能应用一次(最多减少 1)。因此,需要调整:
- 每个查询
[l, r]
只能应用一次(即最多减少 1 的区间操作)。 - 因此,
required[i]
需要被至少nums[i]
个不同的查询覆盖。
因此,问题转化为:
- 对于每个
nums[i]
,需要至少nums[i]
个查询覆盖i
。 - 选择最少数量的查询满足所有
nums[i]
的覆盖需求。
这类似于多覆盖问题(multi-set cover),可以使用贪心算法:
- 按右端点升序排序
queries
。 - 对于每个
i
,选择覆盖i
的最右边的查询,尽可能覆盖更多的i
。 - 使用差分数组或线段树来跟踪每个
i
还需要被覆盖的次数。
修正后的算法
import heapqdef max_queries_to_delete(nums, queries):n = len(nums)queries_sorted = sorted(queries, key=lambda x: x[1]) # 按右端点升序heap = []ptr = 0selected = 0required = nums.copy()for i in range(n):# 添加所有 l <= i 的查询到堆while ptr < len(queries_sorted) and queries_sorted[ptr][1] >= i:l, r = queries_sorted[ptr]heapq.heappush(heap, (-l, r)) # 最大堆按 lptr += 1# 应用查询直到 required[i] <= 0while required[i] > 0 and heap:neg_l, r = heapq.heappop(heap)l = -neg_lif l > i:continue # 不覆盖 i# 应用这个查询一次selected += 1# 区间 [l, r] 的 required 减 1for j in range(l, r + 1):required[j] -= 1if any(r > 0 for r in required):return -1return len(queries) - selected
复杂度分析
- 排序:O(m log m)。
- 堆操作:O(m log m)。
- 区间减操作:最坏 O(n m)(如果每个查询覆盖整个数组)。
- 需要优化区间减操作。
使用差分数组优化
import heapqdef max_queries_to_delete(nums, queries):n = len(nums)queries_sorted = sorted(queries, key=lambda x: x[1]) # 按右端点升序heap = []ptr = 0selected = 0required = nums.copy()diff = [0] * (n + 1)for i in range(n):# 更新 required[i]if i > 0:diff[i] += diff[i-1]required_i = required[i] + diff[i]# 添加所有 l <= i 的查询到堆while ptr < len(queries_sorted) and queries_sorted[ptr][1] >= i:l, r = queries_sorted[ptr]heapq.heappush(heap, (-l, r)) # 最大堆按 lptr += 1# 应用查询直到 required_i <= 0while required_i > 0 and heap:neg_l, r = heapq.heappop(heap)l = -neg_lif l > i:continue # 不覆盖 i# 应用这个查询一次selected += 1# 区间 [l, r] 的 required 减 1diff[l] -= 1if r + 1 < n:diff[r+1] += 1# 更新 required_irequired_i += (diff[i] - (diff[i-1] if i > 0 else 0))# 检查所有 required[i] <= 0total = 0for i in range(n):total += diff[i]if nums[i] + total > 0:return -1return len(queries) - selected
最终代码
结合以上思路,以下是正确的实现:
import heapqdef max_queries_to_delete(nums, queries):n = len(nums)queries_sorted = sorted(queries, key=lambda x: x[1]) # 按右端点升序heap = []ptr = 0selected = 0required = nums.copy()for i in range(n):# 添加所有 l <= i 的查询到堆while ptr < len(queries_sorted) and queries_sorted[ptr][1] >= i:l, r = queries_sorted[ptr]heapq.heappush(heap, (-l, r)) # 最大堆按 lptr += 1# 应用查询直到 required[i] <= 0while required[i] > 0 and heap:neg_l, r = heapq.heappop(heap)l = -neg_lif l > i:continue # 不覆盖 i# 应用这个查询一次selected += 1# 区间 [l, r] 的 required 减 1for j in range(l, r + 1):required[j] -= 1if any(r > 0 for r in required):return -1return len(queries) - selected
复杂度优化
为了优化区间减操作,可以使用差分数组:
import heapqdef max_queries_to_delete(nums, queries):n = len(nums)queries_sorted = sorted(queries, key=lambda x: x[1]) # 按右端点升序heap = []ptr = 0selected = 0required = nums.copy()diff = [0] * (n + 1)for i in range(n):# 更新 required[i]if i > 0:diff[i] += diff[i-1]required_i = required[i] + diff[i]# 添加所有 l <= i 的查询到堆while ptr < len(queries_sorted) and queries_sorted[ptr][1] >= i:l, r = queries_sorted[ptr]heapq.heappush(heap, (-l, r)) # 最大堆按 lptr += 1# 应用查询直到 required_i <= 0while required_i > 0 and heap:neg_l, r = heapq.heappop(heap)l = -neg_lif l > i:continue # 不覆盖 i# 应用这个查询一次selected += 1# 区间 [l, r] 的 required 减 1diff[l] -= 1if r + 1 < n:diff[r+1] += 1# 更新 required_irequired_i -= 1# 检查所有 required[i] <= 0total = 0for i in range(n):total += diff[i]if nums[i] + total > 0:return -1return len(queries) - selected
示例验证
例子 1:
nums = [1, 2, 1]
, queries = [[0,1], [1,2], [0,2]]
排序 queries
: [[0,1], [1,2], [0,2]]
.
i=0
:required[0] = 1
:- 堆:
[(-0,1), (-0,2)]
。 - 选择
[0,2]
:diff[0] = -1
,diff[3] = 1
。selected = 1
。required[0] = 1 + (-1) = 0
。
- 堆:
i=1
:diff[1] += diff[0] = -1
:required[1] = 2 + (-1) = 1
:- 堆:
[(-1,2)]
。 - 选择
[1,2]
:diff[1] = -1 -1 = -2
,diff[3] = 1 +1 = 2
。selected = 2
。required[1] = 2 + (-2) = 0
。
- 堆:
i=2
:diff[2] += diff[1] = -2
:required[2] = 1 + (-2) = -1
。
- 检查
required
:i=0
:1 + (-1) = 0
。i=1
:2 + (-2) = 0
。i=2
:1 + (-2) = -1
。
- 返回
3 - 2 = 1
。
结论
通过贪心算法和差分数组优化,可以高效地解决这个问题。最终的最多删除查询数量为 len(queries) - selected
,其中 selected
是最小需要保留的查询数量。如果无法满足所有 nums[i]
的减少需求,则返回 -1。