【综合笔试题】难度 2.5/5 :「树状数组」与「双树状数组优化」

题目描述

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

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

n 名士兵站成一排。每个士兵都有一个 独一无二 的评分 rating

3 个士兵可以组成一个作战单位,分组规则如下:

  • 从队伍中选出下标分别为 ijk3 名士兵,他们的评分分别为 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),我们枚举其两端 ik,根据 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/SharingSource/LogicStack-LeetCode

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

更多更全更热门的「笔试/面试」相关的资料可访问排版精明的 合集新基地 🎉🎉

本文由mdnice多平台发布

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 219,753评论 6 508
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,668评论 3 396
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 166,090评论 0 356
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 59,010评论 1 295
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 68,054评论 6 395
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,806评论 1 308
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,484评论 3 420
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,380评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,873评论 1 319
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 38,021评论 3 338
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,158评论 1 352
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,838评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,499评论 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,044评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,159评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,449评论 3 374
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,136评论 2 356

推荐阅读更多精彩内容