博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
Day 2 上午
阅读量:5235 次
发布时间:2019-06-14

本文共 9887 字,大约阅读时间需要 32 分钟。

内容提要:

二叉搜索树

二叉堆

区间RMQ问题

 


 

二叉搜索树

前置技能

本节课可能用到的一些复杂度:
O(log n).

n/1+n/2+...+n/n=O(n log n)

入门题:

给出N次操作,每次加入一个数,删除一个之前加入过的数,或者询问当前所有数的最大值。

N ≤ 100000.
二叉搜索树
二叉搜索树(BST)是用有根二叉树来存储的一种数据结构,二叉树中每个节点代表一个数据。每个节点包含一个指向父亲的指针,和两个指向儿子的指
针。如果没有则为空。
每个节点还包含一个key值,代表他本身这个点的权值
特征:
二叉搜索树的key值是决定树形态的标准。
每个点的左子树中,节点的key值都小于这个点。
每个点的右子树中,节点的key值都大于这个点。
示例:

 

用结构体或者是若干个数组存储

在接下来的介绍中,我们将以ls[x]表示x的左儿子,rs[x]表示x的右儿子,fa[x]表示x的父亲,key[x]表示x这个点的权值。

BST是一种支持对权值进行查询的数据结构,它兹磁:
插入一个数,删除一个数,询问最大/最小值,询问第k大值。
当然,在所有操作结束后,它还能把剩下的数从小到大输出来

查询最大/最小值
注意到BST左边的值都比右边小,所以如果一个点有左儿子,就往左儿子走,否则这个点就是最小值啦。

代码(最小值)

int FindMin(){    intx = root;    while (ls[x]) x = ls[x];    return key[x];}

一直找左儿子

现在我们要插入一个权值为x的节点。
为了方便,我们插入的方式要能不改变之前整棵树的形态。
首先找到根,比较一下key[root]和x,如果key[root] < x,节点应该插在root右侧,否则再左侧。
看看root有没有右儿子,如果没有,那么直接把root的右儿子赋成x就完事了。
否则,为了不改变树的形态,我们要去右儿子所在的子树里继续这一操作,直到可以插入为止。

代码:

void insert(intval){    key[+ + tot] = val; ls[tot] = rs[tot] = 0;    int now = root;//当前访问的节点为now.    for(; ; )    {        if (val < key[now])        if (!ls[now]) ls[now] = x, fa[x] = now, break;         else now =ls[now];        else if (!rs[now]) rs[now] = x, fa[x] =now, break;         else now = rs[now];    }}

删除一个值

现在我们要删除一个权值为x的点

之前增加一个点我们能够不改变之前的形态,那删除可以吗?

定位一个节点
要删掉一个权值,首先要知道这个点在哪。
从root开始,像是插入一样找权值为x的点在哪

每次比较当前的key值和节点的大小关系,然后重复操作

代码:

int Find(int x){    int now = root;    while(key[now]! = x)        if (key[now] < x) now = rs[now]; else now = ls[now];    return now;}

删除的方案:

方案一

直接把这个点赋成一种空的状态.但是这样的话查询起来不太方便(会有非常大的困难)。
所以还是稍微麻烦一点吧。

方案二

对这个节点x的儿子情况进行考虑。
如果x没有儿子,删掉x就行了。
如果x有一个儿子,直接把x的儿子接到x的父亲下面就行了。
这样子是肯定没有问题的,因为左边总比右边小嘛,所以x的儿子肯定比右边的大儿子小,这样就可以交换了
如果x有两个儿子,这种情况就比较麻烦了。
定义x的后继y,是x右子树中所有点里,权值最小的点。(有很多方法,这样比较方便,如果找左子树中最大的或者是直接删掉然后调整顺序应该也可以)
找这个点可以x先走一次右儿子,再不停走左儿子。
如果y是x的右儿子,那么直接把y的左儿子赋成原来x的左儿子,然后用y代替x的位置。

 

代码:

build2

先排序,从中间分开,两边生成的二叉搜索树接到中间的点上

求解第k大值(小扩展)
如果不能求的话它将会被堆完爆。
对每个节点在多记一个size[x]表示x这个节点子树里节点的个数(包含他自己)。
每个节点的size就是他的左右儿子的size之和+1
从根开始,如果右子树的size ≥ k,就说明第k大值在右侧,往右边走,如果右子树size + 1 = k,那么说明当前这个点就是第k大值。
否则,把k减去右子树size + 1,然后递归到左子树继续操作。
有相等的数据的话可以定义右边的为≥
插入或者删除的话他所有父亲的size都要+1或者-1
二叉搜索树也可以实现一个排序的作用
遍历
注意到权值在根的左右有明显的区分。.
做一次中序遍历就可以从小到大把所有树排好了。(左儿子--中节点--右儿子)

int dfs(int now){    if (ls[now]) dfs(ls[now]);    print(key[now]);    if (rs[now]) dfs(rs[now]);}

回到最初的题

让我们回到最初的题。
一个良好的例子:3 1 2 4 5
一个糟糕的例子:1 2 3 4 5
二叉搜索树最坏的情况下每次操作访问O(h)个节点。(h为树高)

总结
既然他的复杂度与直接暴力删除类似,那我们为什么要学他呢?
1.因为教学安排里有(大误).
2.这是第一个能够利用树的中序遍历的性质的数据结构。
3.扩展性强。更复杂的splay,treap,SGT等都基于二叉搜索树,只是通过一些对树的形态的改变来保证操作的复杂度,且保持树中序遍历的形态。
4.因为数据很水。随机数据还是很强势的(树高期望是log n)

 

 二叉堆

满 二叉树:除了最后一层节点其他都是满的,而且最后一层也是从左到右

每个节点的儿子是当前节点的序号*2和序号*2+1;
每个节点的父亲就是当前节点的序号/2

 

定义

用二叉搜索树还是没法解决我们之前的问题。
堆是一种特殊的二叉树,并且是一棵满二叉树。
第i个节点的父亲是i/2,这样我们就不用存每个点的父亲和儿子了。
二叉搜索树需要保持树的中序遍历不变,而堆则要保证每个点比两个儿子的权值都小。

 

建堆

首先是要建出这么一个堆,最快捷的方法就是直接O(N log N)排一下序。
反正堆的所有操作几乎都是O(log N)的。
之后可以对这个建堆进行优化。

 

求最小值

可以发现每个点都比两个儿子小,那么最小值显然就是a[1]辣,是不是很simple啊。

 

插入一个值

注意到二叉搜索树中的复杂度都是O(h).
在堆中我们也想让复杂度是O(h) = O(log n).
这样一来我们就要让树的形态不变,所以我们每次改变的都是权值的位置。
首先我们先把新加入的权值放入到n + 1的位置。
然后把这个权值一路往上比较,如果比父亲小就和父亲交换.
注意到堆的性质在任何一次交换中都满足。

 

修改一个点的权值

咦,为什么没有删除最小值?
删除最小值只要把一个权值改到无穷大就能解决辣
比较简单的是把一个权值变小,那只要把这个点像插入一样向上动就行了。

 

变大权值
那么这个点应该往子树方向走。
看看这个点的两个儿子哪个比较小,如果小的那个儿子的权值比他小,就交换,直到无法操作。
就是不断找到两个儿子中较小的一个进行交换

 

解决定位问题

一般来说,堆的写法不同,操作之后堆的形态不同,所以一般给的都是改变一个权值为多少的点.
假设权值两两不同,再记录一下某个权值现在哪个位置,在交换权值的时候顺便交换位置信息。

 

删除权值

理论上来说删除一个点的权值就只需要把这个点赋成inf 然后down一次。
但是这样堆里的元素只会越来越多(最后一层全是inf)
我们可以把堆里第n号元素跟这个元素交换一下。
然后n--,把堆down一下就行了。

#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#define N 300005#define M 8000005#define ls (t<<1)#define rs ((t<<1)|1)#define mid ((l+r)>>1)#define mk make_pair#define pb push_back#define fi first#define se secondusing namespace std;int i,j,m,n,p,k,a[N];int FindMin(){ return a[1];}void build1(){ sort(a+1,a+n+1);}void up(int now){ while (now&&a[now]
a[now*2]) swap(a[now],a[now*2]),now*=2; } else { if (a[now]<=a[now*2]&&a[now]<=a[now*2+1]) break; if (a[now*2]
val) { a[x]=val; up(x); } else { a[x]=val; down(x); }}void build2(){ for (i=n/2;i>=1;--i) down(i);}int main(){ scanf("%d",&n); for (i=1;i<=n;++i) scanf("%d",&a[i]); build2();}

del那里可以比较两个大小再选择up或者down,也可以先up再down

建堆

现在来考虑一种新的建堆方法。
倒序把每个节点都down一下,正确性肯定没有问题。
复杂度n/2 + n/4 * 2 + n/8 * 3 + .... = O(n)

堆排序
给N个数,输出他们从小到大排序的结果.
N ≤ 100000.

题解:

把数全部插进去,每次询问最小值,然后把根删掉就行了.
复杂度O(N log N).

丑数
丑数指的是质因子中仅包含2, 3, 5, 7的数,最小的丑数是1,求前k个丑数。
K ≤ 6000.

题解:

打表大法好!没有什么是打表解决不了的.
算了说正经的。
考虑递增的来构造序列.
x被选中之后,接下来塞进去x * 2, x * 3, x * 5, x * 7.
如果当前最小的数和上一次选的一样,就跳过.
复杂度O(K log N).

Queue

每次都要写堆太麻烦了有没有什么方便的。
在C + +的include < queue >里有一个叫priority queue的东西。

基本操作

Q.push()Q.top()Q.pop()Q.clear()

set

堆好弱小啊,有没有什么更好用的。
在C + +的include < set >里有一个叫set的东西。
高级的二叉搜索树
左闭右开

基本操作:

set
stst.insert()//插入st.erase()//删除st.fnd()//查找st.lower/upper bound()//第一个≥他的位置/第一个>他的位置st.begin()/st.end()//最大值/最小值后面的部分有独有的迭代器set
::iterator it=st.lower_bound(x);++it;--it;//下标右移一位/左移一位x=*it;//下表所对应的值

堆有啥用

我也不知道它有啥用(大雾
了解一种数据结构
为将来学习可并堆,斐波那契堆打下坚实基础(政治课即视
比STL快。
能优化

RMQ

区间RMQ问题是指这样一类问题。
给出一个长度为N的序列,我们会在区间上干的什么(比如单点加,区间加,区间覆盖),并且询问一些区间有关的信息(区间的和,区间的最大值)等。

最简单的问题
给出一个序列,每次询问区间最大值.
N ≤ 100000, Q ≤ 1000000

ST表

ST表是一种处理静态区间可重复计算问题的数据结构,一般也就求求最大最小值辣。
ST表的思想是先求出每个[i, i + 2k)的最值。
注意到这样区间的总数是O(N log N)的.
log N这一复杂度是OI最常用复杂度。
而sqrt(N)是OI最玄学的复杂度。

预处理
不妨令fi,j为[i, i + 2^j)的最小值。
那么首先fi,0的值都是它本身。
而fi,j = min(fi,j−1, fi+2^j−1,j−1)
这样在O(N log N)的时间内就处理好了整个ST表

ST表代码:

#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#define N 300005#define M 8000005#define K 18 #define ls (t<<1)#define rs ((t<<1)|1)#define mid ((l+r)>>1)#define mk make_pair#define pb push_back#define fi first#define se secondusing namespace std;int i,j,m,n,p,k,ST[K+1][N],a[N],Log[N];int Find(int l,int r){ int x=Log[r-l+1]; return max(ST[x][l],ST[x][r-(1<

询问

比如我们要询问[l, r]这个区间的最小值.
找到最大的k满足2^k ≤ r − l + 1.
取[l, l + 2^k), [r − 2^k + 1, r + 1)这两个区间。
注意到这两个区间完全覆盖了[l, r],所以这两个区间最小值
较小的一个就是[l, r]的最小值。
注意到每次询问只要找区间就行了,所以复杂度是O(1).

注意
ST表确实是一个询问O(1)的数据结构,但是它的功效相对也较弱.
例如每次求一个区间的和,利用前缀可以做
到O(N) − O(1).而ST却无法完成。

做题四部曲:

比较简单的问题

给出一个序列,支持对某个点的权值修改,或者询问某个区间的最大值.
N, Q ≤ 100000

冷静分析
嗯,刚学了ST表
它只要O(1)就能询问了。
能不能让它动起来呢?

仔细思考

来考虑一下能不能强行维护ST表.
比如改5这个点.
j = 0 改了一个位置,嗯,完美.
j = 1 改了两个位置4, 5,稳.
j = 2 改了四个位置2, 3, 4, 5,还行.
...

发现问题
注意到当j往上走的时候,要改的区间的个数是这个点的编号.
现在修改一次要修改O(N)个点.
一看就很不靠谱

妈耶我凉了啊

ST表不靠谱,那怎么办呢
稍微考虑一下,可能是我们选的区间不靠谱。(因为会重叠)
换一种区间选取方式也许会靠谱。

线段树
其实线段树被称为区间树比较合适,本质是一棵不会改变形态的二叉树.
树上的每个节点对应于一个区间[a, b](也称线段),a,b通常为整数
他是维护区间的信息

基本形态

同一层的节点所代表的区间,相互不会重叠
同一层节点所代表的区间,加起来是个连续的区间
对于每一个非叶结点所表示的结点[a,b],其左儿子表示的区间为[a,(a+b)/2],右儿子表示的区间为[(a+b)/2+1,b](除法去尾取整)
叶子节点表示的区间长度为1.

示例1:n=10

 

示例2:n=9

 

 

注意到线段树的结构有点像分治结构,深度也是O(log N)的.

每层里面区间长度相差不超过一

区间拆分
区间拆分是线段树的核心操作,我们可以将一个区间[a, b]拆分成若干个节点,使得这些节点代表的区间加起来是[a, b],并且相互之间不重叠.
所有我们找到的这些节点就是”终止节点”.

区间拆分的步骤
从根节点[1, n]开始,考虑当前节点是[L, R].
如果[L, R]在[a, b]之内,那么它就是一个终止节点.
否则,分别考虑[L, Mid],[Mid + 1, R]与[a, b]是否有交,递归两
边继续找终止节点

 

区间拆分的例子

 

 

注意:

有多少个底层的点,至少需要开四倍

解题方法

充分利用区间分解的性质.
思考在终止节点要存什么信息,如何快速维护这些信息,不要每次一变就到最底层.

例1

给一个数的序列A1, A2, ..., An.并且可能多次进行下列两个操作:
1.对序列里面的某个数进行加减
2.询问这个序列里面任意一个连续的子序列Ai, Ai+1...Aj的和是多少.
希望单个操作能在O(log N)的时间内完成.

题解:

对于每个节点[L, R],我们记录AL + ... + AR.
对于操作1:相当于我们对[i, i]这个区间做了一个区间分解.沿路我们在找到[i,i]时经过的所有祖先节点.
对于操作2:我们对[L, R]做一个区间分解,将每个区间对应的和累加起来就是想要知道的区间和.

 

伪代码:

#include
#include
#define N 100005#define ls (t<<1)#define rs ((t<<1)|1)#define mid ((l+r)>>1)int tree[N*4];//操作1 void modify(int ll,int rr,int c,int l,int r,int t)//将t修改为c,并且进行更新 { if (ll<=l&&r<=rr) { tree[t]=c; return; } if (ll<=mid) modify(ll,rr,c,l,mid,ls); if (rr>mid) modify(ll,rr,c,mid+1,r,rs); tree[t]=tree[ls]+tree[rs];}/*void modify(int ll,int c,int r,int t){ if(l==r) { tree[t]=c; return ; } if(ll<=mid) modify(ll,rr,c,l,mid,ls); else modity(ll, rr,c,mid+1,r,rs); tree[t]=tree[ls]+tree[rs];}*/int S;//操作2 void ask(int ll,int rr,int l,int r,int t)//[ll,rr]是要分解的区间 当前正分解到的节点编号是t,区间范围是[l,r] { if (ll<=l&&r<=rr) { S+=tree[t]; return; } if (ll<=mid) ask(ll,rr,l,mid,ls); if (rr>mid) ask(ll,rr,mid+1,r,rs);}int main(){ modify(l,l,c,1,n,1); //相当于是从1~n对[l,l]这个区间进行区间分解 S=0; ask(l,r,1,n,1);}

完整代码

#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#define N 300005#define M 8000005#define ls (t*2)#define rs (t*2+1)#define mid ((l+r)/2)#define mk make_pair#define pb push_back#define fi first#define se secondusing namespace std;int i,j,m,n,p,k,add[N*4],sum[N*4],a[N],ans,x,c,l,r;void build(int l,int r,int t){ if (l==r) sum[t]=a[l]; else { build(l,mid,ls); build(mid+1,r,rs); sum[t]=max(sum[ls],sum[rs]); //预先处理区间[l,r]的最大值 }}void modify(int x,int c,int l,int r,int t) //将a[x]修改为c,然后需要对所有包含x的区间进行更新 { if (l==r) sum[t]=c; //只有一个点的时候可以直接计算 else { if (l<=x&&x<=mid) modify(x,c,l,mid,ls); else modify(x,c,mid+1,r,rs); sum[t]=max(sum[ls],sum[rs]);//回溯的时候[l,mid],[mid+1,r]的答案已经算出,可以利用两个儿子进行更新 }}void ask(int ll,int rr,int l,int r,int t) //询问[ll,rr]这个区间的最大值,l,r,t表示的是当前线段树上位置代表的区间[l,r]和编号t { if (ll<=l&&r<=rr) ans=max(ans,sum[t]); //找到了一个完整被[ll,rr]区间包含的区间,直接把答案记进去 else { if (ll<=mid) ask(ll,rr,l,mid,ls); //如果和左儿子有交就往左儿子走 if (rr>mid) ask(ll,rr,mid+1,r,rs); //如果和右儿子有交就往右儿子走 }}int main(){ scanf("%d",&n); for (i=1;i<=n;++i) scanf("%d",&a[i]); build(1,n,1); modify(1,5,1,n,1); ask(1,5,1,n,1); }

[POJ 3264]Balanced Lineup

给定Q个数A1, ..., AQ,多次询问,每次求某一区间[L, R]中最大值和最小值的差.
Q ≤ 50000

Sol

改存每个区间中的最大最小值,然后更新的时候注意要从左儿子和右儿子处更新过来
(就是min和max)

转载于:https://www.cnblogs.com/lcezych/p/10788398.html

你可能感兴趣的文章
[转载]Trie树|字典树(字符串排序)
查看>>
Git学习系列 (一)
查看>>
【原】移动web页面使用字体的思考
查看>>
xp sp3安装IIS
查看>>
解决IE6浏览器下PNG图片无法正常显示的问题
查看>>
青蛙的约会 扩展欧几里得
查看>>
看了三遍,沉默了五天,人的一生真的很短暂!
查看>>
(转)接口自动化测试之http请求实践总结
查看>>
jQuery与Ext区别
查看>>
php 图片压缩
查看>>
HDU 1811 Rank of Tetris
查看>>
L - Subway - POJ 2502
查看>>
DS_Store 是什么文件
查看>>
浅析Java中的final关键字--转
查看>>
cocoaPods使用
查看>>
javascript中全屏滑动效果实现
查看>>
ABAP 实现内表自定义的F4功能
查看>>
Service
查看>>
BZOJ4542: [Hnoi2016]大数
查看>>
python2.7.3的安装
查看>>