动态开点线段树说明

动态,开点,线段,说明 · 浏览次数 : 323

小编点评

内容生成时需要带简单的排版,以下是一些方法和数据结构笔记: 1. **排序**:将原始数组排序后,按大小排序数组中的每个元素。 2. **动态开点线段树**:构建线段树,根据每个元素的最小值来构建线段树。 3. **数据结构**:使用数组存储元素,使用List存储元素,使用Map存储元素,使用Set存储元素。 4. **排版**:使用Arrays.sort()方法排序数组,使用List.addAll()方法添加元素到List,使用Map.put()方法添加元素到Map。 5. **内存管理**:使用内存分配机制,确保每个元素的存储位置。 **一些例子**: 1. 排序数组中的每个元素,按照大小排序。 2. 建立线段树,根据每个元素的最小值来构建线段树。 3. 使用Map存储元素,使用Set存储元素。 4. 使用List存储元素,使用Arrays.sort()方法排序元素。 **一些数据结构和算法的笔记**: * **DynamicSegmentTree**:是一种使用数组存储元素,构建线段树的算法。 * **Map**:是一种使用键值对存储元素的算法。 * **Set**:是一种使用键值对存储元素的算法。 * **Arrays**:是一种存储数组元素的算法。 * **List**:是一种存储元素的算法。 **一些建议**: * 使用数据结构的性能进行优化。 * 使用算法的性能进行优化。 * 使用内存分配机制,确保每个元素的存储位置。 * 使用简单的排版方法进行优化。

正文

动态开点线段树说明

作者:Grey

原文地址:

博客园:动态开点线段树说明

CSDN:动态开点线段树说明

说明

针对普通线段树,参考使用线段树解决数组任意区间元素修改问题

在普通线段树中,线段树在预处理的时候,需要申请 4 倍大小的数组空间来存放划分的区域,

而本文介绍的动态开点线段树,它和普通线段树的区别是,动态开点线段树不需要像普通线段树那样提前申请 4 倍大小的数据空间来存放划分区域,等到实际使用的时候,再来申请。

先讲一种比较简单的动态开点线段树,这种线段树只支持单点的更新和查询。

即支持如下两个方法

void add(i, v);

该方法表示在 i 上的值加上 v;

int query(int s, int e)

该方法用于获取 s 到 e 区间内的累加和信息。

该线段树只需要定义一个节点数据结构即可

  public static class Node {
    public int sum;
    public Node left;
    public Node right;
  }

其中 sum 表示 Node 所在区间的累加和,left 表示节点左孩子信息,right 表示节点右孩子信息。

线段树初始化过程也只需要

  public static class DynamicSegmentTree {
    public Node root;
    public int size;

    public DynamicSegmentTree(int max) {
      root = new Node();
      size = max;
    }
  }

size 表示线段树支持的范围,这个范围从线段树一开始初始化的时候设定好(编号1 到 编号size就是区间范围)。和普通线段树不一样的地方在于,节点只建立了 root 节点,未初始化所有区间。

接下来看add方法,

    public void add(int i, int v) {
      add(root, 1, size, i, v);
    }

这个方法调用了线段树内部的私有add方法,

    // c-> cur 当前节点!表达的范围 l~r
    // i位置的数,增加v
    // 潜台词!i一定在l~r范围上!
    private void add(Node c, int l, int r, int i, int v) {
      if (l == r) {
        c.sum += v;
      } else { // l~r 还可以划分
        int mid = (l + r) / 2;
        if (i <= mid) { // l ~ mid
          if (c.left == null) {
            c.left = new Node();
          }
          add(c.left, l, mid, i, v);
        } else { // mid + 1 ~ r
          if (c.right == null) {
            c.right = new Node();
          }
          add(c.right, mid + 1, r, i, v);
        }
        c.sum = (c.left != null ? c.left.sum : 0) + (c.right != null ? c.right.sum : 0);
      }
    }

这个add方法的几个参数分别代表

c : 表示 add 操作的区间代表节点是多少

l...r 表示任务区间,由于初始化 size,所以在调用公开的 add 方法时候,l = 1, r = size,表示在初始化区间范围内操作。

i:表示要操作的位置

v: 表示要增加的值

整个 add 私有方法逻辑也比较简单,核心代码

        // i 在节点左边
        if (i <= mid) { 
            // 如果节点的左树为空,则建立新节点
          if (c.left == null) {
            c.left = new Node();
          }
          add(c.left, l, mid, i, v);
        } else { 
            // i 在节点右边
            // 如果节点右树为空,则建立新节点
          if (c.right == null) {
            c.right = new Node();
          }
          add(c.right, mid + 1, r, i, v);
        }
        // 最后当前节点要汇聚左右树的结果,之所以要判空是因为左右树可能不需要都建立出来
        c.sum = (c.left != null ? c.left.sum : 0) + (c.right != null ? c.right.sum : 0);

查询方法的逻辑也比较简单

    public int query(int s, int e) {
      return query(root, 1, size, s, e);
    }

调用了内部的一个私有 query 方法,

    private int query(Node c, int l, int r, int s, int e) {
      if (c == null) {
        return 0;
      }
      if (s <= l && r <= e) { 
        return c.sum;
      }
      int mid = (l + r) / 2;
      if (e <= mid) {
        return query(c.left, l, mid, s, e);
      } else if (s > mid) {
        return query(c.right, mid + 1, r, s, e);
      } else {
        return query(c.left, l, mid, s, e) + query(c.right, mid + 1, r, s, e);
      }
    }
  }

这个私有方法的几个参数说明如下

c:表示要操作的线段树的代表节点是什么;

l...r 是划分的区间范围

s...e 是任务的区间范围

核心逻辑如下

// 如果任务的区间已经包含了划分的区间,直接返回结果
      if (s <= l && r <= e) { 
        return c.sum;
      }
      // 否则,去左右区间拿累加和
      int mid = (l + r) / 2;
      if (e <= mid) {
        return query(c.left, l, mid, s, e);
      } else if (s > mid) {
        return query(c.right, mid + 1, r, s, e);
      } else {
        // 整合成自己的累加和返回
        return query(c.left, l, mid, s, e) + query(c.right, mid + 1, r, s, e);
      }

整个支持单点更新的动态线段树的完整代码如下(含对数器代码)

// 只支持单点增加 + 范围查询的动态开点线段树(累加和)
public class Code01_DynamicSegmentTree {

  public static class Node {
    public int sum;
    public Node left;
    public Node right;
  }

  // arr[0] -> 1
  // 线段树,从1开始下标!
  public static class DynamicSegmentTree {
    public Node root;
    public int size;

    public DynamicSegmentTree(int max) {
      root = new Node();
      size = max;
    }

    // 下标i这个位置的数,增加v
    public void add(int i, int v) {
      add(root, 1, size, i, v);
    }

    // c-> cur 当前节点!表达的范围 l~r
    // i位置的数,增加v
    // 潜台词!i一定在l~r范围上!
    private void add(Node c, int l, int r, int i, int v) {
      if (l == r) {
        c.sum += v;
      } else { // l~r 还可以划分
        int mid = (l + r) / 2;
        if (i <= mid) { // l ~ mid
          if (c.left == null) {
            c.left = new Node();
          }
          add(c.left, l, mid, i, v);
        } else { // mid + 1 ~ r
          if (c.right == null) {
            c.right = new Node();
          }
          add(c.right, mid + 1, r, i, v);
        }
        c.sum = (c.left != null ? c.left.sum : 0) + (c.right != null ? c.right.sum : 0);
      }
    }

    // s~e范围的累加和
    public int query(int s, int e) {
      return query(root, 1, size, s, e);
    }

    // 当前节点c,表达的范围l~r
    // 收到了一个任务,s~e这个任务!
    // s~e这个任务,影响了多少l~r范围的数,把答案返回!
    private int query(Node c, int l, int r, int s, int e) {
      if (c == null) {
        return 0;
      }
      if (s <= l && r <= e) {
        return c.sum;
      }
      int mid = (l + r) / 2;
      if (e <= mid) {
        return query(c.left, l, mid, s, e);
      } else if (s > mid) {
        return query(c.right, mid + 1, r, s, e);
      } else {
        return query(c.left, l, mid, s, e) + query(c.right, mid + 1, r, s, e);
      }
    }
  }

  public static class Right {
    public int[] arr;

    public Right(int size) {
      arr = new int[size + 1];
    }

    public void add(int i, int v) {
      arr[i] += v;
    }

    public int query(int s, int e) {
      int sum = 0;
      for (int i = s; i <= e; i++) {
        sum += arr[i];
      }
      return sum;
    }
  }

  public static void main(String[] args) {
    int size = 10000;
    int testTime = 50000;
    int value = 500;
    DynamicSegmentTree dst = new DynamicSegmentTree(size);
    Right right = new Right(size);
    System.out.println("测试开始");
    for (int k = 0; k < testTime; k++) {
      if (Math.random() < 0.5) {
        int i = (int) (Math.random() * size) + 1;
        int v = (int) (Math.random() * value);
        dst.add(i, v);
        right.add(i, v);
      } else {
        int a = (int) (Math.random() * size) + 1;
        int b = (int) (Math.random() * size) + 1;
        int s = Math.min(a, b);
        int e = Math.max(a, b);
        int ans1 = dst.query(s, e);
        int ans2 = right.query(s, e);
        if (ans1 != ans2) {
          System.out.println("出错了!");
          System.out.println(ans1);
          System.out.println(ans2);
        }
      }
    }
    System.out.println("测试结束");
  }
}

接下来看一个使用动态开点线段树来解决的一个问题

即:LeetCode 315. Count of Smaller Numbers After Self

注:本题可以用归并排序,树状数组,有序表来解,也可以用动态开点线段树来解。

主要思路如下

以如下数组为例来说明

nums = {5,8,7,4,2,9}

首先,初始化一个 List,这个 List 用于存放每个位置的右侧比其小的数有几个,List 的大小和原始数组一样

List<Integer> ans = new ArrayList<>(nums.length);

ans 在初始化的时候,均设置为 0 ,表示,所有位置都还没计算过。

ans = [0,0,0,0,0,0]

接下来对原始数组进行排序(注意:排序的时候,不能只使用值来排序,要带上这个值所在的位置,这样排序后才不会丢失该值在原始数组中的位置信息)

    int[][] arr = new int[n][];
    for (int i = 0; i < n; i++) {
        // 要记录值,也要记录位置,防止排序后找不到值对应的位置在哪里
      arr[i] = new int[] {nums[i], i};
    }
    // 排序按值排序
    Arrays.sort(arr, Comparator.comparingInt(a -> a[0]));

排序后,arr 按如下顺序组织

{值:2,原始位置:4}
{值:4,原始位置:3}
{值:5,原始位置:0}
{值:7,原始位置:2}
{值:8,原始位置:1}
{值:9,原始位置:5}

接下来初始化开点线段树,线段树的size就是原始数组的大小,且每个位置都是0,

按顺序遍历这个 arr 数组,最小值 2 被取出,其原始位置是 4,且 4 号位置右侧没有比自己更小的数,接下来在开点线段树中把把 4 号位置的值加1,表示 4 号位置被处理过了,在线段树中查4号位置以后并没有任何标记记录,说明没有比这个数更小的数了,直接设置4号位置的ans值为0

ans = [0,0,0,0,0,0]

线段树中

seg = [0,0,0,0,1,0]

接下来是 3 号位置的4,在线段树中查到,有一个比它小的,直接设置到 ans 中,然后在线段树中把 3 号位置也标记为 1,说明处理过,

ans = [0,0,0,1,0,0]

线段树中

seg = [0,0,0,1,1,0]

接下来是0号位置的5, 在线段树中,查到右侧有两个标记过的,说明有两个比它小的数,直接在 ans 中把 0 号位置设置为 2, 然后在线段树中把 0 号位置标记为 1 ,说明处理过,此时

ans = [2,0,0,1,0,0]

线段树中

seg = [1,0,0,1,1,0]

接下来是 2 号位置的 7, 在线段树中,查到右侧有两个标记过的,说明有两个比它小的数,直接在 ans 中把 2 号位置设置为 2, 然后在线段树中把 2 号位置标记为 1 ,说明处理过,此时

ans = [2,0,2,1,0,0]

线段树中

seg = [1,0,1,1,1,0]

接下来是 1 号位置的 8, 在线段树中,查到右侧有三个标记过的,说明有三个比它小的数,直接在 ans 中把 1 号位置设置为 3, 然后在线段树中把 1 号位置标记为 1 ,说明处理过,此时

ans = [2,3,2,1,0,0]

线段树中

seg = [1,1,1,1,1,0]

接下来是 5 号位置的 9, 在线段树中,查到右侧没有标记过的,说明没有比它小的数,直接在 ans 中把 5 号位置设置为 0, 然后在线段树中把 5 号位置标记为 1 ,说明处理过,此时

ans = [2,3,2,1,0,0]

线段树中

seg = [1,1,1,1,1,1]

以上就是整个流程。

核心代码如下

  public static List<Integer> countSmaller(int[] nums) {
    if (nums == null || nums.length == 0) {
      return new ArrayList<>();
    }
    int n = nums.length;
    List<Integer> ans = new ArrayList<>(n);
    for (int i = 0; i < n; i++) {
      ans.add(0);
    }
    int[][] arr = new int[n][];
    for (int i = 0; i < n; i++) {
        // 要记录值,也要记录位置,防止排序后找不到值对应的位置在哪里
      arr[i] = new int[] {nums[i], i};
    }
    Arrays.sort(arr, Comparator.comparingInt(a -> a[0]));
    DynamicSegmentTree dst = new DynamicSegmentTree(n);
    for (int[] num : arr) {
      ans.set(num[1], dst.query(num[1] + 1, n));
      dst.add(num[1] + 1, 1);
    }
    return ans;
  }

其中 DynamicSegmentTree 结构就是前面提到的动态开点线段树的实现。

更多

算法和数据结构笔记

与动态开点线段树说明相似的内容:

动态开点线段树说明

动态开点线段树说明 作者:Grey 原文地址: 博客园:动态开点线段树说明 CSDN:动态开点线段树说明 说明 针对普通线段树,参考使用线段树解决数组任意区间元素修改问题 在普通线段树中,线段树在预处理的时候,需要申请 4 倍大小的数组空间来存放划分的区域, 而本文介绍的动态开点线段树,它和普通线段

Particles.js:为Web项目增添动态粒子效果

Particles.js:为Web项目增添动态粒子效果 示例 介绍 Particles.js是一个轻量级的JavaScript库,用于在Web页面上创建和管理动态粒子效果。它允许开发者通过简单的配置文件实现复杂的动画效果,为网页增添视觉吸引力。粒子可以是点、线、图像等,能够根据用户交互进行动态变化,

DPText-DETR: 基于动态点query的场景文本检测,更高更快更鲁棒 | 京东探索研究院

针对场景文本检测任务,近期基于DEtection TRansformer (DETR) 框架预测控制点的研究工作较为活跃。在基于DETR的检测器中,query的构建方式至关重要,现有方法中较为粗糙的位置先验信息构建导致了较低的训练效率以及性能。除此之外,在如何监督模型方面,之前工作中使用的点标签形式

初步动态规划讲解:数字三角形

题目描述 观察下面的数字金字塔。 写一个程序来查找从最高点到底部任意处结束的路径,使路径经过数字的和最大。每一步可以走到左下方的点也可以到达右下方的点。 7 3 8 8 1 0 2 7 4 4 4 5 2 6 5 在上面的样例中,从 7 → 3 → 8 → 7 → 5 7 \to 3 \to 8 \

【ASP.NET Core】动态映射MVC路由

ASP.NET Core 中的几大功能模块(Razor Pages、MVC、SignalR/Blazor、Mini-API 等等)都以终结点(End Point)的方式公开。在HTTP管道上调用时,其扩展方法基本是以 Map 开头,如 MapControllers、MapBlazorHub。 对于

声音好听,颜值能打,基于PaddleGAN给人工智能AI语音模型配上动态画面(Python3.10)

借助So-vits我们可以自己训练五花八门的音色模型,然后复刻想要欣赏的任意歌曲,实现点歌自由,但有时候却又总觉得少了点什么,没错,缺少了画面,只闻其声,却不见其人,本次我们让AI川普的歌声和他伟岸的形象同时出现,基于PaddleGAN构建“靓声靓影”的“懂王”。 PaddlePaddle是百度开源

一颗红心,三手准备,分别基于图片(img)/SCSS(样式)/SVG动画实现动态拉轰的点赞按钮特效

华丽炫酷的动画特效总能够让人心旷神怡,不能自已。艳羡之余,如果还能够探究其华丽外表下的实现逻辑,那就是百尺竿头,更上一步了。本次我们使用图片、SCSS样式以及SVG图片动画来实现“点赞”按钮的动画特效,并比较不同之处。 图片实现 最简单,也最容易理解的实现方式就是使用图片。曾几何时,几乎所有前端特效

< Python全景系列-2 > Python数据类型大盘点

Python作为一门强大且灵活的编程语言,拥有丰富的数据类型系统。本文详细介绍了Python中的每一种数据类型,包括数值、序列、映射、集合、布尔和None类型。每种数据类型的特性、使用方式,以及在实际问题中的应用都将被深入探讨。此外,我们还将探讨Python的动态类型特性,以及如何在实际编程中充分利用这些数据类型来简化代码和提高效率。在文章的最后,我还将分享一个可能你还不知道,但非常有用的特性。

Axure 元件--基本元件

图片:导入图看,根据图片实际大小显示,双击方框,再导入,根据控件的大小来显示图片 热区:1:增加锚点,制作点击事件;2:放在页面下方,控制滚动位置,比如其它控件在交互事件中找到它的坐标。 动态面版:交互神器 元件不知道他有什么作用时,双击一下,可进入元件内部 内联框架:跳转页面、跳转URL 中继器:

为什么反射慢?

反射机制就是通过字节码文件对象获取成员变量、成员方法和构造方法,然后进一步获取它们的具体信息,如名字、修饰符、类型等。 反射机制的性能较低有很多原因,这里详细总结以下4点原因: (1)JIT优化受限: JIT 编译器的优化是基于静态分析和预测的。反射是一种在运行时动态解析类型信息的机制,在编译时无法