高度なデータ構造 - セグメント ツリー、重みセグメント ツリー (Java & JS & Python)

プライマー

ここで、配列 arr = [4, 7, 5, 3, 8, 9, 0, 1, 2, 6]、arr.length = n として、次の操作を不定期に繰り返し実行します。

  • arr の指定区間 [l, r] 内の最大値 max を問い合わせます
  • arr の指定された区間 [l, r] 内の要素の合計をクエリします。
  • arr は、指定されたインデックス i の位置にある要素に C を追加するか、C で上書きします。
  • arr は、指定された間隔 [l, r] の各要素値に対して C を追加するか、C をオーバーライドします。

で:

  • クエリの時間計算量 (最大間隔、合計間隔) は O(n) です。
  • 単一の値を更新する時間計算量は O(1) です。
  • 間隔更新の時間計算量は O(n) です 

arr の指定された間隔の合計を複数回解く必要がある場合は、プレフィックスと最適化を使用できます。詳細については、次を参照してください。

アルゴリズム設計 - プレフィックス和と微分シーケンス - 府城市外のブログ - CSDN ブログ

ただし、上記の要件では、arr 配列が変更される (単一値更新、間隔更新) ため、arr 配列のプレフィックスと配列も変更されます。 arr が更新されるたびに、プレフィックスと配列を再生成する必要があるため、O( 1) 時間計算量は間隔の合計として計算されます。

たとえば、上記の操作のいずれかを m 回実行すると (各操作は異なる場合があります)、最終的な時間計算量は O(m * n) になります。

では、より効率的なアルゴリズムはあるのでしょうか?

セグメントツリーの概念

線分ツリーは分割統治の考え方に基づいた二分木であり、線分ツリーの各ノードはarr配列の区間[l, r]に対応します

  • 線分ツリーの葉ノードは、区間内の l == r に対応します。
  • 線分ツリーの非葉ノードが区間 [l, r] に対応する場合、mid = (l + r) / 2 とします。
  1. 左側の子ノードは区間 [l,mid] に対応します。
  2. 右側の子ノードは区間 [mid + 1, r] に対応します。

線分ツリーのノードには、区間の最大値、区間の合計など、対応する区間 [l, r] 内の結果値も記録されます。

つまり、線分ツリー ノードには 3 つの基本情報が含まれていると考えることができます。

  • 区間左境界 l
  • 区間 r の右境界
  • 間隔結果値 val

たとえば、配列 arr = [4, 7, 5, 3, 8, 9, 0, 1, 2, 6] の場合、対応するセグメント ツリー図は次のとおりです。

このうち、線分ツリーの葉ノードのl==r、i == l == rとすると、線分ツリーの葉ノードの値はarr[i]となる。

間隔の最大値を見つける必要がある場合、各親ノードの val は 2 つの子ノードの val の大きい方に等しいため、線分ツリーは次のように取得できます。

上記の構造により、O(logN) の時間計算量を達成し、任意の間隔の最大値を見つけることができます。

例えば、区間[3, 8]の最大値を求めたい場合、ルートノードから分割統治し、3つの区間[3, 4], [の結果値を求めることと等価です。 [5, 7], [8, 8]. [3, 8] 区間の大きい方の値を最大値とします。

したがって、線分ツリーに基づいて区間情報をクエリすることは非常に効率的な戦略です。

セグメントツリーの基礎となるコンテナ

線分ツリーは実際には二分木であり、満杯ではない最後の層を除いて残りの層は満杯でなければなりません

完全なバイナリ ツリーの場合は、以下に示す完全なバイナリ ツリーのように、配列に格納できます。

完全なバイナリ ツリーでは、親ノードのシリアル番号が k (k>=1) の場合、その左側の子ノードのシリアル番号は 2*k、右側の子ノードのシリアル番号は 2*k+ になります。 1

したがって、バイナリ ツリー全体のノード番号が配列インデックスに対応する場合、その関係は上の図に示すとおりになります。

つまり、配列のインデックス k には、二分木のノード番号 k のノード値が記録されます。 

したがって、線分ツリーを完全な二分木として想像する限り、それを配列に格納することができます。では、線分ツリーはどれくらいの期間適用する必要があるでしょうか?

線分木が表す区間 [l, r] の長さを n とすると、線分木には葉ノードが n 個あることになります。

最後から 2 番目の層には最大で n 個のノードがあり、線分ツリーの最初から最後から 2 番目の層は完全な二分木であり、完全な二分木には次のプロパティがあります。

完全なバイナリ ツリーの最後の層に x 個のノードがある場合、前のすべての層のノード数の合計は x-1 でなければなりません。

証明も非常に簡単で、完全なバイナリ ツリーの各層のノードの数は次のようになります。

レイヤ 1、2^0 ノード

レイヤ 2、2^1 ノード

レイヤ 3、2^2 ノード

....

層が 3 つしかないと仮定すると、次のようになります: 2^0 + 2^1 = 2^2 - 1

線分ツリーの最後から 2 番目の層のノード数が最大で n 個の場合、線分ツリーの最初から最後までの層のノード数は最大で n-1 個になります。

つまり、線分ツリーの第1層から最後から2層までの節点の数は最大で2n−1個となる。

次に、線分ツリーの最後の層が満たされている場合、最大 2n 個のノードが存在する必要があります。

したがって、線分ツリーのノード数は合計で最大 4n 個となり、長さ 4n の配列空間が開けば、線分ツリーのすべてのノードを格納することができる。

線分ツリー構築

線分ツリーの基礎となるコンテナは配列であり、これをツリーと仮定します。

間隔情報をクエリする元の配列 arr の長さが n の場合、線分ツリーの基礎となるコンテナ配列は 4n の長さを定義する必要があります。

ツリー配列要素と線分ツリー ノードの関係は次のとおりです。

  • ツリー配列要素→線分ツリーノード。
  • ツリー配列要素のインデックス → 線分ツリーノードの通し番号

線分ツリーのノードには、次の 3 つの基本情報が含まれています。

  • 区間左境界 l
  • 区間 r の右境界
  • 区間結果値val(区間合計値、区間最大値など)

したがって、ノード情報を記録するために Node クラスを定義できます。したがって、ツリー配列も Node 型の配列になります。

この図を使用して、ツリー配列がどのように見えるかを確認できます。

線分ツリーを構築します。つまり、上図のツリー配列を構築します。

ツリー配列のインデックスkは、線分ツリーノードの通し番号kである。

ツリー[k] = ノード {l, r, max}

上記の擬似コードの意味は、線分ツリーノード k が arr 配列 [l, r] の区間に対応し、この区間の最大値 max を記録するというものです。

分割統治を再帰的に行うことで線分ツリーの構築を完了することができます。

たとえば、k=1 の線分ツリー ノードはすでにわかっており、維持される arr 間隔は [0, 9] ですが、次にこの間隔の最大値を見つける必要があります。

線分は分割統治の考え方に基づく二分木なので、[0, 9]区間は[0, 4]と[5, 9]に分割できます。

つまり、[0, 9] 区間の最大値の問題は、[0, 4] 区間の最大値と [5, 9] 区間の最大値という 2 つの小さな部分問題に変換されます。

間隔 [0, 4] はまさに k=2 ノードによって維持される間隔であり、[5, 9] は k=3 ノードによって維持される間隔です。

その後、引き続きこのロジックに従い、区間 [0, 4] と [5, 9] の最大値を再帰的に求めます。

2分割後の区間の l == r まで、つまりリーフノードに到達するまで、このときの区間 [l, r] の最大値は arr[l] または arr[r] となり、その後、後戻りを開始できます。

バックトラック処理中、親ノードの間隔の最大値は、その 2 つのノードの間隔の最大値のうち大きい方と等しくなります。

具体的なコード実装は次のとおりです (テストコードを含む)。

JSコードの実装

// 线段树节点定义
class Node {
  constructor(l, r) {
    this.l = l; // 区间左边界
    this.r = r; // 区间右边界
    this.max = undefined; // 区间内最大值
  }
}

// 线段树定义
class SegmentTree {
  constructor(arr) {
    // arr是要执行查询区间最大值的原始数组
    this.arr = arr;
    // 线段树底层数据结构,其实就是一个数组,我们定义其为tree,如果arr数组长度为n,则tree数组需要4n的长度
    this.tree = new Array(arr.length * 4);
    // 从根节点开始构建,线段树根节点序号k=1,对应的区间范围是[0, arr.length-1]
    this.build(1, 0, arr.length - 1);
  }

  /**
   * 线段树构建
   * @param {*} k 线段树节点序号
   * @param {*} l 节点对应的区间范围左边界
   * @param {*} r 节点对应的区间范围右边界
   */
  build(k, l, r) {
    // 初始化线段树节点, 即建立节点序号k和区间范围[l, r]的联系
    this.tree[k] = new Node(l, r);

    // 如果l==r, 则说明k节点是线段树的叶子节点
    if (l == r) {
      // 而线段树叶子节点的结果值就是arr[l]或arr[r]本身
      this.tree[k].max = arr[r];
      // 回溯
      return;
    }

    // 如果l!=r, 则说明k节点不是线段树叶子节点,因此其必有左右子节点,左右子节点的分界位置是mid
    const mid = (l + r) >> 1; // 等价于Math.floor((l + r) / 2)

    // 递归构建k节点的左子节点,序号为2 * k,对应区间范围是[l, mid]
    this.build(2 * k, l, mid);
    // 递归构建k节点的右子节点,序号为2 * k + 1,对应区间范围是[mid+1, r]
    this.build(2 * k + 1, mid + 1, r);

    // k节点的结果值,取其左右子节点结果值的较大值
    this.tree[k].max = Math.max(this.tree[2 * k].max, this.tree[2 * k + 1].max);
  }
}

// 测试
const arr = [4, 7, 5, 3, 8, 9, 0, 1, 2, 6];

const tree = new SegmentTree(arr).tree;

console.log("k\t| tree[k]");
for (let k = 0; k < tree.length; k++) {
  if (tree[k]) {
    console.log(
      `${k}\t| Node{ l: ${tree[k].l}, r: ${tree[k].r}, max: ${tree[k].max}}`
    );
  } else {
    console.log(`${k}\t| null`);
  }
}

Javaコードの実装

// 线段树定义
public class SegmentTree {
  // 线段树节点定义
  static class Node {
    int l; // 区间左边界
    int r; // 区间右边界
    int max; // 区间内最大值

    public Node(int l, int r) {
      this.l = l;
      this.r = r;
    }
  }

  int[] arr;

  Node[] tree;

  public SegmentTree(int[] arr) {
    // arr是要执行查询区间最大值的原始数组
    this.arr = arr;
    // 线段树底层数据结构,其实就是一个数组,我们定义其为tree,如果arr数组长度为n,则tree数组需要4n的长度
    this.tree = new Node[arr.length * 4];
    // 从根节点开始构建,线段树根节点序号k=1,对应的区间范围是[0, arr.length-1]
    this.build(1, 0, arr.length - 1);
  }

  /**
   * 线段树构建
   *
   * @param k 线段树节点序号
   * @param l 节点对应的区间范围左边界
   * @param r 节点对应的区间范围右边界
   */
  private void build(int k, int l, int r) {
    // 初始化线段树节点, 即建立节点序号k和区间范围[l, r]的联系
    this.tree[k] = new Node(l, r);

    // 如果l==r, 则说明k节点是线段树的叶子节点
    if (l == r) {
      // 而线段树叶子节点的结果值就是arr[l]或arr[r]本身
      this.tree[k].max = this.arr[r];
      // 回溯
      return;
    }

    // 如果l!=r, 则说明k节点不是线段树叶子节点,因此其必有左右子节点,左右子节点的分界位置是mid
    int mid = (l + r) >> 1;

    // 递归构建k节点的左子节点,序号为2 * k,对应区间范围是[l, mid]
    this.build(2 * k, l, mid);
    // 递归构建k节点的右子节点,序号为2 * k + 1,对应区间范围是[mid+1, r]
    this.build(2 * k + 1, mid + 1, r);

    // k节点的结果值,取其左右子节点结果值的较大值
    this.tree[k].max = Math.max(this.tree[2 * k].max, this.tree[2 * k + 1].max);
  }

  // 测试
  public static void main(String[] args) {
    int[] arr = {4, 7, 5, 3, 8, 9, 0, 1, 2, 6};

    Node[] tree = new SegmentTree(arr).tree;

    System.out.println("k\t| tree[k]");
    for (int k = 0; k < tree.length; k++) {
      if (tree[k] == null) {
        System.out.println(k + "\t| null");
      } else {
        System.out.println(
            k + "\t| Node{ l: " + tree[k].l + ", r: " + tree[k].r + ", max: " + tree[k].max + "}");
      }
    }
  }
}

Pythonコードの実装

# 线段树节点定义
class Node:
    def __init__(self):
        self.l = None
        self.r = None
        self.mx = None


# 线段树定义
class SegmentTree:
    def __init__(self, lst):
        # lst是要执行查询区间最大值的原始数组
        self.lst = lst
        # 线段树底层数据结构,其实就是一个数组,我们定义其为tree,如果lst数组长度为n,则tree数组需要4n的长度
        self.tree = [Node() for _ in range(len(lst) * 4)]
        # 从根节点开始构建,线段树根节点序号k=1,对应的区间范围是[0, len(lst) - 1]
        self.build(1, 0, len(lst) - 1)

    def build(self, k, l, r):
        """
        线段树构建
        :param k: 线段树节点序号
        :param l: 节点对应的区间范围左边界
        :param r: 节点对应的区间范围右边界
        """

        # 初始化线段树节点, 即建立节点序号k和区间范围[l, r]的联系
        self.tree[k].l = l
        self.tree[k].r = r

        # 如果l==r, 则说明k节点是线段树的叶子节点
        if l == r:
            # 而线段树叶子节点的结果值就是lst[l]或lst[r]本身
            self.tree[k].mx = self.lst[r]
            # 回溯
            return

        # 如果l!=r, 则说明k节点不是线段树叶子节点,因此其必有左右子节点,左右子节点的分界位置是mid
        mid = (l + r) >> 1

        # 递归构建k节点的左子节点,序号为2 * k,对应区间范围是[l, mid]
        self.build(2 * k, l, mid)
        # 递归构建k节点的右子节点,序号为2 * k + 1,对应区间范围是[mid+1, r]
        self.build(2 * k + 1, mid + 1, r)

        # k节点的结果值,取其左右子节点结果值的较大值
        self.tree[k].mx = max(self.tree[2 * k].mx, self.tree[2 * k + 1].mx)


# 测试代码
lst = [4, 7, 5, 3, 8, 9, 0, 1, 2, 6]
print("k\t| tree[k]")
for k, node in enumerate(SegmentTree(lst).tree):
    if node.mx:
        print(f"{k}\t| Node[ l: {node.l}, r: {node.r}, mx: {node.mx} ]")
    else:
        print(f"{k}\t| null")

任意の間隔の結果値をクエリする

おすすめ

転載: blog.csdn.net/qfc_128220/article/details/131720641