2924: 经营额统计-伸展树-Splay-数据结构-模板详解
题目链接:http://acm.tzc.edu.cn/acmhome/problemdetail.do?method=showdetail&id=2924
Description Tiger最近被公司升任为营业部经理,他上任后接受公司交给的第一项任务便是统计并分析公司成立以来的营业情况。Tiger拿出了公司的账本,账本上记录了公司成立以来每天的营业额。分析营业情况是一项相当复杂的工作。由于节假日,大减价或者是其他情况的时候,营业额会出现一定的波动,当然一定的波动是能够接受的,但是在某些时候营业额突变得很高或是很低,这就证明公司此时的经营状况出现了问题。经济管理学上定义了一种最小波动值来衡量这种情况: 该天的最小波动值 = min{ |该天以前某天的营业额 - 该天的营业额 | } 当最小波动值越大时,就说明营业情况越不稳定。而分析整个公司的从成立到现在营业情况是否稳定,只需要把每一天的最小波动值加起来就可以了。你的任务就是编写一个程序帮助Tiger来计算这一个值。第一天的最小波动值为第一天的营业额。 Input 测试数据多组,每组的第一行为正整数n(1 <= n <= 32767), 表示该公司从成立一直到现在的天数. 接下来的n行每行有一个整数Ai(Ai <= 1000000) , 表示第i天的营业额。处理到EOF为止。 Output 每组数据占一行,每行输出一个整数,每天最小波动值的和。结果小于2^31 Sample Input 6 5 1 2 5 4 6 Sample Output 12 Hint 5+|1-5|+|2-1|+|5-5|+|4-5|+|6-5|=5+4+1+0+1+1=12
本题的关键是要每次读入一个数,并且在前面输入的数中找到一个与该数相差最小的一个。
用伸展树来做;这也是我第一次写伸展树,理解就花了我很长的时间;所以我需要把我理解的过程记录下来,详细的解释,一来整理我自己的思路,方便我日后用,二来我也希望能够帮到大家;
首先这道题目只用到了伸展树的三个基础操作:插入,求前驱,求后继;(查找,删除,求最大值,求最小值,合并,分离,这个题目暂时没有涉及到,我也还没有去好好领悟,暂时先不详解)
那么什么是伸展树呢????
伸展树(Splay Tree),也叫分裂树,是一种二叉排序树,它能在O(log n)内完成插入、查找和删除操作。它由Daniel Sleator和Robert Tarjan创造,后者对其进行了改进。它的优势在于不需要记录用于平衡树的冗余信息。在伸展树上的一般操作都基于伸展操作
二叉排序树:两个关键,二叉树,排序;
其实这就是将二叉搜索树进一步提升了一下;我们知道二叉搜索树数据量极端的时候会造成时间复杂度为O(n),比如数据 7 6 5 4 3 2 1,最后就成线性的了,很显然这样效率太低了;所以就有了伸展树;伸展树就是你每一次插入数据,我就将这个数据更新为根节点,而且左儿子一定小于根节点,右儿子一定大于根节点;而且最关键的是:满足任一左子树的深度和右子树的深度的差值不超过1;这就是插入,删除,查找等的复杂度都为O(logn);
首先,我们先说一下必备的建立新节点的操作;(变量:pre[N],key[N],ch[N][2],root,tot; 分别表示父节点,键值,左右孩子(0为左孩子,1为右孩子),更节点,节点数量;)
void New_Node(int &r,int father,int k) { r=++tot; // 每新建一个节点,tot加一,根节点更新为tot; pre[r]=father; key[r]=k; ch[r][0]=ch[r][1]=0; }
r为根节点,father为父节点,k为要插入的数;
首先,我们每插入一个数,将其移动至根节点;tot保存的是节点总数;所以根节点更新为节点总数+1,因为我们要新增一个节点;
然后见父节点保存在pre[r]内,并保存要插入的值key[r];同时将左右孩子标记为空
然后就是伸展树的核心了,怎么将插入的数移至为根节点;我们就要用到Splay操作和旋转;
Splay操作:伸展操作Splay(x,S)是在保持伸展树有序性的前提下,通过一系列旋转操作将伸展树S中的元素x调整至树的根部的操作。
其中在旋转的过程中,要分三种情况分别处理:
1)Zig 或 Zag
执行条件:节点x的父节点y是根节点。
2)Zig-Zig 或 Zag-Zag
执行条件: 节点x的父节点y不是根节点,且x与y同时是各自父节点的左孩子或者同时是各自父节点的右孩子。
3)Zig-Zag 或 Zag-Zig
执行条件:节点x的父节点y不是根节点,x与y中一个是其父节点的左孩子而另一个是其父节点的右孩子。
所以我们Splay要做的就是判断要做哪一种旋转,一直变换到符合属于我们Splay树的定义;
我们看一下代码:
void Splay(int r,int goal) //Splay调整,将根为r的子树调整为goal { while(pre[r]!=goal){ if(pre[pre[r]]==goal){ Rotate(r,ch[pre[r]][0]==r); // 旋转一次就可以了;(第一种情况) }else{ int y=pre[r]; int kind=ch[pre[y]][0]==y; if(ch[y][kind]==r){ // 两个方向不同,则先左旋再右旋(第三种情况) Rotate(r,!kind); Rotate(r,kind); }else{ // 两个方向相同,相同方向连续两次(第二种情况) Rotate(y,kind); Rotate(r,kind); } } } if(goal==0) root=r; }
这里父节点就是目标位置,goal为0表示,父节点就是根结点;分三种情况判断执行哪一种旋转方式;
一直到目标位置;如果找到目标位置就是根节点,那就更新root;
然后我们就来看一下怎么去实现旋转的代码;
//旋转,kind为0为右旋,kind为1为左旋 void Rotate(int x,int kind){ int y=pre[x]; //类似SBT,要把其中一个分支先给父节点 ch[y][!kind]=ch[x][kind]; pre[ch[x][kind]]=y; //如果父节点不是根结点,则要和父节点的父节点连接起来 if(pre[y]) ch[pre[y]][ch[pre[y]][1]==y]=x; pre[x]=pre[y]; ch[x][kind]=y; pre[y]=x; }
保存父节点,然后将父节点的旋转方向的(!kind)儿子节点更新为儿子节点的(kind)儿子节点;
判断是否需要合并;
再更新x的父节点为y的父节点;x的一个儿子变为y;y的父节点是x;
看完这些,接下来就是这个题目涉及的操作了;
第一个是插入操作:
int Insert(int k) { int r=root; while(ch[r][key[r]<k]){ // 没有必要重复插入; if(key[r]==k){ // 表示出现过; Splay(r,0); // 旋转到父节点; return 0; // 不执行插入操作; } r=ch[r][key[r]<k]; // 更新r节点,判断是否有出现过; } New_Node(ch[r][key[r]<k],r,k); // 插入,建立新的父节点; Splay(ch[r][key[r]<k],0); return 1; }
插入操作第一步就是判断是否之前插入过同样的数据,避免重复插入(完全没有必要去重复插入嘛)
如果说有出现过,也就是我们找到了键值和我们现在要插入的一样,那我们就直接执行Splay操作,将之前出现的这个节点旋转到父节点;
如果没有出现过,我们就创建一个新的节点,然后执行Splay操作,更改节点位置,保证符合Splay树的性质;
然后就是找先驱和后继的操作了;
//找前驱,即左子树的最右结点 int get_pre(int x){ int tmp=ch[x][0]; if(tmp==0) return inf; while(ch[tmp][1]) tmp=ch[tmp][1]; return key[x]-key[tmp]; } //找后继,即右子树的最左结点 int get_next(int x){ int tmp=ch[x][1]; if(tmp==0) return inf; while(ch[tmp][0]) tmp=ch[tmp][0]; return key[tmp]-key[x]; }
找先驱和后继就是找排序后的和它最近的比它小和比它大的数;最后返回的是它的差值;
这个比较好理解,我就不赘述了;
直接我AC的代码吧:
#include<iostream> #include<cstring> #include<cstdio> #include<algorithm> #define N 100005 #define inf 1<<29 using namespace std; int pre[N],key[N],ch[N][2],root,tot1; //分别表示父结点,键值,左右孩子(0为左孩子,1为右孩子),根结点,结点数量 int n; //新建一个结点 void NewNode(int &r,int father,int k){ r=++tot1; pre[r]=father; key[r]=k; ch[r][0]=ch[r][1]=0; //左右孩子为空 } //旋转,kind为1为右旋,kind为0为左旋 void Rotate(int x,int kind){ int y=pre[x]; //类似SBT,要把其中一个分支先给父节点 ch[y][!kind]=ch[x][kind]; pre[ch[x][kind]]=y; //如果父节点不是根结点,则要和父节点的父节点连接起来 if(pre[y]) ch[pre[y]][ch[pre[y]][1]==y]=x; pre[x]=pre[y]; ch[x][kind]=y; pre[y]=x; } //Splay调整,将根为r的子树调整为goal void Splay(int r,int goal){ while(pre[r]!=goal){ //父节点即是目标位置,goal为0表示,父节点就是根结点 if(pre[pre[r]]==goal) Rotate(r,ch[pre[r]][0]==r); else{ int y=pre[r]; int kind=ch[pre[y]][0]==y; //两个方向不同,则先左旋再右旋 if(ch[y][kind]==r){ Rotate(r,!kind); Rotate(r,kind); } //两个方向相同,相同方向连续两次 else{ Rotate(y,kind); Rotate(r,kind); } } } //更新根结点 if(goal==0) root=r; } int Insert(int k){ int r=root; while(ch[r][key[r]<k]){ //不重复插入 if(key[r]==k){ Splay(r,0); return 0; } r=ch[r][key[r]<k]; } NewNode(ch[r][k>key[r]],r,k); //将新插入的结点更新至根结点 Splay(ch[r][k>key[r]],0); return 1; } //找前驱,即左子树的最右结点 int get_pre(int x){ int tmp=ch[x][0]; if(tmp==0) return inf; while(ch[tmp][1]) tmp=ch[tmp][1]; return key[x]-key[tmp]; } //找后继,即右子树的最左结点 int get_next(int x){ int tmp=ch[x][1]; if(tmp==0) return inf; while(ch[tmp][0]) tmp=ch[tmp][0]; return key[tmp]-key[x]; } int main(){ while(scanf("%d",&n)!=EOF){ root=tot1=0; int ans=0; for(int i=1;i<=n;i++){ int num; scanf("%d",&num); if(i==1){ ans+=num; NewNode(root,0,num); continue; } if(Insert(num)==0) continue; int a=get_next(root); int b=get_pre(root); ans+=min(a,b); } printf("%d\n",ans); } return 0; }
总结:呼呼,第一次写这么长的博客。。。很好的整理了我自己的思路,也希望对大家有帮助;^_^....
版权声明:本文为博主原创文章,未经博主允许不得转载。