线段树
- 格式:doc
- 大小:46.50 KB
- 文档页数:4
线段树及其应用场景一、引言线段树(Segment Tree)是一种基于树状数组(Binary Indexed Tree)的数据结构,用于高效地处理区间查询问题。
它在许多算法和数据结构问题中都有广泛应用,如区间最值查询、区间修改和区间统计等。
二、线段树的定义和构建线段树是一种二叉树结构,每个节点代表一个区间。
叶子节点表示原始数据的单个元素,而非叶子节点则表示区间的合并结果。
线段树的构建过程可以通过递归或迭代的方式完成。
3、线段树的应用场景3.1 区间最值查询线段树的一个常见应用是区间最值查询。
给定一个数组,我们希望能够快速找到指定区间内的最大或最小值。
线段树能够在O(log n)的时间复杂度内完成单次查询,并且支持O(log n)的时间复杂度的区间修改操作。
3.2 区间修改线段树还可以用于区间修改问题。
例如,给定一个数组,我们需要对指定区间内的元素进行加法或乘法操作。
线段树可以在O(log n)的时间复杂度内完成单次修改,并且支持O(log n)的时间复杂度的区间查询操作。
3.3 区间统计线段树还可以用于区间统计问题。
例如,给定一个数组,我们需要统计指定区间内满足某种条件的元素个数。
线段树可以在O(log n)的时间复杂度内完成单次统计,并且支持O(log n)的时间复杂度的区间修改操作。
4、线段树的实现和优化4.1 线段树的存储结构线段树可以使用数组或链表来实现。
数组实现简单高效,但需要额外的空间;链表实现节省空间,但查询和修改操作的时间复杂度会增加。
4.2 线段树的查询和修改算法线段树的查询和修改算法可以通过递归或迭代的方式来实现。
递归实现简洁直观,但可能会导致函数调用过多;迭代实现效率较高,但代码复杂度较高。
4.3 线段树的优化技巧线段树的性能可以通过一些优化技巧来提升。
例如,使用延迟标记(Lazy T ag)来延迟区间修改操作的执行,减少递归或迭代次数;使用预处理技巧来提前计算一些中间结果,减少查询或修改的时间复杂度。
线段树中的操作方法
线段树是一种二叉树结构,用于高效地处理区间查询问题。
线段树常用于解决范围最值查询、区间修改等问题。
线段树的操作方法主要包括以下几点:
1. 构建线段树:线段树的构建方法一般是采用递归的方式,将待处理区间一分为二,构建左右子树,然后将左右子树的值合并到根节点。
2. 查询区间最值:对于一个区间查询问题,可以从根节点开始递归地向下查询。
如果当前节点表示的区间与待查询区间完全不重叠,则返回一个无效值。
如果当前节点表示的区间完全包含待查询区间,则返回当前节点的值。
如果当前节点表示的区间与待查询区间相交但不完全包含,则将查询问题分为左右两个子问题,然后将子问题的结果进行合并。
3. 区间修改:对于一个区间修改问题,可以从根节点开始递归地向下修改。
如果当前节点表示的区间与待修改区间完全不重叠,则不需要修改。
如果当前节点表示的区间完全包含待修改区间,则直接将当前节点的值修改为新值。
如果当前节点表示的区间与待修改区间相交但不完全包含,则将修改问题分为左右两个子问题,然后将子问题的结果进行合并。
4. 更新节点:在线段树中,如果某个节点的值发生了变化,则需要更新该节点
以及其父节点的值。
可以从叶节点开始递归地向上更新。
线段树的操作方法可以根据具体问题的需求进行扩展和优化。
以上是线段树的基本操作方法,具体实现时需要根据具体问题进行适当的调整和改进。
线段树区间最大值模板一、引言线段树是一种在区间求最大值的算法,常常用于处理一些涉及区间最大值的问题。
下面我们将介绍线段树的基本概念,并给出使用线段树解决区间最大值问题的模板。
二、线段树的基本概念线段树是一种将线段划分为若干个小区间,并使用树状结构来表示这些区间的划分方式。
在每个节点处,我们可以记录该节点覆盖的区间最大值,这样就可以方便地通过遍历树状结构来求出某个区间的最大值。
三、区间最大值问题的模板模板一:问题描述给定一个区间[a,b],请使用线段树来求出该区间的最大值。
模板二:代码实现假设线段树的节点类型为Node,其中Node包含一个整数值val和两个指向子节点的指针left和right。
以下是使用Python实现的线段树代码:```pythonclassNode:def__init__(self,val=None,left=None,right=None):self.val=valself.left=leftself.right=rightdefbuild_segment_tree(arr,start,end):ifstart>end:returnNonemid=(start+end)//2root=Node(arr[mid])root.left=build_segment_tree(arr,start,mid-1)root.right=build_segment_tree(arr,mid+1,end)returnrootdefquery(root,start,end):ifrootisNone:returnNoneifstart<=root.val<=end:returnroot.valreturnmax(query(root.left,start,end),query(root.right,start,end)) ```使用方法:首先构建线段树,然后通过查询节点来获取区间[a,b]的最大值。
历史区间最值线段树
历史区间最值线段树是一种数据结构,用于在一个固定的区间上进行最值查询,同时支持历史版本查询。
线段树是一种二叉树,其中每个节点代表一个区间,左子节点表示左半区间,右子节点表示右半区间。
每个节点保存了该区间的最值信息。
通过将整个区间递归地划分成子区间,可以构建出一棵线段树。
历史区间最值线段树基于线段树的基本思想进行扩展。
除了保存每个区间的最值信息外,它还保存了每个区间的历史版本信息。
在每次更新操作时,会复制一份当前区间的最值信息,然后在新版本中进行更新。
这样就可以记录每个区间每个版本的最值信息。
对于查询操作,可以通过二分法在历史版本上进行查找,找到指定版本的最值信息。
同时可以通过二分法在每个版本上进行查找,找到某个区间范围内的最值。
历史区间最值线段树主要用于解决一些需要查询历史版本最值信息的问题,比如求解一段时间范围内的最大或最小值。
它在某些场景下可以提供更高效的查询性能,但是也需要额外的空间来保存历史版本信息。
线段树的概念与应用线段树(Segment Tree)是一种用于解决区间查询问题的数据结构。
它可以高效地支持以下两种操作:区间修改和区间查询。
线段树的应用非常广泛,在离线查询、区间统计、区间更新等问题中有着重要的作用。
一、概念线段树是一颗二叉树,其中每个节点代表了一个区间。
根节点表示整个待查询区间,而叶子节点表示的是单个元素。
每个内部节点包含了其子节点所代表区间的并集。
二、构建线段树线段树的构建过程是自底向上的。
将待查询数组划分成一颗满二叉树,并将每个区间的和存储在相应的节点中。
对于叶子节点,直接存储对应元素的值。
而非叶子节点的值可以通过其子节点的值计算得到。
三、线段树的查询对于区间查询操作,可以通过递归方式实现。
从根节点开始,判断查询区间和当前节点所代表的区间是否有交集。
若没有交集,则返回当前节点的默认值。
若查询区间包含当前节点所代表的区间,则返回当前节点存储的值。
否则,将查询区间分割成左右两部分继续递归查询。
四、线段树的更新对于区间更新操作,也可以通过递归方式实现。
与查询操作类似,首先判断查询区间和当前节点所代表的区间的关系。
若没有交集,则无需更新。
若查询区间包含当前节点所代表的区间,则直接更新当前节点的值。
否则,将更新操作分割成左右两部分继续递归更新。
五、应用案例:区间最值查询一个常见的线段树应用是求解某个区间的最值。
以查询区间最小值为例,可以通过线段树来高效地解决。
首先构建线段树,然后进行区间查询时,分为以下几种情况处理:若当前节点所代表的区间完全包含于查询区间,则直接返回该节点的值;若当前节点所代表的区间与查询区间没有交集,则返回默认值;否则,将查询区间分割成左右两部分继续递归查询,最后返回两个子区间查询结果的较小值。
六、总结线段树是一种非常有用的数据结构,能够高效地解决区间查询问题。
通过合理的构建和操作,线段树可以应用于多种场景,如区间最值查询、离线查询等。
熟练掌握线段树的概念和应用方法,对解决问题具有重要意义。
线段树求解区间最大公约数好了,今天我们聊聊线段树和区间最大公约数(GCD)。
听起来有点学术对吧?别担心,别一听这名字就往头上顶,来,咱们慢慢聊,肯定能让你觉得其实这玩意儿挺简单。
先说说,区间最大公约数是什么?大白话就是,给你一段数,问你这段数里面,能整除所有数的最大的那个数到底是多少。
比方说,给你一段数,求这三者的最大公约数。
你说,12能整除24,能整除36,24和36互相之间也能整除,那最大公约数就是12。
是不是挺简单的?但是要是给你一大堆数,这个问题可就复杂了。
假如这段数的长度是10万,你可不可能一个个去算?太浪费时间了,对吧?这时候线段树就登场了。
线段树啊,听起来很酷,其实它就是一个用来处理区间问题的二叉树。
就像是一个能高效处理大量数据的“小帮手”,一旦有了它,我们就可以瞬间把问题解决,别看它名字生疏,其实好用得很!你可以把线段树想象成一个超级有条理的仓库。
仓库里面分了好多格子,每个格子里都有一段数据。
你想要查哪一段的最大公约数,就可以直接找过去,根本不用一项一项地查看。
这就像你去超市,想找番茄酱,直接去酱料区,而不是一瓶一瓶地在每个货架上找。
是不是立马让你感到轻松多了?不过,问题来了,如何在这么复杂的结构中找到每一段数据的最大公约数呢?其实很简单,线段树的每个节点代表的是一个区间,而每个节点存储的是它所管辖区间的最大公约数。
比如,节点代表的是的最大公约数,线段树就会把你指向包含这个区间的最小几个节点,快速把答案给你。
所以,我们通过查询,能很快知道某一段区间的最大公约数。
简直是秒杀所有传统方法。
你可能会想,既然查起来这么方便,那更新数据是不是也很麻烦?其实也不难!线段树的更新也是基于树的结构进行的。
比如说,某个数发生了变化,更新操作就会沿着树的路径向上走,更新每一个与这个数相关的节点。
这种操作类似你换灯泡:换一个,然后每次走回去检查一遍,确保没问题。
就这么简单,效率超高。
想象一下,我们的线段树就像一位经验丰富的工匠,手里拿着工具箱,每当遇到问题时,总能快速拆解,不拖泥带水。
线段树入门OI中经常能碰见统计类型的题目。
要求出某些时刻、某种情况下,状态是怎样的。
这类问题往往比较简单明了,也能十分容易地写出模拟程序。
但较大的数据规模使得模拟往往不能满足要求。
一、线段树的结构:线段树是一棵二叉树,其结点是一条“线段”——[a,b],它的左儿子和右儿子分别是这条线段的左半段和右半段,即[a, (a+b)/2]和[(a+b)/2,b]。
线段树的叶子结点是长度为1的单位线段[a,a+1]。
下图就是一棵根为[1,10]的线段树:【1,10】/ \【1,5】【5,10】 / \/ \【1,3】【3,5】【5,7】【7,10】/ \ / \ /\ / \【1,2】【2,3】【3,4】【4,5】【5,6】【6,7】【7,8】【8,10】 / \【8,9】【9,10】易证一棵以[a,b]为根的线段树结点数是2*(b-a)-1。
由于线段树是一棵平衡二叉树,因此一棵以[a,b]为根的线段树的深度为log2(2*(b-a))。
一般采用静态完全二叉树来表示线段树。
(但有时有一些缺点)struct segtree{int a,b,left,right;};其中分别表示线段的左端点和右端点,Left,Right 表示左儿子和右儿子的编号。
因此我们可以用一个一维数组来表示一棵线段树:segtree tree[MaxN];根据实际需要,我们可以增加其它的域,例如cover域来计算该线段被覆盖的次数,或是其它一些形式的域。
二、建立线段树:void build(int step, int s, int t){a[step].a = s;a[step].b = t;a[step].cn = 0;if (t-s>1){int mid=(s+t)/2;build(step*2, s, mid);build(step*2+1, mid, t);}}建立以step为根结点,范围从s到t的线段树。
这里应用了完全二叉树的特点:step 的左孩子结点编号为2*step,右孩子结点编号为2*step+1。
线段树维护标记经典题
线段树是一种用于解决区间查询问题的数据结构,它可以高效地处理区间操作,例如区间最大值、区间和、区间更新等。
线段树通常用于解决动态区间查询问题,比如区间修改、区间查询等。
在线段树中,维护标记是一种常见的技巧,它可以帮助我们在区间内进行延迟更新,从而减少不必要的操作。
维护标记通常用于延迟更新操作,当我们需要对区间内的元素进行更新时,我们可以先将更新操作记录在节点上,而不立即更新区间内的所有元素,这样可以减少不必要的更新操作,提高效率。
经典题目中常涉及使用线段树维护标记的问题包括区间修改、区间查询等。
例如,区间修改问题可以是给定一个数组,进行一系列区间修改操作,然后查询某个位置的元素值;区间查询问题可以是给定一个数组,进行一系列区间查询操作,然后求解区间内的最大值、最小值、和等。
在解决经典题目时,我们需要注意线段树的构建、更新、查询等操作,以及维护标记的技巧,这样才能高效地解决问题。
同时,我们也需要注意处理边界情况和特殊情况,保证算法的正确性和鲁
棒性。
总之,线段树是一种非常有用的数据结构,通过合理地维护标记,我们可以解决许多经典的区间查询问题。
在解决这些问题时,
我们需要充分理解线段树的原理和操作,灵活运用维护标记的技巧,才能更好地解决问题。
历史版本和线段树
历史版本和线段树是计算机科学中的两个重要概念。
历史版本,顾名思义,是指数据结构在不同时间点上的不同状态。
在软件开发中,我们经常需要对数据进行修改和更新,但有时候我们也需要回溯到前面的某个状态。
历史版本技术就是为了解决这个问题而提出的。
它可以记录数据结构在不同时间点上的状态,并且可以在需要时恢复到特定的历史状态。
这在很多应用中都非常有用,比如版本控制系统和数据库管理系统。
线段树是一种用于解决区间查询的数据结构。
它可以高效地计算一个区间内的某种属性或者进行某种操作,比如求和、最大值、最小值等。
线段树通过将区间逐步细分成更小的子区间,并且计算每个子区间的属性来构建一个树状结构,从而实现高效的查询和修改。
线段树广泛应用于各种领域,比如计算几何、图像处理、离散数学等。
历史版本和线段树都是计算机科学中非常有用的技术。
历史版本可以帮助我们记录和恢复数据结构的历史状态,而线段树可以高效地解决区间查询问题。
它们在各种应用中都发挥着重要作用,并且得到了广泛的研究和应用。
菜鸟都能理解的线段树入门经典线段树的定义首先,线段树既是线段也是树,并且是一棵二叉树,每个结点是一条线段,每条线段的左右儿子线段分别是该线段的左半和右半区间,递归定义之后就是一棵线段树,图示如下图1.线段树示意图定义线段树的数据结构struct Line{int left, right, count;Line *leftChild, *rightChild;Line(int l, int r): left(l), right(r) {}};PS:其中的count字段表示该条线段有几条明白了线段树的定义之后,我们来举一个例子来说明线段树的应用例题:给定N条线段,{[2, 5], [4, 6], [0, 7]}, M个点{2, 4, 7},判断每个点分别在几条线段出现过看到题目,有的人第一感觉想出来的算法便是对于每一个点,逐个遍历每一条线段,很轻松地判断出来每个点在几条线段出现过,小学生都会的算法,时间复杂度为O(M*N)如国N非常大,比如2^32-1, M也非常大M = 2^32 - 1, O(M*N)的算法将是无法忍受的,这个时候,线段树便隆重登场了线段树的解法:1.首先,我们找出一个最大的区间能够覆盖所有的线段,遍历所有的线段,找线段的最值(左端点的最小值,右端点的最大值)便可以确定这个区间,对于{[2, 5], [4, 6], [0, 7]}, 这个区间为[0, 7],时间复杂度为O(N)2.然后,根据此区间建一棵线段树(见图1), 时间复杂度为O(log(MAX-MIN))3.对于每一条线段A,从根节点开始遍历这棵线段树,对于每一个当前遍历的结点NODE(其实线段树中每一个结点就是一条线段),考虑三种情况a)如果线段A包含在线段NODE的左半区间,那么从NODE的左儿子(其实就是NODE 的左半区间)开始遍历这棵树b)如果线段A包含在线段NODE的右半区间,那么从NODE的右儿子(其实就是NODE 的右半区间)开始遍历这棵树c)如果线段A刚好和线段NODE重合,停止遍历,并将NODE中的count字段加1d)除了以上的情况,就将线段A的左半部分在NODE的左儿子处遍历,将A的右半部分在NODE的右儿子处遍历补充说明:对于以上的步骤,所做的工作其实就是不断地分割每一条线段,使得分割后的每一条小线段刚好能够落在线段树上,举个例子,比如要分割[2, 5],首先将[2, 5]和[0, 7]比较,符合情况d, 将A分成[2, 3]与[4, 5]I)对于[2, 3]从[0, 7]的左半区间[0, 3]开始遍历将[2, 3]与[0, 3]比较,满足情况b,那么从[0, 3]的右半区间[2, 3]开始遍历,发现刚好重合,便将结点[2, 3]count字段加1II)对于[4, 5]从[0, 7]的右半区间[4, 7]开始遍历将[4, 5]与[4, 7]比较,满足情况b,从[4, 7]的左半区间[4, 5]开始遍历,发现刚好重合,便将结点[4, 5]count字段加1于是对于[2, 5]分割之后线段树的情况为图2图2.分割[2,5]之后线段树的情况显然,我们看到,上述的遍历操作起始就是将[2, 5]按照线段树中的线段来分割,分割后的[2, 3]与[4, 5]其实是与[2, 5]完全等效的最后,我们将剩下的两条线段按照同样的步骤进行分割之后,线段树的情况如下图3这一步的时间复杂度为O(N*log(MAX-MIN))4.最后,对于每一个值我们就可以开始遍历这一颗线段树,加上对于结点的count字段便是在线段中出现的次数比如对于4,首先遍历[0, 7],次数= 0+1=1;4在右半区间,遍历[4, 7],次数= 1+0=0;4在[4, 7]左半区间, 次数= 1+2=3;4在[4, 5]左半区间,次数= 3+0 = 4,遍历结束,次数= 3说明4在三条线段中出现过,同理可求其他的值,这一步的时间复杂度为O(M*log(MAX-MIN))最后,总的时间复杂度为O(N)+O(log(MAX-MIN))+O(N*log(MAX-MIN))+(M*log(MAX-MIN)) = O((M+N)*log(MAX-MIN))由于log(MAX-MIX)<=64所以最后的时间复杂度为O(M+N)最后,放出源码[cpp] view plaincopy#include <iostream>using namespace std;struct Line{int left, right, count;Line *leftChild, *rightChild;Line(int l, int r): left(l), right(r) {}};//建立一棵空线段树void createTree(Line *root) {int left = root->left;int right = root->right;if (left < right) {int mid = (left + right) / 2;Line *lc = new Line(left, mid);Line *rc = new Line(mid + 1, right);root->leftChild = lc;root->rightChild = rc;createTree(lc);createTree(rc);}}//将线段[l, r]分割void insertLine(Line *root, int l, int r) {cout << l << " " << r << endl;cout << root->left << " " << root->right << endl << endl;if (l == root->left && r == root->right) {root->count += 1;} else if (l <= r) {int rmid = (root->left + root->right) / 2;if (r <= rmid) {insertLine(root->leftChild, l, r);} else if (l >= rmid + 1) {insertLine(root->rightChild, l, r);} else {int mid = (l + r) / 2;insertLine(root->leftChild, l, mid);insertLine(root->rightChild, mid + 1, r);}}}//树的中序遍历(测试用)void inOrder(Line* root) {if (root != NULL) {inOrder(root->leftChild);printf("[%d, %d], %d\n", root->left, root->right, root->count);inOrder(root->rightChild);}}//获取值n在线段上出现的次数int getCount(Line* root, int n) {int c = 0;if (root->left <= n&&n <= root->right)c += root->count;if (root->left == root->right)return c;int mid = (root->left + root->right) / 2;if (n <= mid)c += getCount(root->leftChild, n);elsec += getCount(root->rightChild, n);return c;}int main() {int l[3] = {2, 4, 0};int r[3] = {5, 6, 7};int MIN = l[0];int MAX = r[0];for (int i = 1; i < 3; ++i) {if (MIN > l[i]) MIN = l[i];if (MAX < r[i]) MAX = r[i];}Line *root = new Line(MIN, MAX);createTree(root);for (int i = 0; i < 3; ++i) {insertLine(root, l[i], r[i]);}inOrder(root);int N;while (cin >> N) {cout << getCount(root, N) << endl; }return 0;}。
什么是线段树线段树也被称为区间树,英文名为Segment Tree或者Interval tree,是一种高级的数据结构。
这种数据结构更多出现在竞赛中,在常见的本科数据结构教材里没有介绍这种数据结构。
但是,在面试中却有可能碰到和线段树相关的问题。
那么为什么会产生线段树这种数据结构,线段树到底是为了解决什么样的一种问题呢?其实这里的线段可以理解为区间,线段树就是为了解决区间问题的。
有一个很经典的线段树问题是:区间染色。
假设有一面墙,长度为 n,每次选择一段墙进行染色。
在区间染色的过程中,每次选择一段区间进行染色,这时新的颜色可能会覆盖之前的颜色。
最后的问题是:在经过 m 次染色操作后,我们可以在整个区间看见多少种颜色?更加普遍的说法是:在经过 m 次染色操作后,我们可以在区间 [i, j]内看见多少种颜色?由于第一个问题是第二个问题的一个特例,我们采用第二种问题来思考解决方法。
从上面可以看出,我们对于区间,有 2 种操作,分别是染色操作和查询区间的颜色,使用更加一般的说法,染色操作就是更新区间,查询区间的颜色就是查询区间。
这类问题里面,更加常见的的是区间查询:一个数组存放的不再是颜色,而是具体的数字,查询某个区间[i, j]的统计值。
这里的统计值是指:区间内最大值、最小值、或者这个区间的数字和。
比如:查询 2018 年注册的用户中消费最高的用户查询 2018 年注册的用户中消费最低的用户注意上面两种情况都是动态查询,我们查询的消费数据不只是 2018 的消费数据。
如果我们想查询 2018 年中消费最高的用户,那么 2018 年的数据已经固定了,我们直接在这一年的数据中进行统计分析就行了。
但是一个 2018 年注册的用户,在 2019 年、2020 年都可能会有消费。
我们实际上查询的是:2018 年注册的用户中,到现在为止,消费最高的用户。
这种情况下,数据是在动态变化的,也就是说:2017 年注册的用户中,每个用户的消费额是会更新的,这就对应到更新区间的操作。
线段树
目录
定义
基本结构
实现代码
树状数组
编辑本段定义
区间在[1,5]内的线段树
线段树又称区间树,是一种对动态集合进行维护的二叉搜索树,该集合中的每个元素 x 都包含一个区间 Interval [ x ]。
线段树支持下列操作:
Insert(t,x):将包含区间 int 的元素 x 插入到树中;
Delete(t,x):从线段树 t 中删除元素 x;
Search(t,i):返回一个指向树 t 中元素 x 的指针。
编辑本段基本结构
线段树是建立在线段的基础上,每个结点都代表了一条线段[a , b]。
长度为1的线段称为元线段。
非元线段都有两个子结点,左结点代表的线段为[a , (a + b ) / 2],右结点代表的线段为[( a + b ) / 2 , b]。
右图就是一棵长度范围为[1 , 5]的线段树。
长度范围为[1 , L] 的一棵线段树的深度为log ( L - 1 ) + 1。
这个显然,而且存储一棵线段树的空间复杂度为O(L)。
线段树支持最基本的操作为插入和删除一条线段。
下面以插入为例,详细叙述,删除类似。
将一条线段[a , b] 插入到代表线段[l , r]的结点p中,如果p不是元线段,那么令mid=(l+r)/2。
如果b<mid,那么将线段[a , b] 也插入到p的左儿子结点中,如果a>mid,那么将线段[a , b] 也插入到p的右儿子结点中。
插入(删除)操作的时间复杂度为O (Log N)。
上面的都是些基本的线段树结构,但只有这些并不能做什么,就好比一个程序有输入没输出,根本没有任何用处。
最简单的应用就是记录线段有否被覆盖,并随时查询当前被覆盖线段的总长度。
那么此时可以在结点结构中加入一个变量int count;代表当前结点代表的子树中被覆盖的线段长度和。
这样就要在插入(删除)当中维护这个count值,于是当前的覆盖总值就是根节点的count值了。
另外也可以将count换成bool cover;支持查找一个结点或线段是否被覆盖。
[1]
相信对算法设计或者数据结构有一定了解的人对线段树都不会太陌生。
它是能够在log(MaxLen)时间内完成线段的添加、删除、查询等操作。
但一般的实现都有点复杂而线段树应用中有一种是专门针对点的。
(点树?)它的实现却非常简单。
这种数据结构有什么用?我们先来考虑一下下面的需求(全部要求在LogN 时间内完成):如何知道一个点在一个点集里的大小“排名”?很简单,开一个点数组,排个序,再二分查找就行了;如何在一个点集内动态增删点?也很简单,弄个平衡树就行了(本来平衡树比线段树复杂得多,但自从世界上有了STL set 这么个好东东,就……^_^)那如果我既要动态增删点,也要随时查询到一个点的排名呢?那对不起,可能就要出动到我们的“点树”了。
其实现原理很简单:每当增加(或删除)一个大小为X的点时,就在树上添加(或删除)一条(X,MaxLen)的线段(不含端点),当要查询一个点的排名时,只要看看其上有多少条线段就可以了。
针对这一需求,这里有个非常简单的实现(见以下代码,十多行,够短了吧?)其中clear()用于清空点集;add()用于添加一个点;cntLs()返回小于n的点的个数,也就是n的升序排名,类似地cntGt 是降序排名。
这个点树有什么用呢?其中一个应用时在O(NlogN)时间内求出一个排列的逆序数(/show_problem.php?pid=1484,你有更好的算法吗?欢迎交流)方法是每读到一个数x,就让逆序数+=cntGt(x);然后再
add(x)。
这个实现还可以进行一些扩展。
比如删除del(int n),只要把add(int n)中的++size换成--size,把a[i/2]++改成a[i/2]--即可。
另外还可以通过二分查找功能在O(logN)时间内查到排名第n的点的大小。
应该也可以三四行内搞定。
补充:杨弋同学在2008年信息学奥赛冬令营上新发明了一种线段树的省空间堆式存储法,具体方法可以见08年冬令营课件.
编辑本段实现代码
template < int N > // 表示可用区间为[0,N),其中N必须是2的幂数;
class PointTree {
int a[ 2 * N];
int size;
void clear() { memset( this , 0 , sizeof ( * this ));}
void add( int n) {
int i = N + n; ++ size;
for ( ++ a[i]; i > 1 ; i /= 2 )
if ( ~ i & 1 ) a[i / 2 ] ++ ;
}
int cntLs( int n) { // 统计小于
int i = N + n,c = 0 ; // 若统计小于等于则c=a;
for (; i > 1 ; i /= 2 )
if (i & 1 ) c += a[i / 2 ];
return c;
}
int cntGt( int n) { return size - a[N + n] - cntLs(n); } } ;
void del(int n){
if(!a[n+=N])return;
--size;
for(--a[n]; n>1; n/=2)
if(~n&1)--a[n/2];
}
/**//* 解决:求点集中第i小的数(由0数起)
* 注意:如果i>=size 返回N-1
*/
int operator[](int n){
int i=1;
while(i<N){
if(n<a) i*=2;
else n-=a, i=i*2+1;
}
return i-N;
}
};
//附一个测试程序
#include<iostream.h>
T<8192> t;
int main(){
char c; int n;
while(cin>>c){
if(c=='c') t.clear();
else{
cin>>n;
if(c=='a') t.add(n);
if(c=='d') t.del(n);
if(c=='q') cout<<t[n]<<endl;
}
}
return 0;
}
编辑本段树状数组
另一种功能上比较类似的数据结构:“树状数组”。
它们有不少相似之处:针对点集的处理(添加、删除、查找);
相似的时空复杂度(logN时间,2N空间);
相似的编程复杂度(都比线段树简短得多);
因此,所有可以用树状数组解决的问题都可以用这个“点树”来解决,另外它还有以下好处:
更直观的转移;
同时支持自下而上和自上而下两种方向的查找和更新,而后者树状数组不支持,所以树状数组不提供某些功能,比如说O(logN)求点集中第k小数。