数据结构的整理以及应用
栈(Stack)
栈(stack)又名堆栈,它是一种运算受限的线性表。限定仅在表尾进行插入和删除操作的线性表。
这一端被称为栈顶,相对地,把另一端称为栈底。
向一个栈插入新元素又称作进栈、入栈或压栈,它是把新元素放到栈顶元素的上面,使之成为新的栈顶元素;
从一个栈删除元素又称作出栈或退栈,它是把栈顶元素删除掉,使其相邻的元素成为新的栈顶元素。
栈的操作不是很多,不过它的功能性不容小觑
栈可以很好的处理表达式树,进制问题;
笛卡尔树的构建方式就是用到了栈:
for(register int i=1;i<=n;++i){
int k=top;
while(k&&a[S[k]]>a[i]) --k;
if(k) rs[S[k]]=i;
if(k<top) ls[i]=S[k+1];
S[++k]=i;
top=k;
}
tarjan的强连通分量缩点也是用到了栈:
inline void tarjan(x){
dfn[x]=low[x]=++tim;
s[++top]=x,b[x]=1;
for(register int i=head[x];i;i=nex[i]){
int u=to[i];
if(!dfn[u]){
tarjan(u);
low[x]=min(low[x],low[u]);
}else if(b[u]) low[x]=min(low[x],dfn[u]);
}
if(dfn[x]==low[x]){
++gg;
int t=-1;
while(t!=x){
t=s[top--];
id[t]=gg;
b[t]=0;
}
}
}
还有很多其他的应用,甚至高难都少不了其身影,所以,不要小看它。
队列(Queue)
队列是一种特殊的线性表,
特殊之处在于它只允许在表的前端(front)进行删除操作,而在表的后端(rear)进行插入操作,
和栈一样,队列是一种操作受限制的线性表。进行插入操作的端称为队尾,进行删除操作的端称为队头。
队列和栈一样属于比较基础的数据结构,正因为基础,所以才很多地方被运用:
在斜率优化中我们需要用到队列的性质
P3195 [HNOI2008]玩具装箱
for(register int i=1;i<=n;++i){
while(head<tail&&slope(q[head],q[head+1])<2*D[i])
++head;
f[i]=f[q[head]]+1LL*(D[i]-X[q[head]])*(D[i]-X[q[head]]);
//s[i]+i-1-L-s[j]-j
while(head<tail&&slope(q[tail-1],q[tail])>slope(q[tail-1],i))
--tail;
q[++tail]=i;
}
最短路中的Spfa也用到了队列
queue<int> Q;
memset(h,0x3f3f3f3f,sizeof h);
h[s]=0,vis[s]=1,Q.push(s);
while(!Q.empty()){
int t=Q.front();Q.pop();
vis[t]=0;
for(register int i=head[t];i;i=nex[i]){
int u=to[i];
if(h[u]>h[t]+cost[i]){
h[u]=h[t]+cost[i];
if(!vis[u]){
vis[u]=1;
Q.push(u);
++total[u];
if(total[u]==n+1)
return false;
}
}
}
}
return true;
同时如果题目中如果隐含了单调性,例如经典的滑动窗口问题,我们可以用队列来解决,甚至动态规划也可以用队列优化
P2827 [NOIP2016 提高组] 蚯蚓
P1440 求m区间内的最小值
合抱之木,生于毫末,九层之台,起于累土,千里之行,始于足下。尽管队列偏向于基础,不可忽视。
优先队列
顾名思义,作为一种队列,它的出队顺序和入队顺序无关;和优先级相关
优先队列具有队列的所有特性,包括队列的基本操作,只是在这基础上添加了内部的一个排序,它本质是一个堆实现的。(当然也可以不用堆来实现)
只不过现在实现优先队列一般不是手写堆,而是直接通过调用\(STL\)进行实现
优先级队列好处就是能够在插入一个元素的时候自动排序\(O(nlogn)\)
常用的最短路算法dijkstra堆优化就是利用了优先队列
inline void DJ(int s){
fill(d,d+n+1,2147483647);
priority_queue<P,vector<P>,greater<P> >Q;
Q.push(P{0,s});
d[s]=0;
while(!Q.empty()){
P t=Q.top();Q.pop();
int w=t.second;
if(t.first!=d[w])
continue;
for(register int i=head[w];~i;i=e[i].pre){
int v=e[i].to,how=e[i].cost;
if(d[v]>d[w]+how){
d[v]=d[w]+how;
Q.push(P(d[v],v));
}
}
}
}
同时优先队列也可以用于带悔贪心
P3620 [APIO/CTSC 2007] 数据备份
可合并堆
可合并堆又称左偏树,比普通的堆好打多了
inline int merge(int x,int y){
if(!x||!y)
return x+y;
if(v[x]==v[y]?id[x]>id[y]:v[x]>v[y]) swap(x,y);
rc[x]=merge(rc[x],y);
if(dist[lc[x]]<dist[rc[x]])
swap(lc[x],rc[x]);
dist[x]=dist[rc[x]]+1;
return x;
}
以上就是关键部分,
一些定义
\(外结点:左儿子或右儿子是空结点的结点。\)
\(距离:一个结点\)x\(的距离dist_x定义为其子树中与结点x最近的外结点x的距离。特别地,定义空结点的距离为-1.\)
\(左偏树的基本性质:\)
\(左偏树具有堆性质,即若其满足小根堆的性质,则对于每个结点x,有v_x\le v_{lc},v_x\le v_{rc}\)
\(左偏树具有 左偏性质 ,对于每个结点 x,有dist_{lc}\ge dist_{rc}\)
\(基本结论\)
\(结点x的距离dist_x=dist_{rc}+1\)
\(距离为n的左偏树至少有2^{n+1}-1个结点。此时该左偏树的形态是一棵满二叉树.\)
\(有 nn 的结点的左偏树的根节点的距离是 O(\log_2 n)\)
\(若以u,v为根的两个堆需要合并,由于已经维护好了左偏性质,所以只需要将v合并到u最右边的空节点,然后再递归维护左偏性质即可.\)
\(复杂度:很显然,在最坏情况下,由于堆的性质,当堆构成一颗完全树的时候 dis[1] 到达 logn,合并操作依然能够维持在 O(logn) ,是非常优秀的。\)
Trie树
在计算机科学中,\(trie\),又称前缀树或字典树,是一种有序树,用于保存关联数组,其中的键通常是字符串。与二叉查找树不同,键不是直接保存在节点中,而是由节点在树中的位置决定。
一个节点的所有子孙都有相同的前缀,也就是这个节点对应的字符串,而根节点对应空字符串。一般情况下,不是所有的节点都有对应的值,只有叶子节点和部分内部节点所对应的键才有相关的值。
\(trie\)中的键通常是字符串,但也可以是其它的结构。\(trie\)的算法可以很容易地修改为处理其它结构的有序序列,比如一串数字或者形状的排列。
基本性质
1,根节点不包含字符,除根节点意外每个节点只包含一个字符
2,从根节点到某一个节点,路径上经过的字符连接起来,为该节点对应的字符串。
3,每个节点的所有子节点包含的字符串不相同。
优点:
可以最大限度地减少无谓的字符串比较,故可以用于词频统计和大量字符串排序。
跟哈希表比较:
1,最坏情况时间复杂度比hash表好
2,没有冲突,除非一个key对应多个值(除key外的其他信息)
3,自带排序功能(类似Radix Sort),中序遍历trie可以得到排序。
缺点:
1、虽然不同单词共享前缀,但其实trie是一个以空间换时间的算法。其每一个字符都可能包含至多字符集大小数目的指针(不包含卫星数据)。
每个结点的子树的根节点的组织方式有几种。
跟哈希表比较:
<1>如果默认包含所有字符集,则查找速度快但浪费空间(特别是靠近树底部叶子)。
<2>如果用链接法(如左儿子右兄弟),则节省空间但查找需顺序(部分)遍历链表。
<3>alphabet reduction: 减少字符宽度以减少字母集个数。
<4>对字符集使用bitmap,再配合链接法。
2,如果数据存储在外部存储器等较慢位置,Trie会较hash速度慢(hash访问O(1)次外存,Trie访问O(树高))。
3,长的浮点数等会让链变得很长。可用bitwise trie改进。
实现起来也比较简便
inline void build(char str[]){
int l=strlen(str),p=1;
for(register int i=0;i<l;++i){
int ch=str[i]-'a';
if(t[p][ch]==0)
t[p][ch]=++tot;
p=t[p][ch];
}
endd[p]=1;
return;
}
线段树&树状数组
优劣比较
假设数组长度为n。线段树和树状数组的基本功能都是在某一满足结合律的操作(比如加法,乘法,最大值,最小值)下,O(logn)的时间复杂度内修改单个元素并且维护区间信息。
不同的是,树状数组只能维护前缀“操作和”(前缀和,前缀积,前缀最大最小),而线段树可以维护区间操作和。
但是某些操作是存在逆元的,这样就给人一种树状数组可以维护区间信息的错觉:
维护区间和,模质数意义下的区间乘积,区间xor和。
能这样做的本质是取右端点的前缀和,然后对左端点左边的前缀和的逆元做一次操作,所以树状数组的区间询问其实是在两次前缀和询问。
可以说树状数组能做的事情其实是线段树的一个子集,大多数情况下使用树状数组真的只是因为它好写并且常数小而已。
用途:
线段树:各种区间问题,优化\(dp\),优化建图
树状数组:区间问题
关于树状数组的理解:
完全理解树状数组的应用要理解透两点:
1.lowbit(i)是干什么用的。
2.c[i]是怎么保存状态的。
\(lowbit(x)=x&-x=x&(x^(x-1))\)
前者利用了负整数的补码特性,非常的巧妙,
后者用另外一种方式计算出了最低位,其思想跟补码的也很相似。
ST表
随着区间问题询问次数的增多,我们会发现\(O(logn)\)的单次询问复杂度已经不够
\((1)ST表(Sparse Table,稀疏表)是一种简单的数据结构,主要用来解决RMQ(Range Maximum/Minimum Query,区间最大/最小值查询)问题。它主要应用倍增的思想,可以实现 [公式] 预处理、 [公式] 查询。\)
\((2)RMQ 表示区间最大(最小)值。\)
\((3)可重复贡献问题是指对于运算,满足,则对应的区间询问就是一个可重复贡献问题。例如,最大值有,gcd 有,所以 RMQ 和区间 GCD 就是一个可重复贡献问题。像区间和就不具有这个性质。 即对f(a,a)=a;\)
实现起来也还好:
P3865 【模板】ST 表
求出每一次询问的区间内数字的最大值:
预处理
for(register int j=1;j<=21;++j)
for(register int i=1;i+(1<<j)-1<=N;++i)
M[i][j]=max(M[i][j-1],M[i+(1<<j-1)][j-1];
查询
inline int Q(int l,int r){
int k=log2(r-l+1);
return max(M[l][k]),M[r-(1<<k)+1][k];
}
ST表的用途的话个人感觉没有前面的那么广泛,不过是通过预处理优化问题的一把好手。
可持久化数据结构
\(可持久化是什么意思呢?就是说,对于我们某一个数据结构,我们要O(1)查询它的历史版本。\)
\(嗯,秘技:反复横跳.\)
\(1.每次修改操作把它放进一个栈里,要查询历史版本就弹栈修改回去。但是这样只能查询上一个历史版本。对于某一个远古版本就巨难查询,可能复杂度爆表,单次nlogn。\)
\(2.对于每一个节点,我们开一个set记录每一个节点在变化时的值,然后打个时间标记,就可以查询了。不过这种方法并没有什么人用,我也不知道为什么,可能是因为不那么优美。所以大家都去写下一种写法了。\)
\(3.我们发现其实一次修改,修改的点其实不超过logn次,所以我们每次修改其实可以复制节点,在复制的节点上修改一波。然后我们从不同时间的根进入就可以查询到不同时间的版本。似乎很难看懂。\)
就是在每次寻找到一个节点的时候开一个同样的节点
平衡树(Treap&Splay...)与动态树(LCT)
\(Splay是一种十分高级的平衡树,而与一般的set或者treap不同的是,它是将多次访问的节点尽可能的靠近根的位置,从而减少查询的递归调用次数,而实现这种平衡的方式就是旋转(rotate)了。\)
\(需要注意的是,为了确保Splay的复杂度,需要不停的进行Splay操作\)
\(Treap比BST多了一个优先级的设定。\)
\(Treap的节点是满足BST性质的。同时Treap的每一个节点还有一个优先级,而这些优先级又是满足堆性质的。\)
\(这就能让Treap的节点保持一种随机的状态,普通情况下不会被卡。\)
\(FHQ Treap能搞Treap,Splay都搞的东西,而且码量比Splay小了不知道多少(虽然文艺平衡树还是一般用Splay)\)
\(主要是合并和分裂两个操作,注意剖出来的子树就好了\)
\(LCT 维护虚链和实链的剖分,动态维护\)
\(在实链剖分的基础下,LCT有更多操作\)
\(1.查询、修改链上的信息(最值,总和等)\)
\(2.随意指定原树的根(即换根)\)
\(3.动态连边、删边\)
\(4.合并两棵树、分离一棵树(跟上面不是一毛一样吗)\)
\(5.动态维护连通性\)
\(在打LCT的时候记得用简便的写法,例如宏定义,这样方便又好调\)
巧用数据结构解决问题,是每一个成功的OIer所必须的能力!
(以上只是基础)

浙公网安备 33010602011771号