关于程序员:综合笔试题难度-255-树状数组与双树状数组优化

35次阅读

共计 3153 个字符,预计需要花费 8 分钟才能阅读完成。

题目形容

这是 LeetCode 上的 1395. 统计作战单位数 ,难度为 中等

Tag :「树状数组」、「容斥原理」

 n 名士兵站成一排。每个士兵都有一个 举世无双 的评分 rating

每 $3$ 个士兵能够组成一个作战单位,分组规定如下:

  • 从队伍中选出下标别离为 ijk 的 $3$ 名士兵,他们的评分别离为 $rating[i]$、$rating[j]$、$rating[k]$
  • 作战单位需满足:$rating[i] < rating[j] < rating[k]$ 或者 $rating[i] > rating[j] > rating[k]$,其中  $0 <= i < j < k < n$

请你返回按上述条件能够组建的作战单位数量。每个士兵都能够是多个作战单位的一部分。

示例 1:

输出:rating = [2,5,3,4,1]

输入:3

解释:咱们能够组建三个作战单位 (2,3,4)、(5,4,1)、(5,3,1)。

示例 2:

输出:rating = [2,1,3]

输入:0

解释:依据题目条件,咱们无奈组建作战单位。

示例 3:

输出:rating = [1,2,3,4]

输入:4

提醒:

  • $n == rating.length$
  • $3 <= n <= 1000$
  • $1 <= rating[i] <= 10^5$
  • rating 中的元素都是惟一的

根本剖析

为了不便,咱们记 ratingrs

题目实质是要咱们统计所有满足「递增」或「递加」的三元组。换句话说,对于每个 $t = rs[i]$ 而言,咱们须要统计比其 $t$ 大或比 $t$ 小的数的个数。

问题波及「单点批改(更新数值 $t$ 的呈现次数)」以及「区间查问(查问某段范畴内数的个数)」,应用「树状数组」求解较为适合。

树状数组 – 枚举两端

一个奢侈的想法是,对于三元组 $(i, j, k)$,咱们枚举其两端 $i$ 和 $k$,依据 $rs[i]$ 和 $rs[k]$ 的大小关系,查问范畴 $[i + 1, k – 1]$ 之间非法的数的个数。

在确定左端点 $i$ 时,咱们从 $i + 1$ 开始「从小到大」枚举右端点 $k$,并将遍历过程中通过的 $rs[k]$ 增加到树状数组进行计数。

处理过程中依据 $a = rs[i]$ 和 $b = rs[k]$ 的大小关系进行分状况探讨:

  • 当 $a < b$ 时,咱们须要在范畴 $[i + 1, k – 1]$ 中找「大于 $a$」同时「小于 $b$」的数的个数,即 query(b - 1) - query(a)
  • 当 $a > b$ 时,咱们须要在范畴 $[i + 1, k – 1]$ 中找「小于 $a$」同时「大于 $b$」的数的个数,即 query(a - 1) - query(b)

一些细节:显然咱们须要在枚举每个左端点 $i$ 时清空树状数组,但留神不能应用诸如 Arrays.fill(tr, 0) 的形式进行清空。

因为在没有离散化的状况下,树状数组的大小为 $m = 1e5$,即执行 Arrays.fill 操作的复杂度为 $O(m)$,这会导致咱们计算量为至多为 $n \times m = 1e8$,会有 TLE 危险。

因而一个适合做法是:在 $[i + 1, n – 1]$ 范畴内枚举完 $k$ 后(进行的是 +1 计数),再枚举一次 $[i + 1, n – 1]$ 进行一次 -1 的计数进行对消。

代码:

class Solution {static int N = (int)1e5 + 10;
    static int[] tr = new int[N];
    int lowbit(int x) {return x & -x;}
    void update(int x, int v) {for (int i = x; i < N; i += lowbit(i)) tr[i] += v;
    }
    int query(int x) {
        int ans = 0;
        for (int i = x; i > 0; i -= lowbit(i)) ans += tr[i];
        return ans;
    }
    public int numTeams(int[] rs) {
        int n = rs.length, ans = 0;
        for (int i = 0; i < n; i++) {int a = rs[i];
            for (int j = i + 1; j < n; j++) {int b = rs[j];
                if (a < b) ans += query(b - 1) - query(a);
                else ans += query(a - 1) - query(b);
                update(b, 1);
            }
            for (int j = i + 1; j < n; j++) update(rs[j], -1);
        }
        return ans;
    }
}
  • 工夫复杂度:令 $m = 1e5$ 为值域大小,整体复杂度为 $O(n^2\log{m})$
  • 空间复杂度:$O(m)$

双树状数组优化 – 枚举中点

咱们思考将 $n$ 的数据范畴晋升到 $1e4$ 该如何做。

上述解法的瓶颈在于咱们枚举三元组中的左右端点,复杂度为 $O(n^2)$,而实际上利用三元组必然递增或递加的个性,咱们能够调整为枚举起点 $j$,从而将「枚举点对」调整为「枚举中点」,复杂度为 $O(n)$。

假如以后枚举到的点为 $rs[i]$,问题转换为在 $[0, i – 1]$ 有多少比 $rs[i]$ 小 / 大 的数,在 $[i + 1, n – 1]$ 有多少比 $rs[i]$ 大 / 小 的数,而后汇合「乘法」原理即可晓得 $rs[i]$ 作为三元组中点的非法计划数。

统计 $rs[i]$ 右边的比 $rs[i]$ 大 / 小 的数很好做,只须要在「从小到大」枚举 $i$ 的过程中,将 $rs[i]$ 增加到树状数组 tr1 即可。

对于统计 $rs[i]$ 左边比 $rs[i]$ 小 / 大 的数,则须要通过「对消计数」来做,起始咱们先将所有 $rs[idx]$ 退出到另外一个树状数组 tr2 中(进行 +1 计数),而后在从前往后解决每个 $rs[i]$ 的时候,在 tr2 中进行 -1 对消,从而确保咱们解决每个 $rs[i]$ 时,tr1 存储右边的数,tr2 存储左边的数。

代码:

class Solution {static int N = (int)1e5 + 10;
    static int[] tr1 = new int[N], tr2 = new int[N];
    int lowbit(int x) {return x & -x;}
    void update(int[] tr, int x, int v) {for (int i = x; i < N; i += lowbit(i)) tr[i] += v;
    }
    int query(int[] tr, int x) {
        int ans = 0;
        for (int i = x; i > 0; i -= lowbit(i)) ans += tr[i];
        return ans;
    }
    public int numTeams(int[] rs) {
        int n = rs.length, ans = 0;
        Arrays.fill(tr1, 0);
        Arrays.fill(tr2, 0);
        for (int i : rs) update(tr2, i, 1);
        for (int i = 0; i < n; i++) {int t = rs[i];
            update(tr2, t, -1);
            ans += query(tr1, t - 1) * (query(tr2, N - 1) - query(tr2, t));
            ans += (query(tr1, N - 1) - query(tr1, t)) * query(tr2, t - 1);
            update(tr1, t, 1);
        }
        return ans;
    }
}
  • 工夫复杂度:令 $m = 1e5$ 为值域大小,整体复杂度为 $O(n\log{m})$
  • 空间复杂度:$O(m)$

最初

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

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

为了不便各位同学可能电脑上进行调试和提交代码,我建设了相干的仓库:https://github.com/SharingSou…。

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

更多更全更热门的「口试 / 面试」相干的材料可拜访排版精明的 合集新基地 🎉🎉

本文由 mdnice 多平台公布

正文完
 0