在力扣每日一题做到的一系列题目,初次碰到的时候觉得颇有意思,遂记录。
零数组变换 Ⅰ
题面:给定一个长度为 n 的整数数组 nums 和一个二维数组 queries,其中 queries[i] = [li, ri]。
对于每个查询 queries[i]:
- 在
nums的下标范围[li, ri]内选择一个下标 子集。 - 将选中的每个下标对应的元素值减 1。
零数组 是指所有元素都等于 0 的数组。
如果在按顺序处理所有查询后,可以将 nums 转换为 零数组 ,则返回 true,否则返回 false。
题目意思应该不难懂,其实就是对一个数组进行一系列操作看它能不能所有元素全部归零,操作具体记录在一个二维数组,每一个内部的一维数组记录一段下标,范围内的执行一次减一操作。
暴力解法
第一反应是暴力,写出来代码如下:
1 | class Solution { |
假设 nums.size() = n,queries.size() = m,每个 query 最坏情况可能影响 O(n) 个元素,时间复杂度为O(m*n)。
太暴力了,直接干超时。
差分数组
于是乎我学到了一个叫差分数组的东西,这个东西简直就是为这道题量身定制的,用来高效处理数组区间修改的操作。其核心就是标记你要处理的数据区间的头部和尾部,通过前缀和来还原整个操作。光是看着这段描述就知道效率有多高了。
举个具体的例子吧:
初始数组:
1 | a = [0, 0, 0, 0, 0] |
操作:
- 对区间
[1, 3]加 2 - 对区间
[2, 4]加 1 - 定义一个差分数组为原数组的Size+1,然后在区间操作的left位置+i,right+1的位置-i。
差分数组变化:
1 | d[1] += 2 // => [0, 2, 0, 0, 0, 0] |
前缀和还原后:
1 | a = [0, 2, 3, 3, 1] |
回到我们这道题,操作只有加一和减一,因此也简单了很多,可能会对right有疑问,自己稍微想一下就马上明白原理了,差分数组是很方便多个操作叠加的。
上代码:
1 | class Solution { |
ps:力扣官方题解的源代码
题目其实不难,只是苯人未曾学过差分数组的概念,特此记录
零数组变换 Ⅱ
题面:给你一个长度为 n 的整数数组 nums 和一个二维数组 queries,其中 queries[i] = [li, ri, vali]。
每个 queries[i] 表示在 nums 上执行以下操作:
- 将
nums中[li, ri]范围内的每个下标对应元素的值 最多 减少vali。 - 每个下标的减少的数值可以独立选择。
零数组 是指所有元素都等于 0 的数组。
返回 k 可以取到的 最小非负 值,使得在 顺序 处理前 k 个查询后,nums 变成 零数组。如果不存在这样的 k,则返回 -1。
题目意思很好理解,其实就是在上题的基础上让你找到一个最小的操作数使得数组变为零数组,首先可以想到二分查找,在0到queries.size()的范围内去二分查找这个k,对于每个值都检查一遍是否零数组。检查的方式与上题类似,即前缀和检验。
差分数组
这里直接码出苯人的代码:
1 | class Solution { |
也是比较暴力的解法,时间复杂度在O((m+n)*log n),n是nums的长度,m是queries的长度,空间复杂度为O(n),所幸能过。
双指针
这里看到力扣题解有个很不错的解法,即顺序遍历 nums,每遇到一个值不足以被当前操作减为 0 的位置时,就继续执行 queries,直到能满足当前值为止。
其实就是从nums的元素逐一检查,找到能够归零的最小k值,k和差分数组是逐渐变大的,这样能够获得一个较好的时间复杂度。
1 | class Solution { |
复杂度分析
- 时间复杂度:O(m+n),其中 n 是
nums的长度,m 是queries的长度。 - 空间复杂度:O(n)。
零数组变换 Ⅲ
题面:给你一个长度为 n 的整数数组 nums 和一个二维数组 queries ,其中 queries[i] = [li, ri] 。
每一个 queries[i] 表示对于 nums 的以下操作:
- 将
nums中下标在范围[li, ri]之间的每一个元素 最多 减少 1 。 - 坐标范围内每一个元素减少的值相互 独立 。
零数组 指的是一个数组里所有元素都等于 0 。
请你返回 最多 可以从 queries 中删除多少个元素,使得 queries 中剩下的元素仍然能将 nums 变为一个 零数组 。如果无法将 nums 变为一个 零数组 ,返回 -1 。
题目开始看着有些复杂了,第一眼时贪心或者dp,但感觉有瑕疵,其实我总觉得贪心的性质经常不够严谨,但是就是能过。
这里我先用了很常规的思路:建立一个差分数组,然后和原数组比较,如果不能归零则返回-1,再写一个数组记录差分数组和原数组的差值,遍历queries,如果其中的l到r的位置都大于0,那么这个数组就是可以删除的,然后就把这个给删了。
但是没能ac,通过了708/824
原因在于这个顺序遍历,没有找到最优的删除顺序,比如一个queries等于另外两个之和,明明可以删两个但是只删了一个。这里的想法是优先找到最小的queries去删除,但是实际上会出现很多问题,比如这个组现在不能删但是之后能删,总之这个顺序敏感不好处理。
于是观察题解,题解用到的是贪心加优先队列,跟我的思路解法正好相反:
首先我们考虑 nums 中下标为 0 的元素,若 nums[0]>0,我们肯定要在queries中找到至少 nums[0] 个左端点为 0 的元素保留,这样才能让 nums[0] 降为 0。
那么选哪 nums[0] 个呢?贪心地考虑,是选右端点最大的那些。选完之后,我们考虑 nums[1]。前一步操作选出的部分元素,可能会有不包含下标 1 的,我们需要将它去掉。这一部分也可以用差分数组deltaArray来完成。此时累积的操作数可能不能将 nums[1] 降为 0,我们仍然要像上一步一样,选出部分 queries 的元素。
这个时候我们可以从未被挑选的元素中,选出左端点 ≤1 的部分中,选出右端点最大的一部分,直到操作数满足 nums[1] 可以降到 0。这个计算过程,可以由一个优先队列 heap 来完成。我们不停地遍历 nums 的过程中,将对应下标为左端点的 queries 的右端点放入 heap 中,并且当操作数不够时,从 heap 中不停取出最大的右端点,直到操作数满足要求。遍历完成后,heap 的长度就是可以删除的 queries。
代码如下:
1 | class Solution { |
复杂度分析
- 时间复杂度:O(n+m×log m),其中 n 是
nums的长度,m 是queries的长度。 - 空间复杂度:O(n+m)。