2569-更新数组后处理求和查询
给你两个下标从 0 开始的数组 nums1
和 nums2
,和一个二维数组 queries
表示一些操作。总共有 3 种类型的操作:
- 操作类型 1 为
queries[i] = [1, l, r]
。你需要将nums1
从下标l
到下标r
的所有0
反转成1
并且所有1
反转成0
。l
和r
下标都从 0 开始。 - 操作类型 2 为
queries[i] = [2, p, 0]
。对于0 <= i < n
中的所有下标,令nums2[i] = nums2[i] + nums1[i] * p
。 - 操作类型 3 为
queries[i] = [3, 0, 0]
。求nums2
中所有元素的和。
请你返回一个数组,包含所有第三种操作类型的答案。
示例 1:
**输入:** nums1 = [1,0,1], nums2 = [0,0,0], queries = [[1,1,1],[2,1,0],[3,0,0]]
**输出:** [3]
**解释:** 第一个操作后 nums1 变为 [1,1,1] 。第二个操作后,nums2 变成 [1,1,1] ,所以第三个操作的答案为 3 。所以返回 [3] 。
示例 2:
**输入:** nums1 = [1], nums2 = [5], queries = [[2,0,0],[3,0,0]]
**输出:** [5]
**解释:** 第一个操作后,nums2 保持不变为 [5] ,所以第二个操作的答案是 5 。所以返回 [5] 。
提示:
1 <= nums1.length,nums2.length <= 105
nums1.length = nums2.length
1 <= queries.length <= 105
queries[i].length = 3
0 <= l <= r <= nums1.length - 1
0 <= p <= 106
0 <= nums1[i] <= 1
0 <= nums2[i] <= 109
前言
线段树是一个二叉树,每个结点保存数组 nums 在区间 [s,e] 的最小值、最大值或者总和等信息。线段树可以用树也可以用数组(堆式存储)来实现。对于数组实现,假设根结点的下标为 1,如果一个结点在数组的下标为 node,那么它的左子结点下标为 node} \times 2,右子结点下标为 node} \times 2 + 1,线段树可以在 O(\log N) 的时间复杂度内实现单点修改、区间修改、区间查询(区间求和,求区间最大值,求区间最小值)等操作,关于线段树的详细描述可以参考「线段树 」。
区间更新的线段树,需要借助「懒惰标记」来标记当前结点所在区间是否需要更新。
建树 build 函数:
我们在结点 node 保存数组 nums 在区间 [s, e] 的总和。
s = e 时,结点 node 是叶子结点,它保存的值等于 nums}[s]。
s < e 时,结点 node 的左子结点保存区间 \Big [ s, \Big \lfloor \dfrac{s + e}{2} \Big \rfloor \Big ] 的总和,右子结点保存区间 \Big [ \Big \lfloor \dfrac{s + e}{2} \Big \rfloor + 1, e \Big ] 的总和,那么结点 node 保存的值等于它的两个子结点保存的值之和。
假设 nums 的大小为 n,我们规定根结点 node} = 1 保存区间 [0, n - 1] 的总和,然后自下而上递归地建树。
区间修改 modify 函数:
当我们要修改区间 nums}[\textit{left}\cdots{right}] 的值时,查看当前区间的结点此前是否已经「更新」过。如果更新过,那么我们通过将 pushdown 函数将更新标记传递到子结点,对之前的操作进行更新,同时更新每个结点的懒标记 lazytag,后续该位置便可以认为无需进行更新操作。
区间查询 query 函数
给定区间 [\textit{left}, \textit{right}] 时,也需要像区间更新操作一样,需要使用 pushdown 函数将更新标记往下传递到子结点,否则区间本身的数值实际上没有更新,懒标记只在区间修改或者区间查询时会往下传递,否则只是标记该区间需要更新。将区间 [\textit{left}, \textit{right}] 拆成多个结点对应的区间。
如果结点 node 对应的区间与 [\textit{left}, \textit{right}] 相同,可以直接返回该结点的值,即当前区间和。
如果结点 node 对应的区间与 [\textit{left}, \textit{right}] 不同,设左子结点对应的区间的右端点为 m,那么将区间 [\textit{left}, \textit{right}] 沿点 m 拆成两个区间,分别向下传递懒标记,并计算左子结点和右子结点。
从根结点开始递归地拆分区间 [\textit{left}, \textit{right}],变拆分边计算并返回最终结果即可。
方法一:线段树
思路与算法
本题目中含有三种操作:
第一种操作是将给定区间 [\textit{left}, \textit{right}] 内的所有数据进行反转,实际为是区间更新,此时我们可以利用线段树进行区间更新,此时需要用到「线段树的区间修改与懒惰标记 」。
第二种操作是唯一对 nums}_2 中的元素进行更新,此时 nums}’_2[i] = \textit{nums}_2[i] + \textit{nums}_1[i] \times p,设数组 nums}_2 更新之前的和为 sum,更新之后的和为 sum}’,计算过程如下:
\begin{aligned}
\textit{sum}’ &= \sum\limits_{i=0}^{n-1}{\textit{nums}’2[i]} \
&= \sum\limits{i=0}^{n-1}(\textit{nums}_2[i] + \textit{nums}1[i] \times p) \
&=\sum\limits{i=0}^{n-1}\textit{nums}2[i] + p \times \sum\limits{i=0}^{n-1}\textit{nums}1[i] \
&= \textit{sum} + p \times \sum\limits{i=0}^{n-1}\textit{nums}_1[i]
\end{aligned}根据上述等式可以看到,每次执行操作二时,实际 nums}_2 的和会加上 p 倍 nums}_1 的元素之和,可在每次更新时维护数组 nums}_2 的和。由于 nums}_1 初始化时全部为 0,经过第一种操作时部分元素会进行反转,因此只需用线段树维护区间内 1 的个数,每次进行区间查询即可得到数组 nums}_1 的元素之和。
第三种操作是求数组 nums}_2 的元素之和,此时返回操作二中维护的 nums}_2 的和即可。
根据以上分析,我们可以建立区间更新的线段树,可以参考「线段树的区间修改与懒惰标记 」,当遇到操作一时进行区间更新,遇到操作二时进行区间查询即可。
代码
1 | struct SegNode { |
1 | class Solution { |
1 | class Solution: |
1 | public class Solution { |
1 | typedef struct SegNode { |
1 | type SegNode struct { |
1 |
|
复杂度分析
时间复杂度:O(m \log n),其中 m 表示操作 1 与操作 2 的次数之和,n 表示数组的长度。每次遇到操作类型 1 与操作类型 2 时需要更新线段树,线段树每次更新与查询的时间复杂度均为 O(\log n),一共最多有 m 次操作,因此总的时间复杂度为 O(m \log n)。
空间复杂度:O(Cn),其中 n 表示数组的长度。本题解中线段树采用堆式存储,假设当前数组的长度为 n,由于线段树是一棵完全二叉树,此时该树的最大深度为 \lceil \log n \rceil,则其叶子结点的总数为 2^{\lceil \log n \rceil,该树中含有的结点总数为 2^{\lceil \log n \rceil + 1} - 1,此时可以知道 2^{\lceil \log n \rceil + 1} - 1 \le 2^{\log n + 2} - 1 \le 4n - 1,因此本题中取 C = 4 即可。