729. My Schedule I: "Simulation" & "Line Segment Tree (Dynamic Open Point)" & "Block + Bit Operation (Bucket)"

Topic description

This is 729 on LeetCode. My schedule I , medium .

Tag : "simulation", "red-black tree", "line segment tree (dynamic open point)", "line segment tree", "blocking", "bit operation", "hash table"

Implement a MyCalendarclass to hold your schedule. If the schedule to be added does not create double bookings, the new schedule can be stored.

Duplicate bookings occur when two schedules have some overlap in time (for example, both schedules are at the same time).

The schedule can be represented by a pair of integers startand end, where the time is a half-open interval, that is [ s t a r t , and n d ) [start, end) , real number  x x is,   s t a r t < = x < and n d start <= x < end

Implement the MyCalendar class:

  • MyCalendar()Initialize the calendar object.
  • boolean book(int start, int end)Return if the schedule can be successfully added to the calendar without causing double bookings true. Otherwise, go back false and don't add the schedule to the calendar.

Example:

输入:
["MyCalendar", "book", "book", "book"]
[[], [10, 20], [15, 25], [20, 30]]

输出:
[null, true, false, true]

解释:
MyCalendar myCalendar = new MyCalendar();
myCalendar.book(10, 20); // return True
myCalendar.book(15, 25); // return False ,这个日程安排不能添加到日历中,因为时间 15 已经被另一个日程安排预订了。
myCalendar.book(20, 30); // return True ,这个日程安排可以添加到日历中,因为第一个日程安排预订的每个时间都小于 20 ,且不包含时间 20 。

hint:

  • 0 < = s t a r t < and n d < = 1 0 9 0 <= start < end <= 10^9
  • For each test case, the bookmethod up to a maximum of 1000 1000 .

simulation

Use the bookoperation to call at most 1000 1000 times, we can use an array to store all the dates that have been booked [ s t a r t , e n d 1 ] [start, end - 1] , for each bookoperation [ s t a r t , e n d ) [start, end) whether it will conflict with the existing date, the conflict will be returnedFalse, otherwise it will be [ s t a r t , e n d 1 ] [start, end- 1] 插入数组并返回 True

代码:

class MyCalendar {
    List<int[]> list = new ArrayList<>();
    public boolean book(int start, int end) {
        end--;
        for (int[] info : list) {
            int l = info[0], r = info[1];
            if (start > r || end < l) continue;
            return false;
        }
        list.add(new int[]{start, end});
        return true;
    }
}
  • 时间复杂度:令 n n book 的最大调用次数,复杂度为 O ( n 2 ) O(n^2)
  • 空间复杂度: O ( n ) O(n)

线段树(动态开点)

线段树维护的节点信息包括:

  • ls/rs: 分别代表当前节点的左右子节点在线段树数组 tr 中的下标;
  • add: 懒标记;
  • val: 为当前区间的所包含的点的数量。

对于常规的线段树实现来说,都是一开始就调用 build 操作创建空树,而线段树一般以「满二叉树」的形式用数组存储,因此需要 4 n 4 * n 的空间,并且这些空间在起始 build 空树的时候已经锁死。

如果一道题仅仅是「值域很大」的离线题(提前知晓所有的询问),我们还能通过「离散化」来进行处理,将值域映射到一个小空间去,从而解决 MLE 问题。

但对于本题而言,由于「强制在线」的原因,我们无法进行「离散化」,同时值域大小达到 1 e 9 1e9 级别,因此如果我们想要使用「线段树」进行求解,只能采取「动态开点」的方式进行。

动态开点的优势在于,不需要事前构造空树,而是在插入操作 add 和查询操作 query 时根据访问需要进行「开点」操作。由于我们不保证查询和插入都是连续的,因此对于父节点 u u 而言,我们不能通过 u << 1u << 1 | 1 的固定方式进行访问,而要将节点 t r [ u ] tr[u] 的左右节点所在 tr 数组的下标进行存储,分别记为 lsrs 属性。对于 t r [ u ] . l s = 0 tr[u].ls = 0 t r [ u ] . r s = 0 tr[u].rs = 0 则是代表子节点尚未被创建,当需要访问到它们,而又尚未创建的时候,则将其进行创建。

由于存在「懒标记」,线段树的插入和查询都是 log n \log{n} 的,因此我们在单次操作的时候,最多会创建数量级为 log n \log{n} 的点,因此空间复杂度为 O ( m log n ) O(m\log{n}) ,而不是 O ( 4 n ) O(4 * n) ,而开点数的预估需不能仅仅根据 log n \log{n} 来进行,还要对常熟进行分析,才能得到准确的点数上界。

动态开点相比于原始的线段树实现,本质仍是使用「满二叉树」的形式进行存储,只不过是按需创建区间,如果我们是按照连续段进行查询或插入,最坏情况下仍然会占到 4 n 4 * n 的空间,因此盲猜 log n \log{n} 的常数在 4 4 左右,保守一点可以直接估算到 6 6 ,因此我们可以估算点数为 6 m log n 6 * m * \log{n} ,其中 n = 1 e 9 n = 1e9 m = 1 e 3 m = 1e3 分别代表值域大小和查询次数。

当然一个比较实用的估点方式可以「尽可能的多开点数」,利用题目给定的空间上界和我们创建的自定义类(结构体)的大小,尽可能的多开( Java 128 M 128M 可以开到 5 1 0 6 5 * 10^6 以上)。

代码:

class MyCalendar {
    class Node {
        // ls 和 rs 分别代表当前节点的左右子节点在 tr 的下标
        // val 代表当前节点有多少数
        // add 为懒标记
        int ls, rs, add, val;
    }
    int N = (int)1e9, M = 120010, cnt = 1;
    Node[] tr = new Node[M];
    void update(int u, int lc, int rc, int l, int r, int v) {
        if (l <= lc && rc <= r) {
            tr[u].val += (rc - lc + 1) * v;
            tr[u].add += v;
            return ;
        }
        lazyCreate(u);
        pushdown(u, rc - lc + 1);
        int mid = lc + rc >> 1;
        if (l <= mid) update(tr[u].ls, lc, mid, l, r, v);
        if (r > mid) update(tr[u].rs, mid + 1, rc, l, r, v);
        pushup(u);
    }
    int query(int u, int lc, int rc, int l, int r) {
        if (l <= lc && rc <= r) return tr[u].val;
        lazyCreate(u);
        pushdown(u, rc - lc + 1);
        int mid = lc + rc >> 1, ans = 0;
        if (l <= mid) ans = query(tr[u].ls, lc, mid, l, r);
        if (r > mid) ans += query(tr[u].rs, mid + 1, rc, l, r);
        return ans;
    }
    void lazyCreate(int u) {
        if (tr[u] == null) tr[u] = new Node();
        if (tr[u].ls == 0) {
            tr[u].ls = ++cnt;
            tr[tr[u].ls] = new Node();
        }
        if (tr[u].rs == 0) {
            tr[u].rs = ++cnt;
            tr[tr[u].rs] = new Node();
        }
    }
    void pushdown(int u, int len) {
        tr[tr[u].ls].add += tr[u].add; tr[tr[u].rs].add += tr[u].add;
        tr[tr[u].ls].val += (len - len / 2) * tr[u].add; tr[tr[u].rs].val += len / 2 * tr[u].add;
        tr[u].add = 0;
    }
    void pushup(int u) {
        tr[u].val = tr[tr[u].ls].val + tr[tr[u].rs].val;
    }
    public boolean book(int start, int end) {
        if (query(1, 1, N + 1, start + 1, end) > 0) return false;
        update(1, 1, N + 1, start + 1, end, 1);
        return true;
    }
}
  • 时间复杂度:令 n n 为值域大小,本题固定为 1 e 9 1e9 ,线段树的查询和增加复杂度均为 O ( log n ) O(\log{n})
  • 空间复杂度:令询问数量为 m m ,复杂度为 O ( m log n ) O(m\log{n})

分块 + 位运算(分桶)

另一个留有遗憾的算法是「分块」,朴素的分块做法是使用一个布尔数组 region 代表某个块是否有被占用,使用哈希表记录具体某个位置是否被占用,但可惜被奇怪的测评机制卡了。

image.png

TLE 代码:

class MyCalendar {
    static Boolean T = Boolean.TRUE, F = Boolean.FALSE;
    static int n = (int)1e9, len = (int) Math.sqrt(n) + 30;
    static boolean[] region = new boolean[len];
    static Map<Integer, Boolean> map = new HashMap<>();
    int getIdx(int x) {
        return x / len;
    }
    void add(int l, int r) {
        if (getIdx(l) == getIdx(r)) {
            for (int k = l; k <= r; k++) map.put(k, T);
        } else {
            int j = l, i = r;
            while (getIdx(j) == getIdx(l)) map.put(j++, T);
            while (getIdx(i) == getIdx(r)) map.put(i--, T);
            for (int k = getIdx(j); k <= getIdx(i); k++) region[k] = true;
        }
    }
    boolean query(int l, int r) {
        if (getIdx(l) == getIdx(r)) {
            boolean cur = region[getIdx(l)];
            for (int k = l; k <= r; k++) {
                if (map.getOrDefault(k, F) || cur) return false;
            }
            return true;
        } else {
            int j = l, i = r;
            while (getIdx(j) == getIdx(l)) {
                if (map.getOrDefault(j, F) || region[getIdx(j)]) return false;
                j++;
            }
            while (getIdx(i) == getIdx(r)) {
                if (map.getOrDefault(i, F) || region[getIdx(i)]) return false;
                i--;
            }
            for (int k = getIdx(j); k <= getIdx(i); k++) {
                if (region[k]) return false;
            }
            return true;
        }
    }
    public MyCalendar() {
        Arrays.fill(region, false);
        map.clear();
    }
    public boolean book(int start, int end) {
        if (query(start, end - 1)) {
            add(start, end - 1);
            return true;
        }
        return false;
    }
}

但我们知道分块算法的复杂度并不糟糕,而哈希表可能是被卡常数的关键,因此我们可以使用 int 数组来充当哈希表,由于只需要记录「是否被占用」,因此我们可以使用 int 的每一位充当格子,通过这种「分桶 + 位运算」,有效降低常数。

AC 代码(2022-07-05 可过):

class MyCalendar {
    static int n = (int)1e9, len = (int) Math.sqrt(n) + 50, cnt = 32;
    static boolean[] region = new boolean[len];
    static int[] map = new int[n / cnt];
    int getIdx(int x) {
        return x / len;
    }
    boolean get(int x) {
        return ((map[x / cnt] >> (x % cnt)) & 1) == 1;
    }
    void set(int x) {
        map[x / cnt] |= (1 << (x % cnt));
    }
    void add(int l, int r) {
        if (getIdx(l) == getIdx(r)) {
            for (int k = l; k <= r; k++) set(k);
        } else {
            int j = l, i = r;
            while (getIdx(j) == getIdx(l)) set(j++);
            while (getIdx(i) == getIdx(r)) set(i--);
            for (int k = getIdx(j); k <= getIdx(i); k++) region[k] = true;
        }
    }
    boolean query(int l, int r) {
        if (getIdx(l) == getIdx(r)) {
            boolean cur = region[getIdx(l)];
            for (int k = l; k <= r; k++) {
                if (get(k) || cur) return false;
            }
            return true;
        } else {
            int j = l, i = r;
            while (getIdx(j) == getIdx(l)) {
                if (get(j) || region[getIdx(j)]) return false;
                j++;
            }
            while (getIdx(i) == getIdx(r)) {
                if (get(i) || region[getIdx(i)]) return false;
                i--;
            }
            for (int k = getIdx(j); k <= getIdx(i); k++) {
                if (region[k]) return false;
            }
            return true;
        }
    }
    public MyCalendar() {
        Arrays.fill(region, false);
        Arrays.fill(map, 0);
    }
    public boolean book(int start, int end) {
        if (query(start, end - 1)) {
            add(start, end - 1);
            return true;
        }
        return false;
    }
}
  • 时间复杂度: O ( n m ) O(n\sqrt{m})
  • 空间复杂度: O ( m + M k ) O(\sqrt{m} + \frac{M}{k}) ,其中 M = 1 e 9 M = 1e9 为值域大小, k = 32 k = 32 为单个数字的能够存储的格子数

最后

这是我们「刷穿 LeetCode」系列文章的第 No.729 篇,系列开始于 2021/01/01,截止于起始日 LeetCode 上共有 1916 道题目,部分是有锁题,我们将先把所有不带锁的题目刷完。

在这个系列文章里面,除了讲解解题思路以外,还会尽可能给出最为简洁的代码。如果涉及通解还会相应的代码模板。

为了方便各位同学能够电脑上进行调试和提交代码,我建立了相关的仓库:github.com/SharingSour…

在仓库地址里,你可以看到系列文章的题解链接、系列文章的相应代码、LeetCode 原题链接和其他优选题解。

Guess you like

Origin juejin.im/post/7116727238808043557