共计 3153 个字符,预计需要花费 8 分钟才能阅读完成。
题目形容
这是 LeetCode 上的 1395. 统计作战单位数 ,难度为 中等。
Tag :「树状数组」、「容斥原理」
n
名士兵站成一排。每个士兵都有一个 举世无双 的评分 rating
。
每 $3$ 个士兵能够组成一个作战单位,分组规定如下:
- 从队伍中选出下标别离为
i
、j
、k
的 $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
中的元素都是惟一的
根本剖析
为了不便,咱们记 rating
为 rs
。
题目实质是要咱们统计所有满足「递增」或「递加」的三元组。换句话说,对于每个 $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 多平台公布