【算法提升班】《个人日程安排表》系列

《个人日程安排表》截止目前(2020-02-03)在 LeetCode 上一共有三道题,其中两个中等难度,一个困难难度,分别是:node

另外 LeetCode 上有一个相似的系列《会议室》,截止目前(2020-02-03)有两道题目。其中一个简单一个中等,分别是:python

今天咱们就来攻克它们。segmentfault

729. 个人日程安排表 I

题目地址

https://leetcode-cn.com/probl...数组

题目描述

实现一个 MyCalendar 类来存放你的日程安排。若是要添加的时间内没有其余安排,则能够存储这个新的日程安排。数据结构

MyCalendar 有一个 book(int start, int end)方法。它意味着在 start 到 end 时间内增长一个日程安排,注意,这里的时间是半开区间,即 [start, end), 实数  x 的范围为,  start <= x < end。app

当两个日程安排有一些时间上的交叉时(例如两个日程安排都在同一时间内),就会产生重复预订。函数

每次调用 MyCalendar.book 方法时,若是能够将日程安排成功添加到日历中而不会致使重复预订,返回 true。不然,返回 false  而且不要将该日程安排添加到日历中。测试

请按照如下步骤调用 MyCalendar 类: MyCalendar cal = new MyCalendar(); MyCalendar.book(start, end)优化

示例 1:spa

MyCalendar();
MyCalendar.book(10, 20); // returns true
MyCalendar.book(15, 25); // returns false
MyCalendar.book(20, 30); // returns true
解释:
第一个日程安排能够添加到日历中. 第二个日程安排不能添加到日历中,由于时间 15 已经被第一个日程安排预约了。
第三个日程安排能够添加到日历中,由于第一个日程安排并不包含时间 20 。
说明:

每一个测试用例,调用  MyCalendar.book  函数最多不超过  100 次。
调用函数  MyCalendar.book(start, end)时, start 和  end 的取值范围为  [0, 10^9]。

暴力法

思路

首先咱们考虑暴力法。每插入一个元素咱们都判断其是否和已有的全部课程重叠。

咱们定一个函数intersected(calendar, calendars),其中 calendar 是即将要插入的课程,calendars 是已经插入的课程。 只要 calendar 和 calendars 中的任何一个课程有交叉,咱们就返回 True,不然返回 False。

对于两个 calendar,咱们的判断逻辑都是同样的。假设连个 calendar 分别是[s1, e1][s2, e2]。那么若是s1 >= e2 or s2 <= e1, 则两个课程没有交叉,能够预约,不然不能够。如图,1,2,3 能够预约,剩下的不能够。

image.png

代码是这样的:

def intersected(calendar, calendars):
        for [start, end] in calendars:
            if calendar[0] >= end or calendar[1] <= start:
                continue
            else:
                return True

        return False

复杂度分析:

  • 时间复杂度:$O(N^2)$。N 指的是平常安排的数量,对于每一个新的平常安排,咱们检查新的平常安排是否发生冲突来决定是否能够预订新的平常安排。
  • 空间复杂度: $O(N)$。

这个代码写出来以后总体代码就呼之欲出了,所有代码见下方代码部分。

代码

代码支持 Python3:

Python3 Code:

#
# @lc app=leetcode.cn id=729 lang=python3
#
# [729] 个人日程安排表 I
#

# @lc code=start


class MyCalendar:

    def __init__(self):
        self.calendars = []

    def book(self, start: int, end: int) -> bool:
        def intersected(calendar, calendars):
            for [start, end] in calendars:
                if calendar[0] >= end or calendar[1] <= start:
                    continue
                else:
                    return True

            return False
        if intersected([start, end], self.calendars):
            return False
        self.calendars.append([start, end])
        return True

        # Your MyCalendar object will be instantiated and called as such:
        # obj = MyCalendar()
        # param_1 = obj.book(start,end)
        # @lc code=end

实际上咱们还能够换个角度,上面的思路判断交叉部分咱们考虑的是“如何不交叉”,剩下的就是交叉。咱们也能够直接考虑交叉。仍是上面的例子,若是两个课程交叉,那么必定知足s1 < e2 and e1 > s2。基于此,咱们写出下面的代码。

代码支持 Python3:

Python3 Code:

#
# @lc app=leetcode.cn id=729 lang=python3
#
# [729] 个人日程安排表 I
#

# @lc code=start


class MyCalendar:

    def __init__(self):
        self.calendars = []

    def book(self, start: int, end: int) -> bool:
        for s, e in self.calendars:
            if start < e and end > s:
                return False
        self.calendars.append([start, end])
        return True

        # Your MyCalendar object will be instantiated and called as such:
        # obj = MyCalendar()
        # param_1 = obj.book(start,end)
        # @lc code=end

二叉查找树法

思路

和上面思路相似,只不过咱们每次都对 calendars 进行排序,那么咱们能够经过二分查找日程安排的状况来检查新平常安排是否能够预订。若是每次插入以前都进行一次排序,那么时间复杂度会很高。如图,咱们的[s1,e1], [s2,e2], [s3,e3] 是按照时间顺序排好的日程安排。咱们如今要插入[s,e],咱们使用二分查找,找到要插入的位置,而后和插入位置的课程进行一次比对便可,这部分的时间复杂度是 O(logN)&dollar;。

image.png

咱们考虑使用平衡二叉树来维护这种动态的变化,在最差的状况时间复杂度会退化到上述的$O(N^2)$,平均状况是$O(NlogN)$,其中 N 是已预订的平常安排数。

image.png

代码

代码支持 Python3:

Python3 Code:

class Node:
    def __init__(self, start, end):
        self.start = start
        self.end = end
        self.left = self.right = None

    def insert(self, node):
        if node.start >= self.end:
            if not self.right:
                self.right = node
                return True
            return self.right.insert(node)
        elif node.end <= self.start:
            if not self.left:
                self.left = node
                return True
            return self.left.insert(node)
        else:
            return False

class MyCalendar(object):
    def __init__(self):
        self.root = None

    def book(self, start, end):
        if self.root is None:
            self.root = Node(start, end)
            return True
        return self.root.insert(Node(start, end))

731. 个人日程安排表 II

题目地址

https://leetcode-cn.com/probl...

题目描述

实现一个 MyCalendar 类来存放你的日程安排。若是要添加的时间内不会致使三重预订时,则能够存储这个新的日程安排。

MyCalendar 有一个 book(int start, int end)方法。它意味着在 start 到 end 时间内增长一个日程安排,注意,这里的时间是半开区间,即 [start, end), 实数  x 的范围为,  start <= x < end。

当三个日程安排有一些时间上的交叉时(例如三个日程安排都在同一时间内),就会产生三重预订。

每次调用 MyCalendar.book 方法时,若是能够将日程安排成功添加到日历中而不会致使三重预订,返回 true。不然,返回 false 而且不要将该日程安排添加到日历中。

请按照如下步骤调用 MyCalendar 类: MyCalendar cal = new MyCalendar(); MyCalendar.book(start, end)

示例:

MyCalendar();
MyCalendar.book(10, 20); // returns true
MyCalendar.book(50, 60); // returns true
MyCalendar.book(10, 40); // returns true
MyCalendar.book(5, 15); // returns false
MyCalendar.book(5, 10); // returns true
MyCalendar.book(25, 55); // returns true
解释:
前两个日程安排能够添加至日历中。 第三个日程安排会致使双重预订,但能够添加至日历中。
第四个日程安排活动(5,15)不能添加至日历中,由于它会致使三重预订。
第五个日程安排(5,10)能够添加至日历中,由于它未使用已经双重预订的时间 10。
第六个日程安排(25,55)能够添加至日历中,由于时间 [25,40] 将和第三个日程安排双重预订;
时间 [40,50] 将单独预订,时间 [50,55)将和第二个日程安排双重预订。

提示:

每一个测试用例,调用  MyCalendar.book  函数最多不超过  1000 次。
调用函数  MyCalendar.book(start, end)时, start 和  end 的取值范围为  [0, 10^9]。

暴力法

思路

暴力法和上述思路相似。可是咱们多维护一个数组 intersectedCalendars 用来存储二次预约的日程安排。若是课程第一次冲突,咱们将其加入 intersectedCalendars,若是和 intersectedCalendars 也冲突了,说明出现了三次预约,咱们直接返回 False。

代码

代码支持 Python3:

Python3 Code:

class MyCalendarTwo:

    def __init__(self):
        self.calendars = []
        self.intersectedCalendars = []

    def book(self, start: int, end: int) -> bool:
        for [s, e] in self.intersectedCalendars:
            if start < e and end > s:
                return False
        for [s, e] in self.calendars:
            if start < e and end > s:
                self.intersectedCalendars.append([max(start, s), min(end, e)])
        self.calendars.append([start, end])
        return True

二叉查找树法

和上面的题目相似,咱们仍然可使用平衡二叉树来简化查找逻辑。具体能够参考这个 discussion

每次插入以前咱们都须要进行一次判断,判断是否能够插入。若是不能够插入,直接返回 False,不然咱们进行一次插入。 插入的时候,若是和已有的相交了,咱们判断是否以前已经相交了一次,若是是返回 False,不然返回 True。关于如何判断是否和已有的相交,咱们能够在 node 节点增长一个字段的方式来标记,在这里咱们使用 single_overlap,True 表示产生了二次预约,False 则表示没有产生过两次及以上的预约。

代码

代码支持 Python3:

Python3 Code:

class Node:
    def __init__(self, start, end):
        self.start = start
        self.end = end
        self.left = None
        self.right = None
        self.single_overlap = False

class MyCalendarTwo:

    def __init__(self):
        self.root = None

    def book(self, start, end):
        if not self.canInsert(start, end, self.root):
            return False

        self.root = self.insert(start, end, self.root)
        return True


    def canInsert(self, start, end, root):
        if not root:
            return True

        if start >= end:
            return True

        if end <= root.start:
            return self.canInsert(start, end, root.left)

        elif start >= root.end:
            return self.canInsert(start, end, root.right)

        else:
            if root.single_overlap:
                return False
            elif start >= root.start and end <= root.end:
                return True
            else:
                return self.canInsert(start, root.start, root.left) and self.canInsert(root.end, end, root.right)



    def insert(self, start, end, root):
        if not root:
            root = Node(start, end)
            return root

        if start >= end:
            return root

        if start >= root.end:
            root.right = self.insert(start, end, root.right)

        elif end <= root.start:
            root.left = self.insert(start, end, root.left)

        else:
            root.single_overlap = True
            a = min(root.start, start)
            b = max(root.start, start)
            c = min(root.end, end)
            d = max(root.end, end)
            root.start, root.end = b, c
            root.left, root.right = self.insert(a, b, root.left), self.insert(c, d, root.right)

        return root

# Your MyCalendarTwo object will be instantiated and called as such:
# obj = MyCalendarTwo()
# param_1 = obj.book(start,end)

732. 个人日程安排表 III

题目地址

https://leetcode-cn.com/probl...

题目描述

实现一个 MyCalendar 类来存放你的日程安排,你能够一直添加新的日程安排。

MyCalendar 有一个 book(int start, int end)方法。它意味着在 start 到 end 时间内增长一个日程安排,注意,这里的时间是半开区间,即 [start, end), 实数  x 的范围为,  start <= x < end。

当 K 个日程安排有一些时间上的交叉时(例如 K 个日程安排都在同一时间内),就会产生 K 次预订。

每次调用 MyCalendar.book 方法时,返回一个整数 K ,表示最大的 K 次预订。

请按照如下步骤调用 MyCalendar 类: MyCalendar cal = new MyCalendar(); MyCalendar.book(start, end)

示例 1:

MyCalendarThree();
MyCalendarThree.book(10, 20); // returns 1
MyCalendarThree.book(50, 60); // returns 1
MyCalendarThree.book(10, 40); // returns 2
MyCalendarThree.book(5, 15); // returns 3
MyCalendarThree.book(5, 10); // returns 3
MyCalendarThree.book(25, 55); // returns 3
解释:
前两个日程安排能够预订而且不相交,因此最大的 K 次预订是 1。
第三个日程安排[10,40]与第一个日程安排相交,最高的 K 次预订为 2。
其他的日程安排的最高 K 次预订仅为 3。
请注意,最后一第二天程安排可能会致使局部最高 K 次预订为 2,但答案仍然是 3,缘由是从开始到最后,时间[10,20],[10,40]和[5,15]仍然会致使 3 次预订。
说明:

每一个测试用例,调用  MyCalendar.book  函数最多不超过  400 次。
调用函数  MyCalendar.book(start, end)时, start 和  end 的取值范围为  [0, 10^9]。

二叉查找树法

思路

咱们仍然可使用上述的平衡二叉树的作法。只不过咱们须要额外维护一个全局的最大值“k”,表示须要多少个预约。最终咱们返回 k。 同时每个 node 咱们都增长一个属性 k,用来表示局部的最大值,对于每次插入,咱们将 node 的 k 和所有的 k 进行比较,取出最大值便可。

代码

代码支持 Python3:

Python3 Code:

class Node(object):
    def __init__(self, start, end, ktime=1):
        self.k = ktime
        self.s = start
        self.e = end
        self.right = None
        self.left = None

class MyCalendarThree(object):

    def __init__(self):
        self.root = None
        self.k = 0

    def book(self, start, end):
        self.root = self.insert(self.root, start, end, 1)
        return self.k
    def insert(self, root, start, end, k):
        if start >= end:
            return root
        if not root:
            self.k = max(self.k, k)
            return Node(start, end, k)
        else:
            if start >= root.e:
                root.right = self.insert(root.right, start, end, k)
                return root
            elif end <= root.s:
                root.left = self.insert(root.left, start, end, k)
                return root
            else:

                a = min(root.s, start)
                b = max(root.s, start)
                c = min(root.e, end)
                d = max(root.e, end)

                root.left = self.insert(root.left, a, b, a == root.s and root.k or k)
                root.right = self.insert(root.right, c,d, d == root.e and root.k or k)
                root.k += k
                root.s = b
                root.e = c
                self.k = max(root.k, self.k)
                return root

Count Map 法

思路

这个是我在看了 Discussion [[C++] Map Solution, beats 95%+](https://leetcode.com/problems... 以后写的解法,解法很是巧妙。

咱们使用一个 count map 来存储全部的预约,对于每次插入,咱们执行count[start] += 1count[end] -= 1。 count[t] 表示从 t 开始到下一个 t 咱们有几个预约。所以咱们须要对 count 进行排序才行。 咱们维护一个最大值来 cnt 来表示须要的预约数。

好比预约[1,3]和[5,7],咱们产生一个预约便可:

image.png

再好比预约[1,5]和[3,7],咱们须要两个预约:

image.png

咱们可使用红黑树来简化时间复杂度,若是你使用的是 Java,能够直接使用现成的数据结构 TreeMap。我这里偷懒,每次都排序,时间复杂度会很高,可是能够 AC。

读到这里,你可能会发现: 这个解法彷佛更具备通用型。对于第一题咱们能够判断 cnt 是否小于等于 1,对于第二题咱们能够判断 cnt 是否小于等于 2。

若是你不借助红黑树等数据结构直接使用 count-map 法,即每次都进行一次排序,第一题和第二题可能会直接超时。

代码

代码支持 Python3:

Python3 Code:

class MyCalendarThree:

    def __init__(self):
        self.count = dict()

    def book(self, start: int, end: int) -> int:
        self.count[start] = self.count.get(start, 0) + 1
        self.count[end] = self.count.get(end, 0) - 1
        cnt = 0
        cur = 0

        for k in sorted(self.count):
            cur += self.count[k]
            cnt = max(cnt, cur)
        return cnt

        # Your MyCalendarThree object will be instantiated and called as such:
        # obj = MyCalendarThree()
        # param_1 = obj.book(start,end)

相关题目

LeetCode 上有一个相似的系列《会议室》,截止目前(2020-02-03)有两道题目。其中一个简单一个中等,解题思路很是相似,你们用这个解题思路尝试一下,检测一下本身是否已经掌握。两道题分别是:

总结

咱们对 LeetCode 上的专题《个人日程安排》的三道题进行了汇总。对于区间判断是否重叠,咱们能够反向判断,也能够正向判断。 暴力的方法是每次对全部的课程进行判断是否重叠,这种解法能够 AC。咱们也能够进一步优化,使用二叉查找树来简化时间复杂度。最后咱们介绍了一种 Count-Map 方法来通用解决全部的问题,不只能够完美解决这三道题,还能够扩展到《会议室》系列的两道题。

相关文章
相关标签/搜索