动态顺序统计、区间树、线段树、树状数组、数据离散化
1、动态顺序统计
顺序统计指的是两个操作:
select(index,set)//找出集合中第index大的元素
rank (e, set) //给定一个元素,判断它在集合中排第几
动态顺序统计指的就是这个数据集合是会变动的,对于数据集合不会变动的情况,最好的方法自然是先排序,然后用二分查找实现upper_bound和lower_bound~~即STL的做法。
对于动态顺序统计算法导论上给出的解决方案是扩展红黑树,给每个节点增加一个size域,表示以该节点为根节点的子树一共有多少个节点。在维护红黑树的同时,动态维护size域的值。
对应的例子:Count of Smaller Numbers After Self
题目大意是给定一个数组,为数组中的每个数计算出一个属性,该属性的值等于 在这个元素右边的, 值比这个元素小的, 数的个数。
很明显的动态顺序统计,即从右到左依次将数组元素插入集合,同时计算rank(e - 1, set)就可以了。
但是写个红黑树很麻烦的好不好。。所以一般对于无法使用动态规划的、涉及区间的计算,通常使用线段树以及线段树的简化版,树状数组
2、线段树
线段树是基于分治,或者说二分的思想产生出来的数据结构。简单明了的说,就是把一个区间(1, n) 划分成一颗深度为lgN的树,树上的每个节点都代表一个区间,通常会根据具体需求再维护一些额外信息,一颗简单的线段树如下:
这个树看起来很像归并排序~~其实归并排序用的也是分治的思想,也是二分的分治么不是~~~
那么还是那个例子 Count of Smaller Numbers After Self,怎么用线段树搞呢~~
首先要遍历一遍这个数组,找到最大值max,最小值min,然后建一个[min, max + 1)的线段树, 对于这个线段树需要维护的额外信息就是一个num,即对于任意节点[i, j), num的值表示目前为止落在[i,j) 内的元素的个数。
具体到代码就是,对于每个元素,在线段树中找到这个元素,然后对从 根-》对应叶子节点的 路径上的所有节点的num,进行+1。
插入之后马上统计[1, e -1)的元素个数,找到对应的那两个节点,加一个num值就可以了,以上面[1, 10)的线段树为例子,对于任意的[i, j),都只要查找最多两条路径,然后把节点一凑就ok~
总的来说,线段树就是用了动态规划的二分算法,根据将问题分成很多小区间,求出结果并存到一个线段树中。
之所以要二分,因为这样的话树是最矮的,同样的,对于将区间进行二分并不能解决的问题,用其他的划分方法也是可以的,但是如果造成树过高的话,也失去了用线段树的意义
3、 区间树
区间树是算法导论上对红黑树的另一种扩展,将每个节点由一个关键字扩充为一个区间[i, j),以区间的左端点为关键字维护红黑树,同时增加一个max域,表示以该节点为根的子树中,所有的区间的右节点中,能够到达的最右边。
区间树可以很方便的对某个给定区间进行查询,查找某个区间与树中 哪些节点发生了 区间重叠,但是我不知道有神马用。。。。因为这个区间树并没有考虑树中各个节点是否产生了重叠的问题,所以用来统计区间信息似乎很不方便 - -!
so, 一般都用线段树,不用区间树,讲道理的话线段树应该是区间树的子集~就是让每个节点的区间恰好被其子节点不重叠的完全覆盖~~
同样的,方便的代价是从红黑树变成可“满二叉树”
4、树状数组
树状数组是简化版的线段树,树状数组只能统计[min, i)的信息,而不能统计任意的[i, j)的信息,但是树状数组比线段树快。虽然时间复杂度还是一样的> <
线段树: 如果[i,j) 可以由任意 [i,k) [k,j)求得,则可以 以lgN的复杂度 求任意[i, j)
树状数组:如果[i, j) 可以由任意的[i,k) [k,j)求得,则可以以 lgN 的复杂度求 任意[min, j),但是由于树状数组保存的区间数量比线段树少,所以只能通过求得[min, i)和[min, j)来计算。
如果问题不支持这么做,那么只能用线段树
总的来说,线段树和树状数组都要求问题[i, j)可以由任意的i <= k < j, 通过[i, k) [k, j)来计算(即大问题可以由随便划分的小问题求得),当这个条件满足时,线段树可以随意求[i, j), 区间树只能求[min, i), 要求[i, j),必须要满足[i, j)可以由[min, i) [min, j)求得,(即小问题可以由大问题以及其他的小问题求得)
凡是树状数组能干的区间树都能干,但是树状数组快一点
5、 数据离散化
还是以Count of Smaller Numbers After Self当例子,万一丫就给两个数,[10000, 11],难道我们得傻愣愣的建一个[11, 10000]的线段树么、、、、这个时候就得用到数据离散化,将原数组排序去重。大概操作是将原数组A的拷贝进行排序,去重,得到数组A',然后将A中的每一个元素用lower_bound离散化成它在A'中的位置,对,就是rank(A[i], A')。
如果还要保留一份对应关系用来还原的话,再拷贝一份就OK了~~
6、树状数组的一些特性
线段树由于它各种东西都很全,所以写起来很方便,树状数组就比较难理解- -!
树状数组是一个用来统计区间信息的数组,要求原数组和树状数组的下标都是从1开始的。

树状数组里有个对下标的操作lowbit,比如对于下标4, 它的二进制的最右端有2个连续的0,那么lowbit的值就是2^2次方,表示这个节点包含了从它往左数一共4个节点的统计信息。这个神奇的lowbit可以用lowbit(x) = x & (-x) 来实现~~
比如下标4的lowbit为4,表示它包括了4,3,2,1共4个节点的统计信息,即区间[1,5), 对于下标6, lowbit为2, 即e[6]包含了[5,7)的区间信息。同时每个e[i]都只好包含了a[i]的信息。
所以初始化的时候就这么干
for (int i = 0; i < e.size(); ++i) { rest = lowbit(i) - 1; add(e[i], a[i]); //每个节点e[i]至少包含a[i]的信息 while(rest) { //将每个节点的直接子节点的信息加入e[i],即把它管理的除了他自己的、lowbit(i) - 1个节点的信息加入e[i] add[e[i], e[rest]); rest -= lowbit(rest); } }
同样的,更新的时候就要从叶子节点向上一路更新到根节点,对于任意节点e[i],他的父节点下标为 e[i + lowbit[i]],即假如a[i] 的修改量为offset,那么更新就是
offset = newVal - a[i]; //得到offset while(i < e.size()) { e[i] += offset; i += lowbit(i); //指向父节点 }
7、 最后贴上Count of Smaller Numbers After Self的AC代码,树状数组实现。
class Solution { public: vector<int> countSmaller(vector<int>& nums) { //离散化,STL大法好 vector<int> copy(nums); sort(copy.begin(), copy.end()); auto end_it = unique(copy.begin(), copy.end()); //unique之后并不保证end_it之后的元素长啥样!!!所以在下面的upper_bound要使用end_it for (int i = 0; i < nums.size(); ++i) { nums[i] = upper_bound(copy.begin(), end_it, nums[i]) - copy.begin(); //使用upper_bound(copy.begin(), end_it, nums[i]), //而不是upper_bound(copy.begin(), copy.end(), nums[i])...... } vector<int> binaryIndexedTree(end_it - copy.begin() + 1, 0); vector<int> ret(nums.size(), 0); for(int i = nums.size() - 1; i >= 0; --i) { plusOne(nums[i], binaryIndexedTree); ret[i] = countVal(nums[i] - 1, binaryIndexedTree); } return ret; } private: inline int lowbit(int x) { return x & (-x); } void plusOne(int val, vector<int> &v) { //将val节点加入到树状数组v中,这将导致v[val]以及其父节点统计的元素数量全部+1 while (val < v.size()) { ++v[val]; val += lowbit(val); } } int countVal(int pos, vector<int> &v) { //统计树状数组v中,[1, pos + 1)的区间中,已经存在多少个节点 int sum = 0; while(pos) { sum += v[pos]; pos -= lowbit(pos); } return sum; } };
浙公网安备 33010602011771号