O(N) 件の実績#
ツリー配列を構築する最も基本的な方法は、各ポイントに値を追加することです。
時間計算量: O(NlogN)
コード
int tr[N]; // tr[] 存储树状数组数据
int a[N]; // a[] 存储原数组数据
int n; // 数列长度
int lowbit(int x) { return x & -x; }
void add(int x, c) {
for (int i = x; i <= n; x += lowbit(x))
tr[i] += c;
}
// 建树
void build() {
for (int i = 1; i <= n; i++)
add(i, a[i]);
}
通常のツリー構築の時間計算量は NlogN であるため、O(N) ツリー構築のアプリケーション シナリオはそれほど多くありません。今回の複雑さは、一部の質問が意図的に行き詰まっているなどしない限り、ほとんどの質問で許容されます。
方法 1 #
ツリー配列 tr[x] の場合、維持される間隔範囲は [x−lowbit(x)+1,x] であることがわかっているため、tr[x]=a[x−lowbit(x)+1,x] 。次に、最初に a[] のプレフィックスの合計を見つけ、次にプレフィックスの合計 O(1) までの [x−lowbit(x)+1,x] の区間合計を見つけて、O(N) を達成して、ツリー配列。
コード
int tr[N]; // 树状数组数据
int a[N]; // 原数组数据
int sum[N]; // sum[] 存储 a[] 的前缀和
int n; // 数列长度
int lowbit(int x) { return x & -x; }
// 建树
void build() {
// 求 a[] 的前缀和 sum[]
for (int i = 1; i <= n; i++)
sum[i] = sum[i - 1] + a[i];
// 利用前缀和求出区间和,O(N)建树
for (int i = 1; i <= n; i++)
tr[i] = sum[i] - sum[i - lowbit(i)];
}
方法 2 #
上の図を観察すると、O(logN) ツリー構築の場合、C[x] が更新されると、すべての親ノードが更新されることがわかります。これにより、C[x] が親ノードを複数回更新することになり、多くの計算が繰り返されることになります。
また、 C[x] の親ノードが C[x+lowbit(x)] であることもわかります。次に、1 から n に進み、各 C[i] ノードがその親ノードを 1 回だけ更新できるようにします。
このようにして、O(N) を達成してツリー配列を構築することもできます。さらに、この方法は方法 1 よりも問題が少なく、事前にプレフィックス合計を前処理する必要がありません。
コード
int tr[N]; // 树状数组数据
int a[N]; // 原数组数据
int n; // 数列长度
int lowbit(int x) { return x & -x; }
// 建树
void build() {
for (int i = 1; i <= n; i++) {
tr[i] += a[i];
int fa = i + lowbit(i); // 获得父节点下标
if (fa <= n) // 判断父节点是否超出数列范围
tr[fa] += tr[i];
}
}
メンテナンス間隔と#
単一ポイント変更、間隔クエリ#
長さ n の配列を指定すると、その配列に対して次の 2 つの操作を Q 回実行します。
1 x y
: 位置 x の数値を y に加算します (または y を減算し、y に変更し、y を掛けます)。2 x y
: 区間 [x,y] の合計を問い合わせます。
これはツリー配列の最も基本的な使用法です。
時間の複雑さ
- 単一点修正 O(logN)
- 間隔クエリ O(logN)
コード
int tr[N];
int a[N];
int n;
int lowbit(int x) { return x & -x; }
// 给 x 位置的数加上 c
void add(int x, int c) {
for (int i = x; i <= n; i += lowbit(i))
tr[i] += c;
}
// 查询 1 ~ x 的区间和
void query(int x) {
int res = 0;
for (int i = x; i; i -= lowbit(i))
res += tr[i];
return res;
}
// 使用
add(x, c); // 给 x 位置的数加上 c
add(x, y - (query(x) - query(x - 1))); // 讲 x 位置的数改为 y
int val1 = query(x); // 查询 [1, x] 的区间和
int val2 = query(r) - query(l - 1); // 查询 [l, r] 的区间和
int val3 = query(x) - query(x - 1); // 查询 x 位置的值
間隔変更、単一ポイントクエリ#
長さ n の配列を指定すると、その配列に対して次の 2 つの操作を Q 回実行します。
1 x y k
: 区間 [x,y] 内のすべての数値に k を加算します (またはすべての数値から k を減算します)。2 x
: x 位置の値を問い合わせます
ここでは、差分配列を維持するためにツリー配列を使用する必要があります。
- 間隔の変更:
add(l, k), add(r + 1, -k);
- シングルポイントクエリ:
query(y) - query(x - 1);
時間の複雑さ
- 間隔変更: O(logN)
- シングルポイントクエリ: O(logN)
コード
int tr[N];
int a[N];
int n;
int lowbit(int x) { return x & -x; }
// 给 x 位置的数加上 c
void add(int x, int c) {
for (int i = x; i <= n; i += lowbit(i)) tr[i] += c;
}
// 查询 1 ~ x 的区间和
void query(int x) {
int res = 0;
for (int i = x; i; i -= lowbit(i)) res += tr[i];
return res;
}
// 使用
add(r, c), add(l - 1, c); // 讲区间 [l, r] 都加上 c
int val = query(x) + a[x]; // 查询 x 位置的值
間隔の変更、間隔のクエリ#
長さ n の配列を指定すると、その配列に対して次の 2 つの操作を Q 回実行します。
1 x y k
: 区間 [x,y] 内のすべての数値に k を加算します (またはすべての数値から k を減算します)。2 x y
: [x,y] の間隔の合計を問い合わせます。
この種の問題に遭遇した場合、通常は線分ツリーを使用して解決することを選択しますが、ツリー配列も実装できます。
ここでは、まず差分配列を使用して実装することを考えますが、間隔の合計をクエリするにはどうすればよいでしょうか?
シーケンス a[i] の場合、その差分配列は b[i]=a[i] - a[i-1] であり、a[i] の値は b[i] のプレフィックスの合計です。次に、 a[i] の接頭辞の合計については、次のようになります。
次に、 (1)∑i=1xai=a1+a2+a3+...+ax(2)=b1(3)+b1+b2(4)+b1+b2+b3(5) があります。 +b1+b2+b3+b4(6)⋮(7)+b1+b2+b3+b4+⋯+bx(8) すると、 ∑i=1xai=∑i=1x∑j=1ibj
列挙した式を補足すると、下図のような行列になります。
列に基づいて合計すると、プレフィックス合計の式は次のように変換できます。
(9)∑i=1xai=(b1+b2+b3+...+bx)×(x+1)−(b1+2b2+3b3+...+xbx)(10)=∑i=1xbi−∑i =1xi×bi
このようにして、問題を bi と i×bi のプレフィックス合計配列を維持することに変換し、2 つのツリー配列 tr1 と tr2 を使用してそれぞれ bi と i×bi のプレフィックス合計を維持することができます。
- 間隔クエリ: プレフィックスの合計を取得し、式に基づいて直接計算します。
- 時間計算量: O(logN)
- 間隔の変更: tr1 と tr2 によってそれぞれ維持されるプレフィックス合計に対応する変更を加えます。
- 時間計算量: O(logN)
- tr1 については、次を実行します。
add(x, k), add(y + 1, -k);
- tr2 の場合は、次を実行します。
add(x, x * k), add(y + 1, (y + 1) * k);
コード
#define int long long
int tr1[N]; // 维护 b[i] 的前缀和
int tr2[N]; // 维护 i * b[i] 的前缀和
int a[N]; // 原数组
int n;
int lowbit(int x) { return x & -x; }
// 对树状数组 tr[] 执行加和操作
void add(int tr[], int x, int c) {
for (int i = x; i <= n; i += lowbit(i)) tr[i] += c;
}
// 对树状数组 tr[] 执行查询前缀和的操作
int query(int tr[], int x) {
int res = 0;
for (int i = x; i; i -= lowbit(i)) res += tr[i];
return res;
}
// 建树
void build() {
for (int i = 1; i <= n; i++) {
int b = a[i] - a[i - 1]; // 差分 b[i]
add(tr1, i, b);
add(tr2, i, i * b);
}
}
// 查询数列的前缀和
int pre_sum(int x) {
return query(tr1, x) * (x + 1) - query(tr2, x);
}
// 执行操作
// 建树(初始化)
build();
// 区间查询
int val = pre_sum(y) - pre_sum(x - 1); // [x, y] 的区间和
// 区间修改
add(tr1, x, k), add(tr1, y + 1, -k); // 修改 tr1[]
add(tr2, x, x * k), add(tr2, y + 1, (y + 1) * -k); // 修改 tr2[]
間隔合計完了コードの統合メンテナンス、間隔変更と間隔クエリをサポート (関数は適切にカプセル化されています)
2次元部分行列の和を維持する(2次元ツリー配列) #
単一点変更、部分行列クエリ#
n×m 行列 A が与えられた場合、行列に対して次の 2 つの演算を Q 回実行します。
1 x y k
: 要素 Ax、y に k を加算します (または両方から k を減算します)。2 a b c d
: 左上隅の (a,b) と右上隅の (c,d) を使用して、部分行列内のすべての数値の合計をクエリします。
2 次元ツリー配列は、ツリー配列内のツリー配列です。元の 1 次元ツリー配列に基づいて、このツリー配列のノードを使用してツリー配列を作成し、それによって行列の和を維持する機能を実現します。
ツリー配列の変更ロジック、つまり、あるノードが変更されたときに、影響を受けるノードの数を考え、影響を受けるノードを変更します。したがって、行列 A のノードを変更すると、1 次元ツリー配列のノード値に影響が生じ、対応する変更が行われます。同様に、1 次元ツリー配列への変更は 2 次元ツリー配列のノード値にも影響するため、対応する変更を行う必要があります。
1 次元ツリー配列の変更は O(logN) であるため、logN ノードに影響します。1 次元ツリー配列の変更されたノードごとに、2 次元ツリー配列のノード値を更新するには O(logN) が必要です。
したがって、変更操作の時間計算量は O(log2N) です。
2次元プレフィックス和の初期化については、 sum[i][j] = sum[i - 1][j] + sum[i][j - 1] - sum[i - 1][j - 1] + a[i][j];
(特に説明はありません。知らない方は先に覚えておいてください。以下同様)があります。
同様に、クエリ操作の場合、2 次元の接頭辞の合計によって部分行列を見つけるための公式は、 であることがわかりますSum = sum[x2][y2] - sum[x1 - 1][y2] - sum[x2][y1 - 1] + sum[x1 - 1][y1 - 1];
。
次に、管理者が保持しているプレフィックスの合計を取得し、式に従って結果を計算するだけで済み、時間計算量も O(log2N) になります。
これが 2 次元ツリー配列の基本ロジックとなり、行列の和を維持する機能が実現されます。
時間の複雑さ
-
初期化: N2log2N
-
単一点変更: O(log2N)
-
部分行列クエリ: O(log2N)
コード
#define int long long
int tr[N][N]; // 二维树状数组
int a[N][N]; // 原数组
int n, m; // 行高和列宽
int lowbit(int x) { return x & -x; }
// 给 (x, y) 位置的数加上 c
void add(int x, int y, int c) {
for (int i = x; i <= n; i += lowbit(i))
for (int j = y; j <= m; j += lowbit(j))
tr[i][j] += c;
}
// 查询 (x, y) 位置的二维前缀和
int query(int x, int y) {
int res = 0;
for (int i = x; i; i -= lowbit(i))
for (int j = y; j; j -= lowbit(j))
res += tr[i][j];
return res;
}
// 建立二维树状数组(初始化)
void build() {
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= m; j++) {
int val = query(i - 1, j)
+ query(i, j - 1)
- query(i - 1, j - 1)
+ a[i][j];
add(i, j, val);
}
}
}
// // 查询左上角为(x1, y1), 右下角为(x2, y2) 的子矩阵的和
int query(int x1, int y1, int x2, int y2) {
return query(x2, y2)
- query(x1 - 1, y2)
- query(x2, y1 - 1)
+ query(x1 - 1, y1 - 1);
}
// 使用
build(); // 初始化
add(x, y, c); // 给 (x, y) 位置的数加上 c
add(x, y, -c); // 给 (x, y) 位置的数减去 c
int sum1 = query(x, y); // 查询左上角为(1, 1), 右下角为(x, y) 的子矩阵的和
int sum2 = query(a, b, c, d); // 查询左上角为(a, b), 右下角为(c, d) 的子矩阵的和
部分行列の変更、単一点クエリ#
n×m 行列 A が与えられた場合、行列に対して次の 2 つの演算を Q 回実行します。
1 a b c d k
: 左上隅に (a,b) を、右上隅に (c,d) を指定して、部分行列の各要素に k を加算します (または両方から k を減算します)。2 x y
: 要素 Ax,y の値を問い合わせます。
上記の間隔変更および単一点クエリと同じですが、これは 1 次元の差分配列を維持するために 1 次元のツリー配列を使用します。同様に、2 次元ツリー配列を使用して 2 次元差分配列を維持することもできます。
2 次元の差分配列の場合、各行列変更操作は であり、b[x1][y1] += c, b[x2 + 1, y1] -= c, b[x1, y2 + 1] -= c, b[x2 + 1][y2 + 1] += c;
各単一点クエリ操作は 2 次元の接頭辞の合計を見つけることです。
時間の複雑さ
- 部分行列の修正: O(log2N)
- シングルポイントクエリ: O(log2N)
コード
#define int long long
int tr[N][N]; // 二维树状数组
int a[N][N]; // 原数组
int n, m; // 行高和列宽
int lowbit(int x) { return x & -x; }
void add(int x, int y, int c) {
for (int i = x; i <= n; i += lowbit(i))
for (int j = y; j <= m; j += lowbit(j))
tr[i][j] += c;
}
void query(int x, int y) {
int res = 0;
for (int i = x; i; i -= lowbit(i))
for (int j = y; j; j -= lowbit(j))
res += tr[i][j];
return res;
}
// 将左上角为 (x1, y1), 右下角为 (x2, y2) 的子矩阵的每个元素都加上 c
void add(int x1, int y1, int x2, int y2, int c) {
add(x1, y1, c);
add(x2 + 1, y1, -c);
add(x1, y2 + 1, -c);
add(x2 + 1, y2 + 1, c);
}
// 使用
add(x1, y1, x2, y2, c); // 将左上角为 (x1, y1), 右下角为 (x2, y2) 的子矩阵的每个元素都加上 c
int val = query(x, y) + a[x][y]; // 查询 (x, y) 位置的元素值
部分行列の変更、部分行列のクエリ#
n×m 行列 A が与えられた場合、行列に対して次の 2 つの演算を Q 回実行します。
1 a b c d k
: 左上隅に (a,b) を、右上隅に (c,d) を指定して、部分行列の各要素に k を加算します (または両方から k を減算します)。2 a b c d
: 左上隅の (a,b) と右上隅の (c,d) を使用して、部分行列内のすべての数値の合計をクエリします。
上記の 1 次元区間合計の場合と同じように考えて、プレフィックス合計の 2 次元配列を維持することで問題を解決できます。
具体的なアイデアや導出プロセスについては詳しく説明しませんが、さらに詳しく知りたい場合は、このブログを参照してください:データ構造学習ノート - 二次元ツリー配列 - Zhihu
具体的なアイデアは、4 つの 2 次元ツリー配列を使用して、それぞれ di,j、(i−1)di,j、(j−1)di,j、(i−1)(j−1)di,j を維持することです。 . 2 次元のプレフィックス合計配列。
次に、派生式を使用してプレフィックスの合計が計算されます。
sn,m=nm∑i=1n∑j=1mdi,j−m∑i=1n∑j=1m(i−1)di,j−n∑i=1n∑j=1m(j−1)di、 j+∑i=1n∑j=1m(i−1)(j−1)di,j
コード
#define int long long
int a[N][N], b[N][N], c[N][N], d[N][N]; // 二维树状数组
int n, m;
int lowbit(int x) { return x & -x; }
void add(int x, int y, int v) {
for (int i = x; i <= n; i += lowbit(i)) {
for (int j = y; j <= m; j += lowbit(j)) {
a[i][j] += v;
b[i][j] += (x - 1) * v;
c[i][j] += (y - 1) * v;
d[i][j] += (x - 1) * (y - 1) * v;
}
}
}
int query(int x, int y) {
int res = 0;
for (int i = x; i; i -= lowbit(i)) {
for (int j = y; j; j -= lowbit(j)) {
res += x * y * a[i][j]
- y * b[i][j]
- x * c[i][j]
+ d[i][j];
}
}
return res;
}
// 将左上角为 (x1, y1), 右上角 (x2, y2) 的子矩阵的所有元素加上 c
void add(int x1, int y1, int x2, int y2, int c) {
add(x1, y1, v);
add(x1, y2 + 1, -v);
add(x2 + 1, y1, -v);
add(x2 + 1, y2 + 1, v);
}
// 查询左上角为 (x1, y1), 右上角 (x2, y2) 的子矩阵的元素和
int query(int x1, int y1, int x2, int y) {
return query(x2, y2)
- query(x1 - 1, y2)
- query(x2, y1 - 1)
- query(x1 - 1, y1 - 1);
}
// 使用
add(x1, y1, x2, y2, c); // 将左上角为 (x1, y1), 右上角 (x2, y2) 的子矩阵的所有元素加上 c
int sum = query(x1, y1, x2, y2);// 查询左上角为 (x1, y1), 右上角 (x2, y2) 的子矩阵的元素和
逆順にペアの数を求めます#
長さ n の配列を指定して、逆順にペアの数を求めます。
逆ペア: 1≤i<j≤n の場合、 ai>aj があります。
マージソートはシーケンス内の逆順ペアの数を見つけることができ、時間計算量は O(logN) です。ツリー配列もこのような問題を解決でき、時間計算量も O(logN) であり、空間計算量はマージ ソートよりも低くなります。
数値を逆順に解く場合、ツリー配列は、各 ai の左側にある数値よりも大きい数値の数を見つけ、それらをすべて加算することによって取得されます。横断するたびに確認することが明らかに不可能な場合、各 ai の左側にあるそれより大きい数字の数をどのように見つけますか?
1 から n までは、ai を添字要素として使用し、ai の位置の数値を +1 します。次に、毎回 1∼ai の区間和をクエリし、得られる値は、1∼i 内の ai 以下の要素の数 (ai 自体を含む) です。このとき、1∼i のうち ai より大きい要素の数は i−sum[1,ai] となります。
このようにして、1~n を走査し、毎回 O(logN) でプレフィックス合計クエリと単一点変更を実行するため、合計の時間計算量は O(NlogN) になります。
また、このアプローチは ai を添字として計算することです。ai が負の数または非常に大きな数の場合は、離散化演算を追加する必要があります。
この場合、ツリー配列の時間とスペースの消費量は、マージ ソートの消費時間とスペースの消費量よりも多くなります(ただし、合計の時間とスペースの複雑さは同じです)。実際、これは逆順ペアを見つけるためのマージ ソートの利点を反映しており、ai の値の範囲を考慮する必要はなく、それぞれに長所と短所があるとしか言えません。
コード
#include <bits/stdc++.h>
using namespace std;
typedef long long LL;
const int N = 1e5 + 10;
int tr[N];
int a[N];
int n;
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;
}
int main() {
cin >> n;
for (int i = 1; i <= n; i++) scanf("%d", &a[i]);
LL res = 0;
for (int i = 1; i <= n; i++) {
add(a[i], 1);
// 求逆序对的个数
res += i - query(a[i]);
}
cout << res << "\n";
return 0;
}
離散化演算を必要とするコード
シーケンス内の x より小さい要素の数を見つけます#
逆順ペアを見つける上記の考え方によれば、シーケンス内の xより小さい (より大きい、以下、以上)要素の数を見つけることができます。
同様に、シーケンス内に負の数値がある場合、または数値が非常に大きい場合は、離散化に O(NlogN) が必要になります。
ここで、このメソッドはオフライン クエリのみをサポートし、前処理の時間計算量は O(NlogN)、各クエリの時間計算量は O(logN) であることに注意してください。
コード
#include <bits/stdc++.h>
using namespace std;
const int N = 1e5 + 10;
int tr[N];
int a[N];
int n;
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;
}
int main() {
cin >> n;
for (int i = 1; i <= n; i++) scanf("%d", &a[i]);
// 预处理
for (int i = 1; i <= n; i++)
add(a[i], 1);
// 查询
int x;
cin >> x;
int num1 = query(x - 1); // 查询小于 x 的元素个数
int num2 = query(x); // 查询小于等于 x 的元素个数
int num3 = n - query(x); // 查询大于 x 的元素个数
int num4 = n - query(x - 1);// 查询大于等于 x 的元素个数
return 0;
}