树状数组
1. 树状数组原理
原理
- 树状数组可以有两个操作:(1)快速求数组A前缀和;(2)修改数组A中某个数据。
- 上面两个操作的时间复杂度都是O(log(N))的
注意:下面所有的数组下标都是从1开始考虑的!!!
如果我们想求出A[1…x]区间和,我们可以根据x的二进制表示将该区间划分为若干部分,如果x的二进制表示如下:
x
=
2
i
k
+
2
i
k
−
1
+
.
.
.
+
2
i
1
i
k
>
i
k
−
1
>
.
.
.
>
i
1
x = 2^{i_k} + 2^{i_{k-1}} + ... + 2^{i_1} \quad \quad i_k > i_{k-1}>...>i_1
x=2ik+2ik−1+...+2i1ik>ik−1>...>i1
则可以将A[1…x]划分为如下区间:
A
(
x
−
2
i
1
,
.
.
.
,
x
]
该
区
间
包
含
2
i
1
个
数
据
A
(
x
−
2
i
1
−
2
i
2
,
.
.
.
,
x
−
2
i
1
]
该
区
间
包
含
2
i
2
个
数
据
.
.
A
(
0
,
.
.
.
,
x
−
2
i
1
−
2
i
2
−
.
.
.
−
2
i
k
−
1
]
该
区
间
包
含
2
i
k
个
数
据
A(x-2^{i_1},..., x] \quad \quad 该区间包含2^{i_1}个数据 \\ A(x-2^{i_1}-2^{i_2},..., x-2^{i_1}] \quad \quad 该区间包含2^{i_2}个数据 \\ . \\ . \\ A(0,..., x-2^{i_1}-2^{i_2}-...-2^{i_{k-1}}] \quad \quad 该区间包含2^{i_k}个数据 \\
A(x−2i1,...,x]该区间包含2i1个数据A(x−2i1−2i2,...,x−2i1]该区间包含2i2个数据..A(0,...,x−2i1−2i2−...−2ik−1]该区间包含2ik个数据
假设这样的区间我们已经知道了,那么我们在O(log(x))的时间里就可以将A[1…x]区间和求出来。
注意到对于上面被划分出来的各个区间,有:
2
i
1
2^{i_1}
2i1 是x的二进制表示的最后一位1;
2
i
2
2^{i_2}
2i2 是
x
−
2
i
1
x-2^{i_1}
x−2i1 的二进制表示的最后一位1;以此类推。因此上述区间又可以写成如下通式,记为tr[R]:
t
r
[
R
]
=
A
[
R
−
l
o
w
b
i
t
(
x
)
+
1
,
R
]
tr[R]=A[R-lowbit(x)+1,\ R]
tr[R]=A[R−lowbit(x)+1, R]
所以有:
A
[
1...
x
]
=
t
r
[
x
]
+
t
r
[
x
−
l
o
w
b
i
t
(
x
)
]
+
t
r
[
y
−
l
o
w
b
i
t
(
y
)
]
+
.
.
.
其
中
y
=
x
−
l
o
w
b
i
t
(
x
)
A[1...x] = tr[x] + tr[x-lowbit(x)]+tr[y-lowbit(y)]+... \quad \quad 其中y=x-lowbit(x)
A[1...x]=tr[x]+tr[x−lowbit(x)]+tr[y−lowbit(y)]+...其中y=x−lowbit(x)
我们可以把tr看成一棵树,那么现在有两个问题:(1)如何找到某个节点的所有儿子;(2)如何找到某个节点的父亲。
(1)寻找tr[x]的儿子(初始化中会用到):A[x],tr[x-1],tr[x-1-lowbit(x-1)],…。如果x=0b…100…000(这里的1是从左往右最后一个1),儿子对应二进制就是:A[x],tr[0b…011…111],tr[0b…011…110],…,tr[0b…010…000]
(2)寻找tr[t]的父亲(更新数据(add)时会用到):tr[t+lowbit(t)]。这个可以由(1)看出来,如果父节点x=0b…100…000,子节点必定是t=0b…011…这种形式(1是连续的),由t可以反向可以推出来x,即x = t + lowbit(t)。
代码模板
int n; // 数组中元素个数
int a[N]; // 原数组,a[1...n]为有效元素
int tr[N];
int lowbit(int x) {
return x & -x;
}
// 更新,操作原数组让a[x] += c;
// 对应上图中自下而上的操作
void add(int x, int c) {
for (int i = x; i <= n; i += lowbit(i)) tr[i] +=c;
}
// 查询,获得a[1...x]的区间和
int sum(int x) {
int res = 0;
for (int i = x; i; i -= lowbit(i)) res += tr[i]; // 0b1100=0b0100+0b1000
return res;
}
// 树状数组的初始化,有很多方式,如下
// 时间复杂度:O(n*log(n))
void init() {
for (int i = 1; i <= n; i++) add(i, a[i]);
}
// 时间复杂度:O(n)
// 对应上图中自上而下的操作
void init() {
for (int i = 1; i <= n; i++) { // 求tr[i]
tr[i] = a[i]; // a[i]也是tr[i]的孩子
for (int j = i - 1; j > i - lowbit(i); j -= lowbit(j)) // 遍历tr[i]的孩子
tr[i] += tr[j];
}
}
// 另外还可以根据tr的定义去算,先求出a的前缀和s,然后求出tr:tr[x] = s[x] - s[x-lowbit(x)]。时间复杂度:O(n)
2. AcWing上的树状数组题目
AcWing 0241. 楼兰图腾
问题描述
-
问题链接:AcWing 0241. 楼兰图腾
分析
- 因为 y 1 y_1 y1~ y n y_n yn 是1~n的一个排列,所以不需要进行离散化了;原数组A的下标是 y 1 y_1 y1~ y n y_n yn 这些数据,数值表示这些数字的个数,非常类似于计数排序的做法。
- 以求解V字形为例,对于我们考察的每个数据 y i y_i yi,我们只要统计出其左侧和其右侧各有多少个数据大于它,然后使用乘法原理就能得到以 y i y_i yi为"山谷"的数量 s i s_i si,然后将所有的 s i s_i si相加就是所有V的个数。
- 分析最多有多少答案,对于每个 y i y_i yi,其左右数据够大于它,最坏情况下会有 1 0 5 × 1 0 5 × 2 × 1 0 5 = 2 × 1 0 15 10^5 \times 10^5 \times 2 \times 10^5=2 \times 10^{15} 105×105×2×105=2×1015个答案,超过int的范围,因此需要使用long long来存储答案。
代码
- C++
#include <iostream>
#include <cstring>
using namespace std;
typedef long long LL;
const int N = 200010;
int n;
int a[N]; // a[y] = 1; 表示y这个数据有1个
int tr[N];
// Greater[k]表示a[1...k-1]中大于a[k]的数据的数量
// lower[k]表示a[1...k-1]中小于a[k]的数据的数量
int Greater[N], lower[N];
int lowbit(int x) {
return x & -x;
}
// 操作原数组,让a[x] += c;
void add(int x, int c) {
for (int i = x; i <= n; i += lowbit(i)) tr[i] +=c;
}
// 获得a[1...x]的区间和
int sum(int x) {
int res = 0;
for (int i = x; i; i -= lowbit(i)) res += tr[i];
return res;
}
int main() {
scanf("%d", &n);
for (int i = 1; i <= n; i++) scanf("%d", &a[i]);
for (int i = 1; i <= n; i++) {
int y = a[i];
Greater[i] = sum(n) - sum(y); // 左侧大于y的数的数量
lower[i] = sum(y - 1); // 左侧小于y的数的数量
add(y, 1);
}
memset(tr, 0, sizeof tr);
LL res1 = 0, res2 = 0;
for (int i = n; i; i--) {
int y = a[i];
res1 += Greater[i] * (LL)(sum(n) - sum(y));
res2 += lower[i] * (LL)(sum(y - 1));
add(y, 1);
}
printf("%lld %lld\n", res1, res2);
return 0;
}
AcWing 242. 一个简单的整数问题
问题描述
分析
-
对于最原始的树状数组存在两个操作:单点加,求区间和(即a[x]+=c, query[L~R]);
-
本题中的操作正好反过来:区间加,求单点和(即a[L~R]+=c, query[x]);
-
这一题的做法是使用差分的思想,将原数组a转化成差分数组b,则:
(1) a [ L , R ] + = c ⟺ b [ L ] + = c , b [ R + 1 ] − = c a[L,R]+=c \iff b[L]+=c, b[R+1]-=c a[L,R]+=c⟺b[L]+=c,b[R+1]−=c;
(2) a [ x ] = ∑ b [ i ] , 1 ≤ i ≤ x a[x] = \sum b[i], 1 \le i \le x a[x]=∑b[i],1≤i≤x;
-
如何将原数组a转换为差分数组呢?转化过程如下,这里必须要求数据从a[1]开始,a[0]=0:
b [ 1 ] = a [ 1 ] − a [ 0 ] b [ 2 ] = a [ 2 ] − a [ 1 ] . . . b [ n ] = a [ n ] − a [ n − 1 ] b[1] = a[1] - a[0] \\ b[2] = a[2] - a[1] \\ ... \\ b[n] = a[n] - a[n - 1] b[1]=a[1]−a[0]b[2]=a[2]−a[1]...b[n]=a[n]−a[n−1]
-
这样,对于数组a的区间加法可以转化成对数组b的单点加;对数组a求单点和可以转化成对数组b的区间和,就可以使用树状数组的操作了。
-
另外数组a中的数据最大为 1 0 9 10^9 109,操作次数为 1 0 5 10^5 105,每次最大加上1000,不会超过int的范围,因此需要使用long long存储结果。
代码
- C++
#include <iostream>
#include <cstring>
using namespace std;
const int N = 100010;
int n, m; // 数列长度、操作个数
int a[N]; // 原数组
int tr[N];
int lowbit(int x) {
return x & -x;
}
// 操作原数组,让b[x] += c;
// 这里的b是差分数组
void add(int x, int c) {
for (int i = x; i <= n; i += lowbit(i)) tr[i] += c;
}
// 查询,获得b[1...x]的区间和
int sum(int x) {
int res = 0;
for (int i = x; i; i -= lowbit(i)) res += tr[i];
return res;
}
int main() {
scanf("%d%d", &n, &m);
for (int i = 1; i <= n; i++) scanf("%d", &a[i]);
for (int i = 1; i <= n; i++) add(i, a[i] - a[i - 1]); // 使用差分数组初始化
while (m--) {
char op[2];
int l, r, d;
scanf("%s%d", op, &l);
if (*op == 'C') {
scanf("%d%d", &r, &d);
add(l, d), add(r + 1, -d);
} else {
printf("%d\n", sum(l));
}
}
return 0;
}
AcWing 243. 一个简单的整数问题2
问题描述
分析
-
对于最原始的树状数组存在两个操作:单点加,求区间和(即a[x]+=c, query[L~R]);
-
AcWing 242. 一个简单的整数问题的操作正好反过来:区间加,求单点和(即a[L~R]+=c, query[x]);
-
本题的操作更进一步:区间加,求区间和(即a[L~R]+=c, query[L~R]);
-
对于区间加,我们仍然可以使用差分的思想,将原数组a转化成差分数组b,则:
(1) a [ L , R ] + = c ⟺ b [ L ] + = c , b [ R + 1 ] − = c a[L,R]+=c \iff b[L]+=c, b[R+1]-=c a[L,R]+=c⟺b[L]+=c,b[R+1]−=c;
(2) a [ x ] = ∑ b [ i ] , 1 ≤ i ≤ x a[x] = \sum b[i], 1 \le i \le x a[x]=∑b[i],1≤i≤x;
-
如何将原数组a转换为差分数组呢?转化过程如下,这里必须要求数据从a[1]开始,a[0]=0:
b [ 1 ] = a [ 1 ] − a [ 0 ] b [ 2 ] = a [ 2 ] − a [ 1 ] . . . b [ n ] = a [ n ] − a [ n − 1 ] b[1] = a[1] - a[0] \\ b[2] = a[2] - a[1] \\ ... \\ b[n] = a[n] - a[n - 1] b[1]=a[1]−a[0]b[2]=a[2]−a[1]...b[n]=a[n]−a[n−1]
- 这样,对于数组a的区间加法可以转化成对数组b的单点加;但是对数组a求区间和我们就要考虑一下如何求解了。
- 求数组a的区间和,只需要求出数组a的前缀和即可,即求出:
∑ i = 1 x a i \sum_{i=1}^{x} a_i i=1∑xai
又因为:
a
[
i
]
=
∑
b
[
j
]
,
1
≤
j
≤
i
a[i] = \sum b[j], 1 \le j \le i
a[i]=∑b[j],1≤j≤i,所以有:
∑
i
=
1
x
a
i
=
∑
i
=
1
x
∑
j
=
1
i
b
i
=
(
b
1
)
+
(
b
1
+
b
2
)
+
.
.
.
+
(
b
1
+
b
2
+
.
.
.
+
b
x
)
\sum_{i=1}^{x} a_i = \sum_{i=1}^{x} \sum_{j=1}^{i} b_i = (b_1)+(b_1+b_2)+...+(b_1+b_2+...+b_x)
i=1∑xai=i=1∑xj=1∑ibi=(b1)+(b1+b2)+...+(b1+b2+...+bx)
如下图(蓝色的是我们需要求解的部分,红色的是我们补上的内容,则蓝色和=全部和-红色和):
则有:
∑
i
=
1
x
a
i
=
(
∑
i
=
1
x
b
i
)
×
(
x
+
1
)
−
(
b
1
+
2
×
b
2
+
.
.
.
+
x
×
b
x
)
\sum_{i=1}^{x} a_i = \Bigl(\sum_{i=1}^{x} b_i \Bigr) \times (x+1) - (b1 + 2 \times b_2 + ... + x \times b_x)
i=1∑xai=(i=1∑xbi)×(x+1)−(b1+2×b2+...+x×bx)
因此,我们在操作的同时维护两个前缀和即可,分别是:
∑
b
i
\sum b_i
∑bi和
∑
i
×
b
i
\sum i \times b_i
∑i×bi。
- 另外数组a中的数据最大为 1 0 9 10^9 109,操作次数为 1 0 5 10^5 105,每次最大加上1000,因为是区间和,所以可能会超过int的范围,因此需要使用long long存储结果。
代码
- C++
#include <iostream>
using namespace std;
typedef long long LL;
const int N = 100010;
int n, m; // 数列长度、操作个数
int a[N]; // 原数组
LL tr1[N]; // 维护a对应的差分数组b的前缀和
LL tr2[N]; // 维护b[i] * i的前缀和
int lowbit(int x) {
return x & -x;
}
void add(LL tr[], int x, LL c) {
for (int i = x; i <= n; i += lowbit(i)) tr[i] += c;
}
LL sum(LL tr[], int x) {
LL res = 0;
for (int i = x; i; i -= lowbit(i)) res += tr[i];
return res;
}
LL prefix_sum(int x) {
return sum(tr1, x) * (x + 1) - sum(tr2, x);
}
int main() {
scanf("%d%d", &n, &m);
for (int i = 1; i <= n; i++) scanf("%d", &a[i]);
// 使用差分数组b对树状数组初始化
for (int i = 1; i <= n; i++) {
int b = a[i] - a[i - 1];
add(tr1, i, b);
add(tr2, i, (LL)b * i);
}
while (m--) {
char op[2];
int l, r, d;
scanf("%s%d%d", op, &l, &r);
if (*op == 'C') {
scanf("%d", &d);
// b[l] += d, tr2维护的是b[i] * i的前缀和
// 因此b[l]增加d, 则(b[l] + d) * l增加了l*d
add(tr1, l, d), add(tr2, l, l * d);
// b[r + 1] -= d
add(tr1, r + 1, -d), add(tr2, r + 1, (r + 1) * -d);
} else {
printf("%lld\n", prefix_sum(r) - prefix_sum(l - 1));
}
}
return 0;
}
AcWing 244. 谜一样的牛
问题描述
-
问题链接:AcWing 244. 谜一样的牛
分析
-
由题目可知,这n有牛的身高是1~n的一个排列。
-
我们可以从排在最后的一头牛开始考虑,假设 h i h_i hi表示第i头牛前面有 h i h_i hi头牛比它矮,则当考虑 h n h_n hn时,表示第n头牛应该是第 h n + 1 h_n+1 hn+1矮的牛;因此当考虑第 h i h_i hi头牛是,它应该是剩余高度中第 h i + 1 h_i+1 hi+1矮的牛(所谓剩余是指已经确定高度的数据不能参与进来)。
-
考虑上面的分析中存在什么操作:
(1)从剩余的数中,找出第K小的数;
(2)删除某个数;
-
这两个操作是平衡树的两个典型操作,但是平衡树很难写,我们考虑使用树状数组解决。
-
我们可以使用一个数组记录每个数据是否被使用过,记为数组a, a [ i ] = = 1 a[i]==1 a[i]==1表示高度i没有被使用过, a [ i ] = = 0 a[i]==0 a[i]==0表示已经存在某头牛的高度为i;设 s u m ( x ) = ∑ a [ i ] , 1 ≤ i ≤ x sum(x)=\sum a[i], 1 \le i \le x sum(x)=∑a[i],1≤i≤x则上面的操作等价于:
(1)找到一个最小的x,使得sum(x)==K;
(2)将a[x]置为0,代表该高度已经被使用过。
-
第(2)个操作相当于单点加,第(1)个操作相当于区间和,可以使用树状数组解决。
-
对于第(1)个操作,因为sum(x)是x的一个单调递增函数,因此可以使用二分求解。
代码
- C++
#include <iostream>
using namespace std;
const int N = 100010;
int n;
int h[N]; // h[i]表示前面有h[i]头牛比当前牛矮
int ans[N]; // 最终这一列牛的高度
int tr[N]; // 维护上面分析中的数组a的前缀和, 我们并不需要将a定义出来
int lowbit(int x) {
return x & -x;
}
void add(int x, int c) {
for (int i = x; i <= n; i += lowbit(i)) tr[i] += c;
}
int sum(int x) {
int res = 0;
for (int i = x; i; i -= lowbit(i)) res += tr[i];
return res;
}
int main() {
scanf("%d", &n);
for (int i = 2; i <= n; i++) scanf("%d", &h[i]);
// 初始化树状数组, 因为开始a中所有元素都为1
// 所以根据tr[i]的定义,即tr[i] = a[i - lowbit(i) + 1, i]之和
// tr[i] = lowbit(i)
for (int i = 1; i <= n; i++) tr[i] = lowbit(i);
for (int i = n; i; i--) {
int k = h[i] + 1; // 此时第i头牛的高度是所有牛当中第k小的
int l = 1, r = n;
while (l < r) {
int mid = l + r >> 1;
if (sum(mid) >= k) r = mid;
else l = mid + 1;
}
ans[i] = r;
add(r, -1); // 让a[i]变为0
}
for (int i = 1; i <= n; i++) printf("%d\n", ans[i]);
return 0;
}
3. 力扣上的树状数组的题目
Leetcode 0307 区域和检索 - 数组可变
问题描述
代码
- C++
class NumArray {
public:
int n;
vector<int> tr, nums;
int lowbit(int x) {
return x & -x;
}
void add(int x, int v) {
for (int i = x; i <= n; i += lowbit(i)) tr[i] += v;
}
int query(int x) {
int res = 0;
for (int i = x; i; i -= lowbit(i)) res += tr[i];
return res;
}
NumArray(vector<int> &_nums) {
nums = _nums;
n = nums.size();
tr.resize(n + 1);
// 线性初始化
for (int i = 1; i <= n; i++) {
tr[i] = nums[i - 1];
for (int j = i - 1; j > i - lowbit(i); j -= lowbit(j))
tr[i] += tr[j];
}
}
void update(int i, int val) {
add(i + 1, val - nums[i]);
nums[i] = val;
}
int sumRange(int i, int j) {
i++, j++;
return query(j) - query(i - 1);
}
};
Leetcode 0315 计算右侧小于当前元素的个数
问题描述
代码
- C++
class Solution {
public:
int n = 20001;
vector<int> tr;
int lowbit(int x) {
return x & -x;
}
void add(int x, int v) {
for (int i = x; i <= n; i += lowbit(i)) tr[i] += v;
}
int query(int x) {
int res = 0;
for (int i = x; i; i -= lowbit(i)) res += tr[i];
return res;
}
vector<int> countSmaller(vector<int> &nums) {
tr.resize(n + 1);
vector<int> res(nums.size());
for (int i = nums.size() - 1; i >= 0; i--) {
int x = nums[i] + 10001;
res[i] = query(x - 1);
add(x, 1);
}
return res;
}
};
- Java
public class Solution {
public static final int N = 20001;
static int[] tr = new int[N + 1];
private int lowbit(int x) {
return x & -x;
}
private void add(int x, int v) {
for (int i = x; i <= N; i += lowbit(i)) tr[i] += v;
}
private int query(int x) {
int res = 0;
for (int i = x; i > 0; i -= lowbit(i)) res += tr[i];
return res;
}
public List<Integer> countSmaller(int[] nums) {
Arrays.fill(tr, 0); // 必须加上,否则lc无法通过
List<Integer> res = new ArrayList<>();
for (int i = nums.length - 1; i >= 0; i--) {
int x = nums[i] + 10001;
res.add(query(x - 1));
add(x, 1);
}
Collections.reverse(res);
return res;
}
}
Leetcode 0327 区间和的个数
问题描述
-
问题链接:Leetcode 0327 区间和的个数
代码
- C++
class Solution {
public:
int m;
vector<int> tr; // 树状数组
vector<LL> all; // 待离散化的数据
int get(LL x) {
return lower_bound(all.begin(), all.end(), x) - all.begin() + 1;
}
int lowbit(int x) {
return x & -x;
}
void add(int x, int v) {
for (int i = x; i <= m; i += lowbit(i)) tr[i] += v;
}
int query(int x) {
int res = 0;
for (int i = x; i; i -= lowbit(i))res += tr[i];
return res;
}
int countRangeSum(vector<int> &nums, int lower, int upper) {
int n = nums.size();
vector<LL> s(n + 1); // 前缀和
all.push_back(0);
for (int i = 1; i <= n; i++) {
s[i] = s[i - 1] + nums[i - 1];
all.push_back(s[i]);
all.push_back(s[i] - lower);
all.push_back(s[i] - upper - 1);
}
sort(all.begin(), all.end());
all.erase(unique(all.begin(), all.end()), all.end());
m = all.size();
tr.resize(m + 1);
int res = 0;
// 为什么有下面这句话:https://www.acwing.com/activity/content/code/content/477194/
// 相当于考虑分析中的 sj = 0 --> lower <= si <= upper 这种情况
add(get(0), 1);
for (int i = 1; i <= n; i++) {
res += query(get(s[i] - lower)) - query(get(s[i] - upper - 1));
add(get(s[i]), 1);
}
return res;
}
};
Leetcode 0406 根据身高重建队列
问题描述
分析
- 这一题存在两种做法。
方法1
-
0~n-1,一共n个位置,我们需要考虑每个人放在哪个位置。
-
将people按照第一维升序、第二维降序的顺序排序,然后依次从前往后考虑每一个二元组。
-
假设当前考察的是第i个二元组 ( h , k ) (h, k) (h,k),假设第i个二元组的身高严格大于第i-1个二元组的身高,则我们需要在还没有放入人的位置中找到第k+1位置对应的下标,然后将第i个人(二元组)放到这个位置。
-
假设第i个二元组的身高等于第i-1个二元组的身高,则我们仍按照上面的方式操作,结果仍然正确,因为此时第i个二元组一定放在第i-1个二元组左边。第i-1个人左边大于等于他的人数没有改变(因为等于也符合条件)。
-
考虑上面的分析中存在什么操作(类似于AcWing 244. 谜一样的牛):
(1)从剩余的数(对应的位置索引)中,找出第K小的数;
(2)删除某个数(索引);
-
我们可以使用一个数组记录每个数据是否被使用过,记为数组a, a [ i ] = = 0 a[i]==0 a[i]==0表示位置i没有被使用过, a [ i ] = = 1 a[i]==1 a[i]==1表示位置i已经被人占了;设 s u m ( x ) = ∑ a [ i ] , 1 ≤ i ≤ x sum(x)=\sum a[i], 1 \le i \le x sum(x)=∑a[i],1≤i≤x则上面的操作等价于(代码实现中a不需要定义出来):
-
则上面的操作等价于:
(1)找到一个最小的x,使得sum(x)==K;
(2)将a[x]置为1,代表该高度已经被使用过。
-
第(2)个操作相当于单点加,第(1)个操作相当于区间和,可以使用树状数组解决。
-
对于第(1)个操作,因为sum(x)是x的一个单调递减函数,因此可以使用二分求解。
方法2
- 将people按照第一维降序、第二维升序的顺序排序,然后依次从前往后考虑每一个二元组。
- 假设记录答案的数组为res,对于第i个二元组 ( h , k ) (h, k) (h,k),将其插入res[k]位置即可。
- 上图参考的网址:网址
代码
- C++
// 方法1
/**
* 执行用时:104 ms, 在所有 C++ 提交中击败了76.69%的用户
* 内存消耗:23.9 MB, 在所有 C++ 提交中击败了24.85%的用户
*/
class Solution {
public:
int n;
vector<int> tr;
int lowbit(int x) {
return x & -x;
}
void add(int x, int c) {
for (int i = x; i <= n; i += lowbit(i)) tr[i] += c;
}
int query(int x) {
int res = 0;
for (int i = x; i; i -= lowbit(i)) res += tr[i];
return res;
}
vector<vector<int>> reconstructQueue(vector<vector<int>> &people) {
n = people.size();
tr.resize(n + 1); // 树状数组下标必须从1开始
sort(people.begin(), people.end(), [](vector<int> a, vector<int> b) {
if (a[0] != b[0]) return a[0] < b[0]; // 按照第一维升序
return a[1] > b[1]; // 按照第二维降序
});
vector<vector<int>> res(n);
for (auto p : people) {
int h = p[0], k = p[1];
int l = 1, r = n;
while (l < r) {
int mid = l + r >> 1;
// query(mid)返回a[1..mid]中1的个数,0表示该位置没被占用
if (mid - query(mid) >= k + 1) r = mid;
else l = mid + 1;
}
res[r - 1] = p; // a[1]表示第0个位置的占用情况
add(r, 1);
}
return res;
}
};
// 方法二
/**
* 执行用时:200 ms, 在所有 C++ 提交中击败了35.38%的用户
* 内存消耗:11.8 MB, 在所有 C++ 提交中击败了70.56%的用户
*/
class Solution {
public:
vector<vector<int>> reconstructQueue(vector<vector<int>> &people) {
// 按照第一维降序,第二维升序排列
sort(people.begin(), people.end(), [](const vector<int> &a, const vector<int> &b) {
if (a[0] == b[0]) return a[1] < b[1];
return a[0] > b[0];
});
vector<vector<int>> res;
for (auto &p : people) {
res.insert(res.begin() + p[1], p);
}
return res;
}
};
- Java
// 方法二
/**
* Date: 2020/11/16 8:59
* Content: 按照身高降序 K升序排序;然后将排序后数据依次插入list,先插入的数据一定是比较高的人
* 执行用时:8 ms, 在所有 Java 提交中击败了90.65%的用户
* 内存消耗:39.5 MB, 在所有 Java 提交中击败了75.50%的用户
*/
public class Solution {
public int[][] reconstructQueue(int[][] people) {
Arrays.sort(people, (o1, o2) -> {
// 如果身高相等(o1[0] == o2[0]) , 按照K降序排列(o2[0] - o1[0])
return o1[0] == o2[0] ? o1[1] - o2[1] : o2[0] - o1[0];
});
List<int[]> list = new ArrayList<>();
for (int[] p : people) {
list.add(p[1], p);
}
return list.toArray(new int[0][0]);
}
}