Problem: 3165. 不包含相邻元素的子序列的最大和
做题历程
起初,我直接想到的也是dp做法,但是这需要$O(n^2)$的时间复杂度,最后做出来也的确超时了。
我便想,这个dp数组能否在不同查询中共用?
可是维护这个dp数组也需要$O(n)$的时间复杂度,这样做是白白浪费空间。我们需要的是一个至少能够以$O(logn)$的时间维护和查询不包含相邻元素的子序列的最大和的数据结构。
而看到$O(logn)$这个数量级,不出意料的话应该需要不断将输入(这里就是数组)二分成不同区间了。
除了时间复杂度,我们需要这个数据结构有什么样的性质呢?
这个时间通过dp思想的启发,我们可以想到,如果这个数据结构能够维护数组的区间信息,利用dp思想来求得组合这些区间后,我们就能够求出不包含相邻元素的子序列的最大和。
而这就是线段树。
线段树
线段树是算法竞赛中常用的用来维护 区间信息 的数据结构。
线段树可以在$O(logn)$的时间复杂度内实现单点修改、区间修改、区间查询(区间求和,求区间最大值,求区间最小值)等操作。OI Wiki。
我们递归地把数组二分成更小的区间,每一个节点代表一个区间,而其子节点代表其更小的两个子区间。可见线段树也是一个完全二叉树。
我们可以使用数组像存储堆一样来存储线段树。如果存储下标从1
开始,那么一个节点的左子节点为2*i
,右子节点为2*i+1
。
假设线段树有n
个叶子节点,即数组有n个元素,那么树的高度为$\lceil logn \rceil$,总的节点数量为$2^{\lceil logn \rceil + 1} - 1$,可以证明这个值是小于4n
的,在编程中可以直接用4n
代替。
线段树我们先关注其构建、维护、查询三个操作。
构建
我们采用递归的方式构建。给定区间左右端点,我们递归地构建左右子树,如果左右端点相等则停止递归;然后计算所需的区间信息,在本题中就是不包含相邻元素的子序列的最大和。在计算时我们可以利用到子节点的信息,因为我们可以以$O(1)$的时间访问左右子节点。建树的时间复杂度取决于计算区间信息的时间复杂度,由主方法可知,如果可以以$O(1)$的时间完成该操作,即$O(f) = O(1)$ ,那么为$O(logn)$;否则为$O(f)$。
维护
在更改了一个单点信息后,我们需要维护线段树性质。由于这个点的修改只会影响到其祖先节点,我们以二分的思想递归查找到这个点,然后再维护区间信息即可。时间复杂度同上。
查询
本题只查询最大区间,因此$O(1)$时间即可。
推广到更大的范围,如果我们要查找任意一个区间,那么这个区间有可能不是完全和线段树节点的范围一样。我们只需递归地计算其左右区间的值,并相加起来。最后当区间范围完全被查询范围覆盖时,递归结束,直接返回该节点的信息即可。时间复杂度为$O(logn)$。
思路
知道了线段树的数据结构,我们就只差一步之遥,即维护什么样的区间信息。我们发现如果需要用子区间的信息计算出父区间的信息,我们需要左右两个区间分别左右两个端点是否取值时的区间值,运用dp的思想求解。详见代码。其中0表示这个端点不能取到,1表示这个端点可取可不取。可见11可以涵盖所有情况。
复杂度
- 时间复杂度: $O(n + qlogn)$。
- 空间复杂度: $O(n)$,
n
为数组长度。
Code
1 | class Solution { |