Estrutura de dados avançada - árvore de segmentos, árvore de segmentos de peso (Java, JS e Python)

Cartilha

Agora, dada uma matriz arr = [4, 7, 5, 3, 8, 9, 0, 1, 2, 6], arr.length = n, execute as seguintes operações repetidamente de forma irregular:

  • Consulte o valor máximo max dentro do intervalo especificado [l, r] de arr
  • Consulte a soma dos elementos no intervalo especificado [l, r] de arr
  • arr adiciona C ao elemento na posição i do índice especificado ou o substitui por C
  • arr adiciona C ou substitui C para cada valor de elemento no intervalo especificado [l, r]

em:

  • A complexidade de tempo da consulta (intervalo máximo, soma do intervalo) é O(n)
  • A complexidade de tempo de atualização de um único valor é O(1)
  • A complexidade de tempo da atualização do intervalo é O(n) 

Se você precisar resolver a soma do intervalo especificado de arr várias vezes, poderá usar o prefixo e a otimização. Para obter detalhes, consulte:

Design de algoritmo - Soma de prefixo e sequência diferencial - Blogs fora de Fucheng - Blog CSDN

No entanto, nos requisitos acima, a matriz arr muda (atualização de valor único, atualização de intervalo), então o prefixo e a matriz da matriz arr também mudam. Sempre que arr é atualizado, o prefixo e a matriz precisam ser regenerados, então O( 1) A complexidade do tempo é calculada como a soma do intervalo.

Se, digamos, executar qualquer uma das operações acima m vezes (cada operação pode ser diferente), a complexidade de tempo final é O(m * n)

Então, existe um algoritmo mais eficiente?

Conceito de árvore de segmento

A árvore de segmento de linha é uma árvore binária baseada na ideia de dividir e conquistar. Cada nó da árvore de segmento de linha corresponde a um intervalo [l, r] do array arr

  • O nó folha da árvore de segmento de linha corresponde a l == r no intervalo
  • Se o nó não-folha da árvore do segmento de linha corresponder ao intervalo [l, r], suponha que mid = (l + r) / 2
  1. O nó filho esquerdo corresponde ao intervalo [l, mid]
  2. O nó filho direito corresponde ao intervalo [mid + 1, r]

Os nós da árvore de segmento de linha também registram os valores dos resultados no intervalo correspondente [l, r], como o valor máximo do intervalo, a soma do intervalo.

Ou seja, podemos pensar que o nó da árvore do segmento de linha contém três informações básicas:

  • limite esquerdo do intervalo l
  • o limite direito do intervalo r
  • Valor do resultado do intervalo val

Por exemplo, a matriz arr = [4, 7, 5, 3, 8, 9, 0, 1, 2, 6], o diagrama de árvore de segmento correspondente é o seguinte:

Entre eles, l==r do nó folha na árvore de segmento de linha, assumindo i == l == r, então o valor do nó folha da árvore de segmento de linha é arr[i].

Se precisarmos encontrar o valor máximo do intervalo, o val de cada nó pai é equivalente ao maior dos vals de seus dois nós filhos, então a árvore de segmento de linha pode ser obtida da seguinte forma:

Com a estrutura acima, podemos atingir a complexidade de tempo O (logN) e encontrar o valor máximo de qualquer intervalo.

Por exemplo, se quisermos encontrar o valor máximo do intervalo [3, 8], é equivalente a dividir e conquistar a partir do nó raiz e encontrar os valores resultantes dos três intervalos [3, 4], [ 5, 7], [8, 8].Tome o valor maior como o valor máximo do intervalo [3, 8].

Portanto, é uma estratégia muito eficiente consultar informações de intervalo com base na árvore de segmentos de linha.

O contêiner subjacente da árvore de segmentos

A árvore de segmento de linha é na verdade uma árvore binária e, exceto pela última camada que pode não estar cheia, o restante das camadas deve estar cheia.

Para uma árvore binária completa, podemos armazená-la em um array, como a árvore binária completa mostrada abaixo:

Em uma árvore binária completa, se o número de série do nó pai for k (k>=1), o número de série de seu nó filho esquerdo será 2*k e o número de série de seu nó filho direito será 2*k+ 1

Portanto, se o número completo do nó da árvore binária corresponder ao índice da matriz, o relacionamento será mostrado na figura acima.

Ou seja, o índice k na matriz registra o valor do nó do número do nó k na árvore binária. 

Portanto, contanto que imaginemos a árvore de segmento de linha como uma árvore binária completa, ela pode ser armazenada em uma matriz, então por quanto tempo a árvore de segmento de linha precisa ser aplicada?

Supondo que o intervalo [l, r] descrito pela árvore de segmento de linha tenha comprimento n, significa que a árvore de segmento de linha tem n nós folha

A penúltima camada tem no máximo n nós, e a penúltima camada da árvore de segmento de linha é uma árvore binária completa, e a árvore binária completa tem as seguintes propriedades:

Se houver x nós na última camada de uma árvore binária completa, a soma do número de nós em todas as camadas anteriores deverá ser x-1.

A prova também é muito fácil, o número de nós em cada camada da árvore binária completa:

Camada 1, com 2^0 nós

Camada 2, com 2^1 nós

Camada 3, com 2 ^ 2 nós

....

Supondo que existam apenas 3 camadas, deve haver: 2^0 + 2^1 = 2^2 - 1

Se a penúltima camada da árvore de segmentos de linha tiver no máximo n nós, então a penúltima camada da árvore de segmentos de linha terá no máximo n-1 nós,

Ou seja, existem no máximo 2n-1 nós da primeira camada até a penúltima camada da árvore de segmentos de linha.

Então, se a última camada da árvore de segmentos de linha for preenchida, deverá haver no máximo 2n nós.

Portanto, a árvore de segmentos de linha possui no máximo 4n nós no total, ou seja, desde que um espaço de array de 4n comprimento seja aberto, todos os nós da árvore de segmentos de linha podem ser armazenados.

Construção de árvore de segmento de linha

O contêiner subjacente da árvore de segmento de linha é um array, que assumimos ser uma árvore.

Se o comprimento da matriz original arr a ser consultada para obter informações de intervalo for n, então a matriz contêiner subjacente da árvore de segmento de linha precisa definir um comprimento de 4n.

A relação entre os elementos da matriz de árvore e os nós da árvore de segmento de linha é a seguinte:

  • elemento da matriz de árvore → nó da árvore do segmento de linha.
  • O índice do elemento da matriz de árvore → o número de série do nó da árvore do segmento de linha

Os nós da árvore de segmentos de linha contêm três informações básicas:

  • limite esquerdo do intervalo l
  • o limite direito do intervalo r
  • Valor do resultado do intervalo val (como soma do intervalo, valor máximo do intervalo)

Portanto, podemos definir uma classe Node para registrar informações do nó. Portanto, o array tree também é um array do tipo Node.

Podemos usar o diagrama para ver como é a matriz da árvore

Construa uma árvore de segmento de linha, ou seja, construa uma matriz de árvore na figura acima.

O índice k da matriz de árvore é o número de série k do nó da árvore do segmento de linha.

árvore[k] = Nó {l, r, max}

O significado do pseudocódigo acima é: o nó da árvore do segmento de linha k corresponde ao intervalo da matriz arr [l, r] e registra o valor máximo max neste intervalo

Podemos completar a construção da árvore de segmentos de linha dividindo e conquistando recursivamente.

Por exemplo, já conhecemos o nó da árvore do segmento de linha com k = 1, e o intervalo arr mantido é [0, 9].Agora precisamos encontrar o valor máximo desse intervalo?

Como o segmento de linha é uma árvore binária baseada na ideia de dividir e conquistar, o intervalo [0, 9] pode ser dividido em [0, 4] e [5, 9]

Ou seja, o problema do valor máximo do intervalo [0, 9] é transformado em dois subproblemas menores do valor máximo do intervalo [0, 4] e do valor máximo do intervalo [5, 9].

O intervalo [0, 4] é exatamente o intervalo mantido por k=2 nós, e [5, 9] é o intervalo mantido por k=3 nós.

Depois disso, continue seguindo esta lógica para resolver recursivamente o valor máximo do intervalo [0, 4] e [5, 9].

Até l == r do intervalo após ser dividido em dois, ou seja, quando o nó folha é atingido, o valor máximo do intervalo [l, r] neste momento é arr[l] ou arr[r], e então você pode começar a retroceder.

Durante o processo de retrocesso, o valor máximo do intervalo do nó pai é igual ao maior dos valores máximos dos intervalos de seus dois nós.

A implementação específica do código é a seguinte (incluindo código de teste):

Implementação de código 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`);
  }
}

Implementação de código 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 + "}");
      }
    }
  }
}

Implementação de código 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")

Consulte qualquer valor de resultado de intervalo

Acho que você gosta

Origin blog.csdn.net/qfc_128220/article/details/131720641
Recomendado
Clasificación