セグメント ツリーは、間隔情報を維持するために一般的に使用されるデータ構造であり、単一点の変更、間隔の変更、および間隔のクエリ (間隔の合計、間隔の最大値または間隔の最小値) を O(logn) の時間計算量で実装できます。操作は、RMQ の問題を解決するためによく使用されます。
RMQ (範囲最小値/最大値クエリ) 質問は次のことを指します: 長さ n のシーケンス A について、いくつかのクエリ RMQ(A, i, j) (i, j <= n) に答え、i のシーケンス A の添字を返します。 j の最小(最大)値。言い換えれば、RMQ 問題は、間隔の最大値を見つける問題を指します。通常、この種の問題の解決策には、再帰的分割統治、動的プログラミング、線分ツリー、単調スタック/単調キューが含まれます。
この内容は 2 週間断続的に書きましたが、練習することで線分木の理解が深まり、徐々に覚えていき、難しいと感じることはなくなりました。最初はコード量が多くなりますが、落ち着いて以下の3つの部分を少しずつマスターしていけば、それほど難しいことではないと思います。
1. 線分ツリー
線分ツリーは、長さが1でない各区間を左右2つの区間に分割して再帰的に解を導き、左右の区間の情報をマージすることで現在の区間の情報を取得します。
例えば、サイズ5の配列nums = {10, 11, 12, 13, 14}を線分ツリーに変換し、線分ツリーのルートノード番号を1と指定します。配列tree[]を使用して、線分ツリーのノードを保存します。tree[i]は、以下に示すように、線分ツリー上のi番号のノードを表します。
図の各ノードは区間合計と区間範囲を示しており、tree[i]の左側のサブツリーノードはtree[2i]、右側のサブツリーノードはtree[2i + 1]です。Tree[i] によって記録された間隔が [a, b] の場合、左側のサブツリー ノードによって記録された間隔は [a, Mid]、右側のサブツリー ノードによって記録された間隔は [mid + 1, b] になります。ここで、mid = (a + b) / 2です。
線分ツリーの基本を理解したところで、間隔クエリと単一点変更のコード実装を見てみましょう。
区間クエリと線分ツリーの単一点変更
まず、線分ツリーのノードを定義します。
/**
* 定义线段树节点
*/
class Node {
/**
* 区间和 或 区间最大/最小值
*/
int val;
int left;
int right;
public Node(int left, int right) {
this.left = left;
this.right = right;
}
}
val フィールドには間隔の合計が格納されることに注意してください。ツリーのノードを定義した後、ツリーを構築するロジックを見てみましょう。コード内のコメントに注意してください。セグメント ツリーに割り当てるノード配列のサイズは、元の配列のサイズの 4 倍です。 . これは、配列が完全なバイナリ ツリーに変換される場合の最悪のシナリオです。
public SegmentTree(int[] nums) {
this.nums = nums;
tree = new Node[nums.length * 4];
// 建树,注意表示区间时使用的是从 1 开始的索引值
build(1, 1, nums.length);
}
/**
* 建树
*
* @param pos 当前节点编号
* @param left 当前节点区间下界
* @param right 当前节点区间上界
*/
private void build(int pos, int left, int right) {
// 创建节点
tree[pos] = new Node(left, right);
// 递归结束条件
if (left == right) {
// 赋值
tree[pos].val = nums[left - 1];
return;
}
// 如果没有到根节点,则继续递归
int mid = left + right >> 1;
build(pos << 1, left, mid);
build(pos << 1 | 1, mid + 1, right);
// 当前节点的值是左子树和右子树节点的和
pushUp(pos);
}
/**
* 用于向上回溯时修改父节点的值
*/
private void pushUp(int pos) {
tree[pos].val = tree[pos << 1].val + tree[pos << 1 | 1].val;
}
ツリーを構築するとき、表現間隔はインデックス 0 からではなく、インデックス 1 から始まります。これは、左側のサブツリー ノード インデックスを計算するときに、それが 2i になり、右側のサブツリー ノード インデックスが 2i + 1 になるようにするためです。
build()
メソッドを実行すると、まず値を代入せずに対応する位置にノードを作成します。葉ノードへの再帰時にのみ値を代入します。このときの間隔サイズは 1、ノード値は現在の間隔の値。その後、非リーフ ノードの値は、pushUp()
現在のノードの 2 つの子ノードの値を遡って加算することによって計算されます。
次に、間隔内の値の変更と、線分ツリーが値を更新する方法を見てみましょう。コメントに注目してください。
/**
* 修改单节点的值
*
* @param pos 当前节点编号
* @param numPos 需要修改的区间中值的位置
* @param val 修改后的值
*/
private void update(int pos, int numPos, int val) {
// 找到该数值所在线段树中的叶子节点
if (tree[pos].left == numPos && tree[pos].right == numPos) {
tree[pos].val = val;
return;
}
// 如果不是当前节点那么需要判断是去左或右去找
int mid = tree[pos].left + tree[pos].right >> 1;
if (numPos <= mid) {
update(pos << 1, numPos, val);
} else {
update(pos << 1 | 1, numPos, val);
}
// 叶子节点的值修改完了,需要回溯更新所有相关父节点的值
pushUp(pos);
}
変更方法は比較的単純ですが、リーフ ノードの値が更新されると、pushUp()
関連するすべての親ノードの値を更新するメソッドを呼び出す必要があります。
次に、対応する間隔の合計を見つける方法を見ていきます。
/**
* 查找对应区间的值
*
* @param pos 当前节点
* @param left 要查询的区间的下界
* @param right 要查询的区间的上界
*/
private int query(int pos, int left, int right) {
// 如果我们要查找的区间把当前节点区间全部包含起来
if (left <= tree[pos].left && tree[pos].right <= right) {
return tree[pos].val;
}
int res = 0;
int mid = tree[pos].left + tree[pos].right >> 1;
// 根据区间范围去左右节点分别查找求和
if (left <= mid) {
res += query(pos << 1, left, right);
}
if (right > mid) {
res += query(pos << 1 | 1, left, right);
}
return res;
}
この方法も比較的単純で、間隔範囲が左の子ノードと右の子ノードに対して別々の検索と計算を必要とするかどうかを判断する必要があります。
区間の合計を表す線分ツリーについては説明しましたが、誰もがコードを学習して読みやすくするために、完全なコードをここに投稿しました。
public class SegmentTree {
/**
* 定义线段树节点
*/
static class Node {
/**
* 区间和 或 区间最大/最小值
*/
int val;
int left;
int right;
public Node(int left, int right) {
this.left = left;
this.right = right;
}
}
Node[] tree;
int[] nums;
public SegmentTree(int[] nums) {
this.nums = nums;
tree = new Node[nums.length * 4];
// 建树,注意表示区间时使用的是从 1 开始的索引值
build(1, 1, nums.length);
}
/**
* 建树
*
* @param pos 当前节点编号
* @param left 当前节点区间下界
* @param right 当前节点区间上界
*/
private void build(int pos, int left, int right) {
// 创建节点
tree[pos] = new Node(left, right);
// 递归结束条件
if (left == right) {
// 赋值
tree[pos].val = nums[left - 1];
return;
}
// 如果没有到根节点,则继续递归
int mid = left + right >> 1;
build(pos << 1, left, mid);
build(pos << 1 | 1, mid + 1, right);
// 当前节点的值是左子树和右子树节点的和
pushUp(pos);
}
/**
* 修改单节点的值
*
* @param pos 当前节点编号
* @param numPos 需要修改的区间中值的位置
* @param val 修改后的值
*/
private void update(int pos, int numPos, int val) {
// 找到该数值所在线段树种的叶子节点
if (tree[pos].left == numPos && tree[pos].right == numPos) {
tree[pos].val = val;
return;
}
// 如果不是当前节点那么需要判断是去左或右去找
int mid = tree[pos].left + tree[pos].right >> 1;
if (numPos <= mid) {
update(pos << 1, numPos, val);
} else {
update(pos << 1 | 1, numPos, val);
}
// 叶子节点的值修改完了,需要回溯更新所有相关父节点的值
pushUp(pos);
}
/**
* 用于向上回溯时修改父节点的值
*/
private void pushUp(int pos) {
tree[pos].val = tree[pos << 1].val + tree[pos << 1 | 1].val;
}
/**
* 查找对应区间的值
*
* @param pos 当前节点
* @param left 要查询的区间的下界
* @param right 要查询的区间的上界
*/
private int query(int pos, int left, int right) {
// 如果我们要查找的区间把当前节点区间全部包含起来
if (left <= tree[pos].left && tree[pos].right <= right) {
return tree[pos].val;
}
int res = 0;
int mid = tree[pos].left + tree[pos].right >> 1;
// 根据区间范围去左右节点分别查找求和
if (left <= mid) {
res += query(pos << 1, left, right);
}
if (right > mid) {
res += query(pos << 1 | 1, left, right);
}
return res;
}
}
間隔の最大値または最小値を表す線分ツリーを作成する場合、ツリーを構築するロジックは変更されず、**pushUp() メソッドと query()** メソッドを次のように変更するだけで済みます。最大値または最小値を計算するロジック。
2. 線分ツリーの間隔変更と遅延マーキング
単一のポイントを変更するだけでなく、間隔も変更する場合、間隔を変更するときに、現在の間隔値と現在の間隔を含むサブ間隔値を変更する必要があります。これによって生成されるオーバーヘッドは許容できないため、ここではこの即時のオーバーヘッドを回避するために、遅延マークアップアプローチを使用します。
簡単に言えば、遅延マーキングはノード情報への変更を遅らせ、それによって潜在的に不必要な操作の数を減らします。変更が実行されるたびに、マーキング方法を使用して、ノードに対応する間隔が特定の操作で変更されたが、ノードの子ノードの情報は更新されないことを示します。大幅な変更は、レイジー マークが付いたノードの子ノードに対して次回「今後のアクセス (更新またはクエリ)」が発生したときにのみ行われます。
ノード クラスに add フィールドを追加することで遅延マークを記録します。これは、間隔のサブ間隔値の「変更サイズ」を示し(よく理解する必要があります)、次の 2 つのサブ間隔に「累積」されます。 PushDown メソッドによる現在の間隔。ノード間隔値に含まれます。
現在の区間のサブ区間にアクセスしない限り、サブ区間の値は変化せず、サブ区間の値に相当する変化量が加算フィールドを介して現在のノードに「保持」される。
PushDownメソッドは、前述のpushUpメソッドとは異なり、前者は現在のノード値の累積レイジーマーク値を子ノードに同期するのに対し、後者は現在の子ノードの変更完了後に遡って現在の子ノードの親ノード値を変更します。 Down と Up に基づいて、これら 2 つのメソッドの方向と変更範囲をよりよく理解できます。
プロセスと具体的なコードを見てみましょう. ノード クラスは次のようになり、add フィールドが追加されます。
static class Node {
int left;
int right;
int val;
int add;
public Node(int left, int right) {
this.left = left;
this.right = right;
}
}
間隔の変更
ツリーを構築するプロセスは、上で述べたことと一致するため、ここでは詳細には説明しませんが、次の初期線分ツリーを例として、主に区間変更のプロセスに焦点を当てます。各ノードの add は 0 です。
次に、区間 [3, 5] を変更します。区間内の各値の変化は 1 です。プロセスは次のとおりです。
最初にノード 1 を走査し、区間 [3, 5]が区間 [1, 5]を完全に含むことができないことがわかりました。変更は加えず、ノード 2 の走査を続けます。ノード 2 はまだ間隔 [3, 5] に含まれていません。ノード 5 のトラバースを続けて、ノードが完全に間隔に含まれていることを確認する必要があります。次の図に示すように、ノード 2 を変更してレイジー マーク値を追加します。
この手順を完了したら、元に戻して、tree[2] ノードの値を変更する必要があります。
区間 [3, 5] の 3 が変更され、4, 5 は変更されていません。右のサブツリーで再帰的検索を続行し、tree[3] の区間 [3, 5] が次のとおりであることを確認する必要があります。変更したい間隔。] が完全に含まれている場合、次のようにこのノードを変更して遅延マークする必要があります。tree[3] ノードには 2 つの子ノードがありますが、その子ノードにはアクセスしていないため、追加値を各子ノードに同期する必要はありません。:
同様に、このステップを完了するには、親ノードの値を変更するためにバックトラックする必要もあります。
ここまでで間隔の変更は完了しました。このプロセスによると、コード例は次のようになります。
/**
* 修改区间的值
*
* @param pos 当前节点编号
* @param left 要修改区间的下界
* @param right 要修改区间的上界
* @param val 区间内每个值的变化量
*/
public void update(int pos, int left, int right, int val) {
// 如果该区间被要修改的区间包围的话,那么需要将该区间所有的值都修改
if (left <= tree[pos].left && tree[pos].right <= right) {
tree[pos].val += (tree[pos].right - tree[pos].left + 1) * val;
// 懒惰标记
tree[pos].add += val;
return;
}
// 该区间没有被包围的话,需要修改节点的信息
pushDown(pos);
int mid = tree[pos].left + tree[pos].right >> 1;
// 如果下界在 mid 左边,那么左子树需要修改
if (left <= mid) {
update(pos << 1, left, right, val);
}
// 如果上界在 mid 右边,那么右子树也需要修改
if (right > mid) {
update(pos << 1 | 1, left, right, val);
}
// 修改完成后向上回溯修改父节点的值
pushUp(pos);
}
private void pushDown(int pos) {
// 根节点 和 懒惰标记为 0 的情况不需要再向下遍历
if (tree[pos].left != tree[pos].right && tree[pos].add != 0) {
int add = tree[pos].add;
// 计算累加变化量
tree[pos << 1].val += add * (tree[pos << 1].right - tree[pos << 1].left + 1);
tree[pos << 1 | 1].val += add * (tree[pos << 1 | 1].right - tree[pos << 1 | 1].left + 1);
// 子节点懒惰标记累加
tree[pos << 1].add += add;
tree[pos << 1 | 1].add += add;
// 懒惰标记清 0
tree[pos].add = 0;
}
}
private void pushUp(int pos) {
tree[pos].val = tree[pos << 1].val + tree[pos << 1 | 1].val;
}
間隔クエリ
Tree[3] ノードには遅延マーク 1 があります。このとき区間 [5, 5] の値を問い合わせる場合、tree[3] ノードを再帰的に通過するときに PushDown 遅延マーク計算を実行し、追加する必要があります。 Tree[6] と Tree[7] のノード値が変更され、結果は次のようになります。
最終的に、結果値は 15 として取得されます。間隔クエリ処理のサンプル コードは次のとおりです。
public int query(int pos, int left, int right) {
if (left <= tree[pos].left && tree[pos].right <= right) {
// 当前区间被包围
return tree[pos].val;
}
// 懒惰标记需要下传修改子节点的值
pushDown(pos);
int res = 0;
int mid = tree[pos].left + tree[pos].right >> 1;
if (left <= mid) {
res += query(pos << 1, left, right);
}
if (right > mid) {
res += query(pos << 1 | 1, left, right);
}
return res;
}
同様に、皆さんの学習を容易にするために、コード全体もリストアップしましたが、線分ツリーの区間変更を学習する上で最も重要なポイントは、addフィールドの意味とpushDownメソッドのタイミングを理解することだと思います。また、線分ツリーの特定の領域のみが、変更したい間隔(更新メソッドとクエリメソッドの条件判断)に各間隔が完全に含まれている場合、値は遅延的に変更され、マークされることにも注意する必要があります。間隔値は、pushUp メソッドがバックトラックする場合にのみ変更されます。
public class SegmentTree2 {
static class Node {
int left;
int right;
int val;
int add;
public Node(int left, int right) {
this.left = left;
this.right = right;
}
}
Node[] tree;
int[] nums;
public SegmentTree2(int[] nums) {
this.tree = new Node[nums.length * 4];
this.nums = nums;
build(1, 1, nums.length);
}
private void build(int pos, int left, int right) {
tree[pos] = new Node(left, right);
// 递归结束条件
if (left == right) {
tree[pos].val = nums[left - 1];
return;
}
int mid = left + right >> 1;
build(pos << 1, left, mid);
build(pos << 1 | 1, mid + 1, right);
// 回溯修改父节点的值
pushUp(pos);
}
/**
* 修改区间的值
*
* @param pos 当前节点编号
* @param left 要修改区间的下界
* @param right 要修改区间的上界
* @param val 区间内每个值的变化量
*/
public void update(int pos, int left, int right, int val) {
// 如果该区间被要修改的区间包围的话,那么需要将该区间所有的值都修改
if (left <= tree[pos].left && tree[pos].right <= right) {
tree[pos].val += (tree[pos].right - tree[pos].left + 1) * val;
// 懒惰标记
tree[pos].add += val;
return;
}
// 该区间没有被包围的话,需要修改节点的信息
pushDown(pos);
int mid = tree[pos].left + tree[pos].right >> 1;
// 如果下界在 mid 左边,那么左子树需要修改
if (left <= mid) {
update(pos << 1, left, right, val);
}
// 如果上界在 mid 右边,那么右子树也需要修改
if (right > mid) {
update(pos << 1 | 1, left, right, val);
}
// 修改完成后向上回溯修改父节点的值
pushUp(pos);
}
public int query(int pos, int left, int right) {
if (left <= tree[pos].left && tree[pos].right <= right) {
// 当前区间被包围
return tree[pos].val;
}
// 懒惰标记需要下传修改子节点的值
pushDown(pos);
int res = 0;
int mid = tree[pos].left + tree[pos].right >> 1;
if (left <= mid) {
res += query(pos << 1, left, right);
}
if (right > mid) {
res += query(pos << 1 | 1, left, right);
}
return res;
}
private void pushDown(int pos) {
// 根节点 和 懒惰标记为 0 的情况不需要再向下遍历
if (tree[pos].left != tree[pos].right && tree[pos].add != 0) {
int add = tree[pos].add;
// 计算累加变化量
tree[pos << 1].val += add * (tree[pos << 1].right - tree[pos << 1].left + 1);
tree[pos << 1 | 1].val += add * (tree[pos << 1 | 1].right - tree[pos << 1 | 1].left + 1);
// 子节点懒惰标记
tree[pos << 1].add += add;
tree[pos << 1 | 1].add += add;
// 懒惰标记清 0
tree[pos].add = 0;
}
}
private void pushUp(int pos) {
tree[pos].val = tree[pos << 1].val + tree[pos << 1 | 1].val;
}
}
3. 線分ツリーの動的開始点
線分ツリーの動的開始点は、実際には理解するのは難しくありません。前述した線分ツリーのすべてのノードを直接作成するのとは異なります。線分ツリーの動的開始点は、最初にルートを 1 つだけ作成します。ノードは間隔全体を表し、他のノードは必要な場合にのみ作成されるため、スペースが節約されます。もちろん、現在のノードの左と右の子ノードを見つけるために and を使用することはできなくなり、代わりに、ノード内で left と right を使用して、tree[] の左と右の子ノードの位置を記録しますpos << 1
。pos << 1 | 1
注意すべきこと:
static class Node {
// left 和 right 不再表示区间范围而是表示左右子节点在 tree 中的索引位置
int left, right;
int val;
int add;
}
区間 [1, 5] を例に、区間 [5, 5] を 14 として作成するプロセスは次のとおりです。
上図ではデフォルトのルートノードのtree[1]が最初に作成され、次にtree[2]とtree[3]のノードが作成されることがわかりますが、区間[5,5]が見つかりません。上図のtree[4]ノードとtree[5]ノード(すべてのノードを直接作成するのとは異なり、すべてのノードを直接作成する場合、その位置はtree[6]内にある必要があります) ] と Tree[7])、今度は Tree[5] ノードによって表される間隔は、探している条件を満たしており、割り当てとプッシュアップ操作を実行できます。すべてのノードを直接作成する場合と比較して、ポイントを動的に開くと、作成されるノードが 4 つ少なくなりますは、図で赤くマークされた 4 つのノードです。は作成されません。
各操作は新しい一連のノードを作成してアクセスする可能性があるため、m 回の単一点操作後のノードの空間複雑さは O(mlogn) です。問題を解決するために線分ツリーを使用して点を動的に開く場合、空間は次のようにする必要があります。サイズが大きい場合もありますが、Java は 128M で 5e6 ノードを超えるまで開くことができます。
図に基づいて、ポイントを動的に開くプロセスを理解できるはずです~~ (理解できない場合は、自分で描いてください)~~、具体的なコードを見てみましょう:
/**
* 修改区间的值
*
* @param pos 当前节点的索引值
* @param left 当前线段树节点表示的范围下界
* @param right 当前线段树节点表示的范围上界
* @param l 要修改的区间下界
* @param r 要修改的区间上界
* @param val 区间值变化的大小
*/
public void update(int pos, int left, int right, int l, int r, int val) {
// 当前区间被要修改的区间全部包含
if (l <= left && right <= r) {
tree[pos].val += (right - left + 1) * val;
tree[pos].add += val;
return;
}
lazyCreate(pos);
pushDown(pos, right - left + 1);
int mid = left + right >> 1;
if (l <= mid) {
update(tree[pos].left, left, mid, l, r, val);
}
if (r > mid) {
update(tree[pos].right, mid + 1, right, l, r, val);
}
pushUp(pos);
}
// 为该位置创建节点
private void lazyCreate(int pos) {
if (tree[pos] == null) {
tree[pos] = new Node();
}
// 创建左子树节点
if (tree[pos].left == 0) {
tree[pos].left = ++count;
tree[tree[pos].left] = new Node();
}
// 创建右子树节点
if (tree[pos].right == 0) {
tree[pos].right = ++count;
tree[tree[pos].right] = new Node();
}
}
private void pushDown(int pos, int len) {
if (tree[pos].left != 0 && tree[pos].right != 0 && tree[pos].add != 0) {
// 计算左右子树的值
tree[tree[pos].left].val += (len - len / 2) * tree[pos].add;
tree[tree[pos].right].val += len / 2 * tree[pos].add;
// 子节点懒惰标记
tree[tree[pos].left].add += tree[pos].add;
tree[tree[pos].right].add += tree[pos].add;
tree[pos].add = 0;
}
}
private void pushUp(int pos) {
tree[pos].val = tree[tree[pos].left].val + tree[tree[pos].right].val;
}
全体的なロジックは難しくありません。新しい LazyCreate メソッドは、ポイントを動的に開くロジックです。間隔更新を実行するとき、メソッドのパラメータには、現在のノードの範囲を表す左パラメータと右パラメータがあることに注意してください。現在のノード には左右の子ノードの位置のみが保存されますが、間隔情報はありません。そのため、パラメータでそれを運ぶ必要があります。そうしないと、現在の間隔が探している間隔と一致するかどうかを判断する方法がありません。のために。
皆さんの便宜のために、完全なコードを以下に示します。
public class SegmentTree3 {
static class Node {
// left 和 right 不再表示区间范围而是表示左右子节点在 tree 中的索引位置
int left, right;
int val;
int add;
}
// 记录当前节点数
int count;
Node[] tree;
public SegmentTree3() {
count = 1;
tree = new Node[(int) 5e6];
tree[count] = new Node();
}
public int query(int pos, int left, int right, int l, int r) {
if (l <= left && right <= r) {
return tree[pos].val;
}
lazyCreate(pos);
pushDown(pos, right - left + 1);
int res = 0;
int mid = left + right >> 1;
if (l <= mid) {
res += query(tree[pos].left, left, mid, l, r);
}
if (r > mid) {
res += query(tree[pos].right, mid + 1, right, l, r);
}
return res;
}
/**
* 修改区间的值
*
* @param pos 当前节点的索引值
* @param left 当前线段树节点表示的范围下界
* @param right 当前线段树节点表示的范围上界
* @param l 要修改的区间下界
* @param r 要修改的区间上界
* @param val 区间值变化的大小
*/
public void update(int pos, int left, int right, int l, int r, int val) {
// 当前区间被要修改的区间全部包含
if (l <= left && right <= r) {
tree[pos].val += (right - left + 1) * val;
tree[pos].add += val;
return;
}
lazyCreate(pos);
pushDown(pos, right - left + 1);
int mid = left + right >> 1;
if (l <= mid) {
update(tree[pos].left, left, mid, l, r, val);
}
if (r > mid) {
update(tree[pos].right, mid + 1, right, l, r, val);
}
pushUp(pos);
}
// 为该位置创建节点
private void lazyCreate(int pos) {
if (tree[pos] == null) {
tree[pos] = new Node();
}
// 创建左子树节点
if (tree[pos].left == 0) {
tree[pos].left = ++count;
tree[tree[pos].left] = new Node();
}
// 创建右子树节点
if (tree[pos].right == 0) {
tree[pos].right = ++count;
tree[tree[pos].right] = new Node();
}
}
private void pushDown(int pos, int len) {
if (tree[pos].left != 0 && tree[pos].right != 0 && tree[pos].add != 0) {
// 计算左右子树的值
tree[tree[pos].left].val += (len - len / 2) * tree[pos].add;
tree[tree[pos].right].val += len / 2 * tree[pos].add;
// 子节点懒惰标记
tree[tree[pos].left].add += tree[pos].add;
tree[tree[pos].right].add += tree[pos].add;
tree[pos].add = 0;
}
}
private void pushUp(int pos) {
tree[pos].val = tree[tree[pos].left].val + tree[tree[pos].right].val;
}
}
巨人の肩
オープンソース フレームワーク NanUI の作者がスチールの販売に切り替えたため、プロジェクトは中断されました。Apple App Store の無料リストのナンバー 1 はポルノ ソフトウェア TypeScript です。人気が出てきたばかりなのに、なぜ大手はそれを放棄し始めるのでしょうか。 ? TIOBE 10月リスト:Javaが最大の下落、C#はJavaに迫る Rust 1.73.0リリース AIガールフレンドにイギリス女王暗殺を勧められた男性に懲役9年の実刑判決 Qt 6.6正式リリース ロイター:RISC-Vテクノロジーが中米テクノロジー戦争の鍵となる 新たな戦場 RISC-V: 単一の企業や国に支配されない レノボ、Android PC の発売を計画著者: JD Logistics 王一龍
出典:JD Cloud Developer Community Ziyuanqishuo Tech 転載の際は出典を明記してください