線分木の始め方を0から教えます~

1. セグメントツリーとは何ですか?

1.1 線分ツリーの事前探索

定義:線分ツリーとは、間隔クエリの問題を解決するために使用されますデータ構造は一般化された二分探索ツリーです。

原則:間隔を複数の小さなサブ間隔に分割最大、最小、合計など、各サブ間隔の有用な情報を保存します。

解決できる問題:線分ツリーは、区間に格納された情報を段階的に要約することにより、合計、最大値、最小値、または特定の区間の値の更新など、さまざまなタイプの区間クエリに迅速に答えることができます。

**時間計算量:** 線分ツリーの構築とクエリ操作の時間計算量はO(logN)です(N は間隔のサイズです)。

制約事項:線分ツリーで解ける問題は区間加算を満たさなければなりません区間加算とは[L,R]、区間について、[L,M]と の[M+1,R]答えを組み合わせることで答えが得られることを意味します。ここで、M は間隔の中点です

1.2 線分木と二分木の違い

配列 の場合[1,2,3,4,5,6]、そのバイナリ ツリーと線分ツリーは次の図に示されています。**「間隔の添字は 0 から始まります」**:

valこの図から、バイナリ ツリーの単一ノードに格納される内容は値であるのに対し、セグメント ツリーは間隔情報を格納することがわかります

セグメント ツリーを見ると、次の結論をすぐに引き出すこともできます。

  • 各ノードの左側と右側の子には、それぞれノード間隔の半分が格納されます。
  • 区間を分割できない場合は葉ノードを取得する

1.3 線分ツリーの添字

次に、配列の添字を線分ツリーの各ノードに追加すると、結果は次のようになります。

i線分ツリーにおいて、ノードの添字が であるとき、その左の子の添字は2 * i + 1、右の子の添字は であることがわかります2 * i + 2このとき考えなければならないのは、配列のサイズがnのとき、線分ツリーの空間はどれくらいにすべきかということです。

答え: 2 * n - 1、完全な二分木では、葉ノードの数は非葉ノードの数から 1 を引いたものに等しくなります線分ツリーでは、葉ノードの数は配列サイズ n に等しく、非葉ノードの数は n - 1 であるため、線分ツリーの空間は2 * n - 1になります。計算を容易にし、配列が境界を越えないようにするために、通常、線分ツリーの空間サイズをノードの総数よりも大きい最小の 2 のべき乗、つまり 4 *n の空間に開くことに注意してください。サイズ。

1.4 線分ツリーの格納内容

セクション 1.1 では次のように述べられています: 線分ツリーは、合計、最大値、最小値など、さまざまなタイプの間隔クエリにすぐに答えることができます。では、総和では、線分ツリーはどのように表現されるのでしょうか?

val各リーフ ノードは配列添字値を格納し、各非リーフ ノードの合計値はその左右の子ノードの格納値の合計に等しいことがわかります。同様に、最大値と最小値ではvalue、非リーフ ノード ストア の値は、その左右の子ノードの中で大きいか小さい値です

2. 問題を解決するための線分ツリーの手順

2.1 設立

線分の情報は線分ツリーに格納されますが、区間の左辺値、区間の右辺値、および合計を格納するクラスを定義する必要はありません。再帰 + 添え字関係によって線分ツリーを作成できるため、線分ツリーのノードは和を格納するだけで済みます。

int nums[] = new int[]{
    
    1, 2, 3, 4, 5, 6};
int n = nums.length;
int[] segTree = new int[4 * n]; // 为线段树分配空间


void buildTree(int index, int left, int right) {
    
     // index 表示下标,left 表示左区间,right 表示右区间
    if (left == right) {
    
    
        segTree[index] = nums[left];
        return; // 到叶子节点就不能继续划分啦~
    }
    int mid = (left + right) / 2; // 一分为 2,例如将 [1,6] 划分为 [1,3] 和 [4,6]
    buildTree(2 * index + 1, left, mid); // 构建左子树,左孩子的下标为 2 * index + 1
    buildTree(2 * index + 2, mid + 1, right); // 构建右子树,右孩子的下标为 2 * index + 2
    segTree[index] = segTree[2 * index + 1] + segTree[2 * index + 2]; // 这里是求和,所以非叶子节点存储的值是左右孩子节点存储的值之和
}

public static void main(String[] args) {
    
    
        Solution solution = new Solution();
        solution.buildTree(0, 0, solution.nums.length - 1);
    }

2.2 単一点変更

単一点変更は、間隔変更の特殊なケースです。線分ツリーがどのように更新されるかを確認するために、単純な単一点変更から始めましょう。

アイデア:配列のを更新したい場合は、区間の左側と区間の右側に等しいルート ノードからノードを見つけて、その値を変更できます。その後、ixi戻る途中でその祖先ノードの値を更新し続けます

public void update(int i, int value) {
    
    
    update(0, 0, nums.length - 1, i, value);
}

private void update(int index, int left, int right, int i, int value) {
    
     // i 表示要更新数组的下标,value 是更改后的值
    if (left == right) {
    
     // 当搜寻到叶子节点的时候,就可以修改了,前提是 i 在[0,2 * n - 2] 之间,下标从 0 开始算
        segTree[left] = value;
        return; // 赋值完就结束
    }
    int mid = (left + right) / 2;
    if (i <= mid) update(2 * index + 1, left, mid, i, value);
    else update(2 * index + 2, mid + 1, right, i, value);
    segTree[index] = segTree[index * 2 + 1] + segTree[index * 2 + 2]; // 更新祖先节点
}

2.3 単一点変更のみの間隔クエリ

線分ツリーの使用条件を覚えていますか?間隔加算を満足する必要があります

したがって、interval をクエリするときは[a,b]、interval の加算を満たすサブ間隔に分割できます。または、例として合計を考えてみましょう: sum[1,5] = sum[1,3] + sum[4,5], sum[2,5] = sum[2,2] + sum[3,3] + sum[4,5].

public int query(int x, int y) {
    
    
        return query(0, 0, nums.length - 1, x, y);
    }

    private int query(int index, int left, int right, int x, int y) {
    
     // x 表示要查询的左区间,y 表示要查询的右区间
        if (x > right || y < left) return 0; // 如果查询区间在线段树区间外返回 0 
        if (x <= left && y >= right) return segTree[index];  // 当查询区间包含线段树区间,返回节点值
        int mid = (left + right) / 2;
        int leftQuery = query(2 * index + 1, left, mid, x, y); // 计算左孩子
        int rightQuery = query(2 * index + 2, mid + 1, right, x, y); // 计算右孩子
        return leftQuery + rightQuery; // 求和 
    }

2.4 間隔の変更

変更する必要があるコンテンツが単一ポイントではなく間隔である場合、for ループを介して単一ポイント ループを呼び出すことはできません。これは総当たりクラッキングと変わらないためです。

この問題を解決するには、新しい概念である遅延マーキング(遅延マーキングとも呼ぶことができます) を導入する必要があります。このマークの意味は、このマークでマークされた間隔値は更新されましたが、そのサブ間隔は更新されておらず更新された情報はマークに格納されている値です

遅延マーカーを導入する間隔の変更は、次の規則に従います。

(1) 変更する間隔が現在の間隔を完全にカバーしている場合は、この間隔を直接更新し、遅延としてマークします。

(2) 完全にカバーされておらず、現在の間隔に遅延マークがある場合は、まず遅延マークをサブ間隔にダウンロードし、次に現在の間隔の遅延マークをクリアします。

(3) 変更された区間が左の息子と交差する場合は左の息子を検索し、右の息子と交差する場合は右の息子を検索します。

(4) 現在の間隔の値を更新します

言葉が多すぎてめまいを感じますか?それは問題ではありません。具体的な例を使用して間隔の変更を見てみましょう~

nums 配列[0,3] の区間内の各数値に 1 を加算すると、配列は になります[2,3,4,5,5,6]線分ツリーでは、最初にルート ノードにアクセスし[0,5]、変更間隔は明らかに間隔を完全にカバーしておらず[0,5]、現在のノードには遅延マークがありません。現在のノードの左側の子を見て[0,2]、明らかに[0,3][0,2]次のノードがあります。交差点、左側の子を検索します。次に、左側の子 Children を
検索します。最初に を完全にカバーし、次にこの間隔を更新します。これは、ノードが合計を記録するためです。この間隔内の各数値に 1 を加算してから、合計を計算する必要があります。 、つまり; 次に、ノードに遅延のマークを付けます。これは、このノードの子ノードが操作されていないことを示します [0,2] [0,3][0,2]sum = sum + 1 * 区间长度sum = 6 + (2 - 0 + 1) * 1 = 9LazyTag = + 1+1


左の子を検索した後、の右の子との交差があること[0,2]がわかり、右の子の検索を開始します。まず、カバレッジがなく遅延マークもありません。左の子と右の子を確認します。それぞれの子を検索し、左の子との交差があることを見つけて、検索を開始します[0,3][0,5][3,5][0,3][3,5][3,5][3,5][3,4][3,4]


[0,3]が完全にカバーされていないことがわかり[3,4][3,4]遅延マーカーが含まれていないノードを見つけて、その子を検索します。[0,3]と が[3,3]交差しており、[0,3]完全に覆われていることが判明した場合は[3,3]、間隔を更新し、sum = sum + 更新值 * 区间长度ie sum = 4 + 1 * ( 3 - 3 + 1) = 5; ノードを遅延としてマークしますLazyTag = +1「リーフ ノードにも遅延マークがあり、分散し続ける必要があるため、そのためには 2 倍のスペースが必要となるため、2 * n - 1 のスペースでは不十分で、4 * n のスペースが必要になります。」


ここまでで検索が終了し、レイヤーごとに間隔値の更新を開始しました。

この時点で注意していれば、遅延マーキング ルールの 2 番目のルール「完全にカバーされておらず、現在の間隔に遅延マーキングがある場合は、最初に遅延マーキングをサブ間隔にダウンロード、先ほどの例では、現在のインターバルの遅延マーキングをクリアします。遅延マークがサブインターバルにダウンロードされるケースはありません。これは、間隔更新を 1 回だけ実行したためであり、複数の間隔更新を実行するとこの問題が発生します~~
ここに画像の説明を挿入

[0,1]すべてに 1 を加算して間隔を更新するとします。[0,1]まず、ルートノードの間隔と間隔の関係を決定します。[0,1]カバーされていない場合は、の子ノードを[0,5]検索します。部分的にカバーされており位置するノードに遅延マークがある場合は、次の操作を実行します。 :[0,5][0,1][0,2][0,2]

  1. 遅延マーカーを左右の子ノードにダウンロードします。
  2. 左右の子ノードの間隔値を更新し、 、つまり[0,1]として更新します右の子ノードsum = sum + 更新值 * 区间长度sum = 3 + (1 - 0 + 1) * 1 = 5sum = 3 + (2 - 2 + 1) * 1 = 4
  3. 現在のノードの遅延マークを 0 にリセットします。つまり、遅延マークはありません。

ダウンロード遅延フラグが完成しました。[0,2]と交差するノードを検索すると、それが完全にカバーされている[0,1]ことがわかり、間隔に等しい値を直接更新し、遅延マークを追加し、最後に間隔値を上方に更新します[0,2][0,1][0,1]5+(1-0+1)=7LazyTag = + 1

覚えやすいように、間隔更新のステップを次のように省略します。完全なカバレッジ、部分的なカバレッジ、「マークとダウンロード」、子の検索、および更新間隔です。コードの実装を見てみましょう。

void pushUp(int index) {
    
    
    segTree[index] = segTree[index * 2 + 1] + segTree[index * 2 + 2]; // 向上更新,用孩子节点更新父节点
}

void pushDown(int index, int left, int right) {
    
     // 向下传递延迟标记
    if (lazyTag[index] != 0) {
    
    
        int mid = (left + right) / 2;
        lazyTag[index * 2 + 1] += lazyTag[index]; //更新左孩子的延迟标记
        lazyTag[index * 2 + 2] += lazyTag[index];//更新右孩子的延迟标记
        segTree[index * 2 + 1] += lazyTag[index] * (mid - left + 1); // 区间值 = sum + 更新值 *(区间长度)
        segTree[index * 2 + 2] += lazyTag[index] * (right - mid);
        lazyTag[index] = 0; // 清除延迟标记
    }
}
public void intervalUpdate(int x, int y, int value) {
    
    
    intervalUpdate(0, 0, nums.length - 1, x, y, value);
}
private void intervalUpdate(int index, int left, int right, int x, int y, int value) {
    
    
    if (x <= left && y >= right) {
    
     // 完全覆盖
        segTree[index] += value * (right - left + 1); // 更新区间值
        lazyTag[index] += value; // 更新延迟标记
        return;
    }
    pushDown(index, left, right); // 部分覆盖,下传延迟标记
    int mid = (left + right) / 2;
    if (x <= mid) intervalUpdate(index * 2 + 1, left, mid, x, y, value);
    if (y > mid) intervalUpdate(index * 2 + 2, mid + 1, right, x, y, value);
    pushUp(index);
}

2.5 間隔の変更に基づくクエリ

範囲の変更に基づくクエリは、マーキングが延期されるため異なります。これは次の規則に従います。

  • クエリする間隔がノード間隔を完全にカバーする場合は、間隔値を直接返します。
  • 部分的にカバーされている場合は、まず遅延マークをダウンロードしてからクエリを実行する必要があります。

次に、コードの実装を見てみましょう~

public int query(int x, int y) {
    
    
        return query(0, 0, nums.length - 1, x, y);
}
private int query(int index, int left, int right, int x, int y) {
    
     // x 表示要查询的左区间,y 表示要查询的右区间
    if (x > right || y < left) return 0; // 如果查询区间在线段树区间外返回 0
    if (x <= left && y >= right) return segTree[index];  // 当查询区间包含线段树区间,返回节点值
    pushDown(index,left,right); //下传延迟标记
    int mid = (left + right) / 2;
    int leftQuery = query(2 * index + 1, left, mid, x, y); // 计算左孩子
    int rightQuery = query(2 * index + 2, mid + 1, right, x, y); // 计算右孩子
    return leftQuery + rightQuery; // 求和
}

セクション 2.3 のクエリと比較すると、このステップを完全にカバーした後でダウンロード遅延マークの操作を追加するだけであることがわかりました~

3. 完全なコード

最後に、完全なコードとテストデータを添付します。

import java.util.List;

public class Solution {
    
    
    int nums[] = new int[]{
    
    1, 2, 3, 4, 5, 6};
    int n = nums.length;
    int[] segTree = new int[4 * n]; // 为线段树分配空间
    int lazyTag[] = new int[4 * n]; // 为延迟标记分配空间

    void buildTree(int index, int left, int right) {
    
     // index 表示下标,left 表示左区间,right 表示右区间
        if (left == right) {
    
    
            segTree[index] = nums[left];
            return; // 到叶子节点就不能继续划分啦~
        }
        int mid = (left + right) / 2; // 一分为 2,例如将 [1,6] 划分为 [1,3] 和 [4,6]
        buildTree(2 * index + 1, left, mid); // 构建左子树,左孩子的下标为 2 * index + 1
        buildTree(2 * index + 2, mid + 1, right); // 构建右子树,右孩子的下标为 2 * index + 2
        segTree[index] = segTree[2 * index + 1] + segTree[2 * index + 2]; // 这里是求和,所以非叶子节点存储的值是左右孩子节点存储的值之和
    }

    void pushUp(int index) {
    
    
        segTree[index] = segTree[index * 2 + 1] + segTree[index * 2 + 2]; // 向上更新,用孩子节点更新父节点
    }

    void pushDown(int index, int left, int right) {
    
     // 向下传递延迟标记
        if (lazyTag[index] != 0) {
    
    
            int mid = (left + right) / 2;
            lazyTag[index * 2 + 1] += lazyTag[index]; //更新左孩子的延迟标记
            lazyTag[index * 2 + 2] += lazyTag[index];//更新右孩子的延迟标记
            segTree[index * 2 + 1] += lazyTag[index] * (mid - left + 1); // 区间值 = sum + 更新值 *(区间长度)
            segTree[index * 2 + 2] += lazyTag[index] * (right - mid);
            lazyTag[index] = 0; // 清除延迟标记
        }
    }

    public void intervalUpdate(int x, int y, int value) {
    
    
        intervalUpdate(0, 0, nums.length - 1, x, y, value);
    }

    private void intervalUpdate(int index, int left, int right, int x, int y, int value) {
    
    
        if (x <= left && y >= right) {
    
     // 完全覆盖
            segTree[index] += value * (right - left + 1); // 更新区间值
            lazyTag[index] += value; // 更新延迟标记
            return;
        }
        pushDown(index, left, right); // 部分覆盖,下传延迟标记
        int mid = (left + right) / 2;
        if (x <= mid) intervalUpdate(index * 2 + 1, left, mid, x, y, value);
        if (y > mid) intervalUpdate(index * 2 + 2, mid + 1, right, x, y, value);
        pushUp(index);
    }

    // 区间查询
    public int query(int x, int y) {
    
    
        return query(0, 0, nums.length - 1, x, y);
    }

    private int query(int index, int left, int right, int x, int y) {
    
     // x 表示要查询的左区间,y 表示要查询的右区间
        if (x > right || y < left) return 0; // 如果查询区间在线段树区间外返回 0
        if (x <= left && y >= right) return segTree[index];  // 当查询区间包含线段树区间,返回节点值
        pushDown(index, left, right); //下传延迟标记
        int mid = (left + right) / 2;
        int leftQuery = query(2 * index + 1, left, mid, x, y); // 计算左孩子
        int rightQuery = query(2 * index + 2, mid + 1, right, x, y); // 计算右孩子
        return leftQuery + rightQuery; // 求和
    }

    public static void main(String[] args) {
    
    
        Solution solution = new Solution();
        solution.buildTree(0, 0, solution.nums.length - 1);
        solution.intervalUpdate(0,3,1);
        solution.intervalUpdate(1,2,1);
        System.out.println(solution.query(0, 2));
    }
}

最後が見えるよ、君は本当にすごいよ、さあ~


おすすめ

転載: blog.csdn.net/jiaweilovemingming/article/details/131992995