标签:遍历 重复 序列 说明 初始化 线性 处理过程 连续 初始
问题:给定一个源区间[x, y]和N个无序的目标区间[x1, y1] [x2, y2] ... [xn, yn],判断源区间是不是在目标区间内(即源区间与[目标区间的并集]是否相交)。
这道题我是少有的不看答案就能把所有解法思考出来的,给了我的笨脑袋一点信心。
简单叙述下两个解法:
解法一:从源区间依次减去目标区间,到最后如果源区间为空,则说明在目标区间内。这种方法在处理过程中,计算过程中源区间变成了一个数组,因为每次减法可能会将其一分为二。每次减法计算哪些源区间被覆盖需要O(logN)的时间复杂度(即使有多个被覆盖也成立,因为我们要找的是一个连续的源区间,因此分别对新区间的left和right进行二分就能达到目的),但是更新这个数组需要O(N),毕竟是数组,牵一发而动全身。因此整体时间是O(N^2)。
解法二:将目标区间预处理,按left从小到大排序(O(N*logN)),然后将连续的有重叠的合并(O(N)),然后查询源区间是否属于这些处理后的任一个区间(二分,O(logN))。整体时间O(N*logN)。
解法一在中间过程需要存储源区间序列,一般来说无非数组或者链表的形式。如果采用链表,更新只需要O(1),但是不能随机访问,意味着不能用二分法进行覆盖查询了,因此每一步还是得O(N).
有没有办法结合数组和链表的优势呢?我想到了N年前学过的一个东西:线段树。
源区间初始为这个二叉树的根节点,表示一条线段。每次减去一个目标空间,可能将这个线段直接左截短,右截短,或者分成两个子线段,也就是两棵子树。在有子树的情况下,减法需要递归地向下进行,可能产生新的结点,也可能删除结点。最后判断这棵树是否为空即可。
比如源区间[0,10],目标区间[4, 6], [2, 8], [1,10]。
初始化:
. └── 0,10
减去[4,6]:
. └── 0,10 ├── 0,4 └── 6,10
减去[2,8]:
. └── 0,10 ├── 0,2 └── 8,10
减去[1,10]:
. └── 0,10 └── 0,1
直觉认为每一次减法的时间复杂度是O(logN),但是如何证明呢?
首先,N个结点的平均树深度是logN,如果减去一个区间始终都是线性地向下遍历这棵树,那么结论是成立的。对于左截短和右截短的操作,就满足这样的遍历方式。但是还有一种,例如上面的减去[2,8]这一步,会分成左右两棵子树遍历,就不是线性的了。其实这样的分化至多一次,因为分化之后就简化为左截短和右截短了。因此整体上每一步减法的时间复杂度还是O(logN)。
这么一来,时间复杂度优化为解法二的O(N*logN)了。
即使解法一做了上述优化,解法二的空间上也更省。另外一点就是,实际应用中可能不止查询一个源区间,可能是多个。对于这样的应用,解法二无疑更合适,因为每次查询只需要logN的时间。
解法二之所以好,是因为对静态数据进行了预处理,建立了一个静态模型,这样对于动态的查询,能够避免很多重复计算。这让我想到了大数据中的流式处理。
不过如果问题反过来,源区间是静态的,而目标区间是动态的呢?比如目标区间会不断的给出,每次更新目标区间都要进行查询。这样每次都要更新模型,不管怎么优化,时间复杂度都会提高到O(N)。
此时如果用解法一就会更快,甚至可以支持删除目标区间这样的操作,相当于是在线段树上做加法,和减法的时间复杂度一样,只是可能会在树的中间而不是叶子处添加结点。
标签:遍历 重复 序列 说明 初始化 线性 处理过程 连续 初始
原文地址:https://www.cnblogs.com/xrst/p/13289638.html