栈
栈。。。好吧,不知道的自己去查。
单调栈
功能简单暴力:找到从左或右遍历第一个比它大或者小的元素位置。
维护单调性:每次插入元素时判断栈顶元素与该元素是否满足单调性,满足则直接插入该元素,否则弹出栈顶元素,继续判断。注意判断栈是否为空。
习题:
HDU 1506
HDU 5033
PKU 2796
PKU 3250
队列
同上,不解释基础内容。
单调队列
不同於单调栈,它求解的是前面一段区间的最值,而最值是队首元素。
注:单调队列或是单调栈都可以优化dp。
习题:
Luogu 1725 琪露诺
Luogu 3088 挤奶牛
Luogu 1714 切蛋糕
Luogu 2629 好消息,坏消息
Luogu 2422 良好的感觉
SCOI2009 生日礼物
Luogu 2698 花盆
POI2011 Temperature
堆(优先队列)
直接用c++中的queue包就可以了。。。就是一个数据结构,堆顶元素是其中所有元素的最值,复杂度
应用:
最短路地界斯特拉的堆优化。。。
有的题要开一个大根堆和小根堆,在贪心算法中有用,每次维护当前元素大小在两堆堆顶元素之间即可,可以保证每次选择都是最优的。
二叉排序树
定义:这棵树满足对于所有节点均满足它的左子树的所有元素小于该节点的值且右子树的所有元素大于该节点的值。但是它有一个非常不好的地方:它很容易变成一条链,二叉排序树的复杂度就变成了 就需要想办法把它左右子树变得差不多重,将复杂度变成 ,即维护二叉排序树的均衡。
方法1:splay
它是将一个节点通过左右旋将其变为该树的根的操作,可以实现子树的删除,区间翻转,插入,删除元素等操作。由于不断进行splay,它的平摊复杂度为
所以它很容易被卡数据。splay()复杂度为 ,但是常数大的不行。
#define geng(x) a[x].sum=a[a[x].l].sum+a[a[x].r].sum+a[x].w;a[x].size=a[a[x].l].size+a[a[x].r].size+1;a[x].maxx=max(a[x].w,max(a[a[x].l].maxx,a[a[x].r].maxx));
#define mark(x) if(a[x].lazy)a[x].lazy=false;else a[x].lazy=true;
void put(LL x){
if(x==0)return;a[x].lazy=false;
swap(a[x].l,a[x].r);
mark(a[x].l)mark(a[x].r)
}//区间翻转打懒标记
void you(LL x){
LL y=a[x].fa,z=a[y].fa;
if(a[z].l==y)a[z].l=x;else a[z].r=x;
a[x].fa=z;
a[y].l=a[x].r;
a[a[x].r].fa=y;
a[x].r=y;
a[y].fa=x;
geng(y);geng(x);
}//右旋
void zuo(LL x){
LL y=a[x].fa,z=a[y].fa;
if(a[z].l==y)a[z].l=x;else a[z].r=x;
a[x].fa=z;
a[y].r=a[x].l;
a[a[x].l].fa=y;
a[x].l=y;
a[y].fa=x;
geng(y);geng(x);
}//左旋
void splay(LL x){
LL y,z;
while(a[x].fa){
y=a[x].fa;z=a[y].fa;
if(a[z].lazy)put(z);
if(a[y].lazy)put(y);
if(a[x].lazy)put(x);
if(z){
if(a[z].l==y){
if(a[y].l==x){you(y);you(x);}else {zuo(x);you(x);}
}
else {
if(a[y].r==x){zuo(y);zuo(x);}else {you(x);zuo(x);};
}
}
else {
if(a[y].l==x)you(x);else zuo(x);
}
}
geng(root);
root=x;
}//splay操作
void insert(LL x){
if(cnt==0){
a[++cnt].w=x;
a[cnt].fa=0;
a[cnt].maxx=x;
a[cnt].sum=a[cnt].w;
a[cnt].size=1;
root=cnt;
return;
}
cnt++;LL p;
a[cnt-1].r=cnt;a[cnt].w=x;a[cnt].fa=cnt-1;a[cnt].sum=x;a[cnt].maxx=x;a[cnt].size=1;
splay(cnt);
}//插入操作
LL cha(LL k){
LL p=root,sum=0;
while(p){
if(a[p].lazy)put(p);
if(a[a[p].l].size+1==k)break;
else if(a[p].l&&a[a[p].l].size+1>k)p=a[p].l;
else {
k-=a[a[p].l].size+1;
p=a[p].r;
}
}
return p;
}//查询第k大的元素
void s(LL x,LL y){
x=cha(x);y=cha(y);
splay(x);
root=a[x].r;
a[a[x].r].fa=0;
a[x].r=0;
splay(y);
a[root].fa=x;
a[x].r=root;
root=x;
}//将 [x,y]区间翻转在一起。
嗯,编程复杂度太大了。。老板还要求10分钟内必须打出来
法2:替罪羊树
个人认为是三种方法中最为暴力的方法,但是很保险。根本没有所谓的左右旋操作,暴力维护原树的平衡。只支持查询,插入单点,删除单点操作。
我们定义一个平衡常数 ,我们要维护对于所有点,均满足:
为当前点。如此保证复杂度,自然有暴力的维护方法:
插入:插入后,从该点向上深搜,找到深度最小不满足平衡的点,将该点的子树暴力变成平衡二叉树。
其他操作都差不多了。
法3:treap(tree+heap)
想优化也是想疯了。。玄学拼欧气
但是确实比splay简单多了,而且常数也小,只需要左右旋操作即可。
对于每个点,和二叉排序树不同的是,它每个节点有一个随机数,记为 .
heap的性质来了:对于每棵子树来说,它的根节点的 值是该子树的最值,满足堆的性质。每次维护堆的时候不能破坏原二叉排序树的顺序,即只能使用左右旋操作维护。
这样我也不知道为啥就维护了二叉树的平衡。。。。他们说很管用。。。。
顺便介绍一个和可持续化的树状结构
分块:
优美的暴力,运用均值不等式来使时间复杂度下降。但是有的题必须使用它。
直白点来说,将所有元素分为很多块,对于修改操作,维护每块之间的关系,然后暴力修改剩下在块外的元素;对于查询操作,直接运用块与块之间维护的关系,然后暴力查询块外元素即可。
倍增RMQ与分治
分治如此简单就。。。。
倍增有点像dp,实际上是 预处理, 查询。树上倍增找公共祖先有用。
线段树与树状数组
线段树->戳传送门
树状数组是单点修改,区间查询的数据结构,查询和修改都是 ,比线段树的常数小。可以差分变为区间修改,单点查询。常常用来求逆序对。