【面试高频题】可拓展变形的「区间求和」经典题

一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第12天,点击查看活动详情

题目描述

这是 LeetCode 上的 1893. 检查是否区域内所有整数都被覆盖 ,难度为 简单

Tag : 「模拟」、「树状数组」、「线段树」

给你一个二维整数数组 ranges 和两个整数 left 和 right。每个  r a n g e s [ i ] = [ s t a r t i , e n d i ] ranges[i] = [start_i, end_i]  表示一个从  s t a r t i start_i  到  e n d i end_i  的 闭区间 。

如果闭区间  [ l e f t , r i g h t ] [left, right]  内每个整数都被 ranges 中 至少一个 区间覆盖,那么请你返回 true,否则返回 false

已知区间 r a n g e s [ i ] = [ s t a r t i , e n d i ] ranges[i] = [start_i, end_i] ,如果整数 x 满足 s t a r t i < = x < = e n d i start_i <= x <= end_i ,那么我们称整数 x 被覆盖了。

示例 1:

输入:ranges = [[1,2],[3,4],[5,6]], left = 2, right = 5

输出:true

解释:2 到 5 的每个整数都被覆盖了:
- 2 被第一个区间覆盖。
- 3 和 4 被第二个区间覆盖。
- 5 被第三个区间覆盖。
复制代码

示例 2:

输入:ranges = [[1,10],[10,20]], left = 21, right = 21

输出:false

解释:21 没有被任何一个区间覆盖。
复制代码

提示:

  • 1 < = r a n g e s . l e n g t h < = 50 1 <= ranges.length <= 50
  • 1 < = s t a r t i < = e n d i < = 50 1 <= start_i <= end_i <= 50
  • 1 < = l e f t < = r i g h t < = 50 1 <= left <= right <= 50

模拟

一个简单的想法是根据题意进行模拟,检查 [ l e f t , r i g h t ] [left, right] 中的每个整数,如果检查过程中发现某个整数没被 r a n g e s ranges 中的闭区间所覆盖,那么直接返回 False,所有数值通过检查则返回 True

代码:

class Solution {
    public boolean isCovered(int[][] rs, int l, int r) {
        for (int i = l; i <= r; i++) {
            boolean ok = false;
            for (int[] cur : rs) {
                int a = cur[0], b = cur[1];
                if (a <= i && i <= b) {
                    ok = true;
                    break;
                }
            }
            if (!ok) return false;
        }
        return true;
    }
}
复制代码
  • 时间复杂度:令 [ l e f t , r i g h t ] [left, right] 之间整数数量为 n n r a n g e s ranges 长度为 m m 。整体复杂度为 O ( n m ) O(n * m)
  • 空间复杂度: O ( 1 ) O(1)

树状数组

针对此题,可以有一个很有意思的拓展,将本题难度提升到【中等】甚至是【困难】。

将查询 [ l e f t , r i g h t ] [left, right] 修改为「四元查询数组」 q u e r y s querys ,每个 q u e r y s [ i ] querys[i] 包含四个指标 ( a , b , l , r ) (a,b,l,r) :代表询问 [ l , r ] [l, r] 中的每个数是否在 r a n g e range [ a , b ] [a, b] 的闭区间所覆盖过。

如果进行这样的拓展的话,那么我们需要使用「持久化树状数组」或者「主席树」来配合「容斥原理」来做。

基本思想都是使用 r a n g e [ 0 , b ] range[0,b] 的计数情况减去 r a n g e [ 0 , a 1 ] range[0, a-1] 的计数情况来得出 [ a , b ] [a, b] 的计数情况。

回到本题,由于数据范围很小,只有 50 50 ,我们可以使用「树状数组」进行求解:

  • void add(int x, int u):对于数值 x x 出现次数进行 + u +u 操作;
  • int query(int x):查询某个满足 < = x <= x 的数值的个数。

那么显然,如果我们需要查询一个数值 x x 是否出现过,可以通过查询 c n t = q u e r y ( x ) q u e r y ( x 1 ) cnt = query(x) - query(x - 1) 来得知。

代码:

class Solution {
    int n = 55;
    int[] tr = new int[n];
    int lowbit(int x) {
        return x & -x;
    }
    void add(int x, int u) {
        for (int i = x; i <= n; i += lowbit(i)) tr[i] += u;
    }
    int query(int x) {
        int ans = 0;
        for (int i = x; i > 0; i -= lowbit(i)) ans += tr[i];
        return ans;
    }
    public boolean isCovered(int[][] rs, int l, int r) {
        for (int[] cur : rs) {
            int a = cur[0], b = cur[1];
            for (int i = a; i <= b; i++) {
                add(i, 1);
            }
        }
        for (int i = l; i <= r; i++) {
            int cnt = query(i) - query(i - 1);
            if (cnt == 0) return false;
        }
        return true;
    }
}
复制代码
  • 时间复杂度:令 [ l e f t , r i g h t ] [left, right] 之间整数数量为 n n i = 0 r a n g e . l e g t h 1 r a n g e s [ i ] . l e n g t h \sum_{i = 0}^{range.legth - 1}ranges[i].length s u m sum ,常数 C C 固定为 55 55 。建树复杂度为 O ( s u m log C ) O(sum\log C) ,查询查询复杂度为 O ( n log C ) O(n\log{C}) 。整体复杂度为 O ( s u m log C + n log C ) O(sum\log{C} + n\log{C})
  • 空间复杂度: O ( C ) O(C)

树状数组(去重优化)

在朴素的「树状数组」解法中,我们无法直接查询 [ l , r ] [l, r] 区间中被覆盖过的个数的根本原因是「某个值可能会被重复添加到树状数组中」。

因此,一种更加优秀的做法:在往树状数组中添数的时候进行去重,然后通过 c n t = q u e r y ( r ) q u e r y ( l 1 ) cnt = query(r) - query(l - 1) 直接得出 [ l , r ] [l, r] 范围内有多少个数被添加过。

这样的 Set 去重操作可以使得我们查询的复杂度从 O ( n log C ) O(n\log{C}) 下降到 O ( log C ) O(\log{C})

由于数值范围很小,自然也能够使用数组来代替 Set 进行标记(见 P2)

代码:

class Solution {
    int n = 55;
    int[] tr = new int[n];
    int lowbit(int x) {
        return x & -x;
    }
    void add(int x, int u) {
        for (int i = x; i <= n; i += lowbit(i)) tr[i] += u;
    }
    int query(int x) {
        int ans = 0;
        for (int i = x; i > 0; i -= lowbit(i)) ans += tr[i];
        return ans;
    }
    public boolean isCovered(int[][] rs, int l, int r) {
        Set<Integer> set = new HashSet<>();
        for (int[] cur : rs) {
            int a = cur[0], b = cur[1];
            for (int i = a; i <= b; i++) {
                if (!set.contains(i)) {
                    add(i, 1);
                    set.add(i);
                }
            }
        }
        int tot = r - l + 1, cnt = query(r) - query(l - 1);
        return tot == cnt;
    }
}
复制代码
class Solution {
    int n = 55;
    int[] tr = new int[n];
    boolean[] vis = new boolean[n];
    int lowbit(int x) {
        return x & -x;
    }
    void add(int x, int u) {
        for (int i = x; i <= n; i += lowbit(i)) tr[i] += u;
    }
    int query(int x) {
        int ans = 0;
        for (int i = x; i > 0; i -= lowbit(i)) ans += tr[i];
        return ans;
    }
    public boolean isCovered(int[][] rs, int l, int r) {
        for (int[] cur : rs) {
            int a = cur[0], b = cur[1];
            for (int i = a; i <= b; i++) {
                if (!vis[i]) {
                    add(i, 1);
                    vis[i] = true;
                }
            }
        }
        int tot = r - l + 1, cnt = query(r) - query(l - 1);
        return tot == cnt;
    }
}
复制代码
  • 时间复杂度:令 [ l e f t , r i g h t ] [left, right] 之间整数数量为 n n i = 0 r a n g e . l e g t h 1 r a n g e s [ i ] . l e n g t h \sum_{i = 0}^{range.legth - 1}ranges[i].length s u m sum ,常数 C C 固定为 55 55 。建树复杂度为 O ( s u m log C ) O(sum\log C) ,查询查询复杂度为 O ( log C ) O(\log{C}) 。整体复杂度为 O ( s u m log C + log C ) O(sum\log{C} + \log{C})
  • 空间复杂度: O ( C + i = 0 r a n g e . l e g t h 1 r a n g e s [ i ] . l e n g t h ) O(C + \sum_{i = 0}^{range.legth - 1}ranges[i].length)

线段树(不含“懒标记”)

更加进阶的做法是使用「线段树」来做,与「树状数组(优化)」解法一样,线段树配合持久化也可以用于求解「在线」问题。

与主要解决「单点修改 & 区间查询」的树状数组不同,线段树能够解决绝大多数「区间修改(区间修改/单点修改)& 区间查询」问题。

对于本题,由于数据范围只有 55 55 ,因此我们可以使用与「树状数组(优化)」解法相同的思路,实现一个不包含“懒标记”的线段树来做(仅支持单点修改 & 区间查询)。

代码:

class Solution {
    // 代表 [l, r] 区间有 cnt 个数被覆盖
    class Node {
        int l, r, cnt;
        Node (int _l, int _r, int _cnt) {
            l = _l; r = _r; cnt = _cnt;
        }
    }
    int N = 55;
    Node[] tr = new Node[N * 4];
    void pushup(int u) {
        tr[u].cnt = tr[u << 1].cnt + tr[u << 1 | 1].cnt;
    }
    void build(int u, int l, int r) {
        if (l == r) {
            tr[u] = new Node(l, r, 0);
        } else {
            tr[u] = new Node(l, r, 0);
            int mid = l + r >> 1;
            build(u << 1, l, mid);
            build(u << 1 | 1, mid + 1, r);
            pushup(u);
        }
    }
    // 从 tr 数组的下标 u 开始,在数值 x 的位置进行标记
    void update(int u, int x) {
        if (tr[u].l == x && tr[u].r == x) {
            tr[u].cnt = 1;
        } else {
            int mid = tr[u].l + tr[u].r >> 1;
            if (x <= mid) update(u << 1, x);
            else update(u << 1 | 1, x);
            pushup(u);
        }
    }
    // 从 tr 数组的下标 u 开始,查询 [l,r] 范围内有多少个数值被标记
    int query(int u, int l, int r) {
        if (l <= tr[u].l && tr[u].r <= r) return tr[u].cnt;
        int mid = tr[u].l + tr[u].r >> 1;
        int ans = 0;
        if (l <= mid) ans += query(u << 1, l, r);
        if (r > mid) ans += query(u << 1 | 1, l, r);
        return ans;
    }
    public boolean isCovered(int[][] rs, int l, int r) {
        build(1, 1, N);
        for (int[] cur : rs) {
            int a = cur[0], b = cur[1];
            for (int i = a; i <= b; i++) {
                update(1, i);
            }
        }
        int tot = r - l + 1 , cnt = query(1, l, r);
        return tot == cnt;
    }
}
复制代码
  • 时间复杂度:令 [ l e f t , r i g h t ] [left, right] 之间整数数量为 n n i = 0 r a n g e . l e g t h 1 r a n g e s [ i ] . l e n g t h \sum_{i = 0}^{range.legth - 1}ranges[i].length s u m sum ,常数 C C 固定为 55 55 。建树复杂度为 O ( s u m log C ) O(sum\log C) ,查询查询复杂度为 O ( log C ) O(\log{C}) 。整体复杂度为 O ( s u m log C + log C ) O(sum\log{C} + \log{C})
  • 空间复杂度: O ( C 4 ) O(C * 4)

最后

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

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

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

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

猜你喜欢

转载自juejin.im/post/7085524540876390407