[HNOI2002]营业额统计_Treap
[HNOI2002]营业额统计
题目大意:给你一串n数序列,对于每一个刚输入的数a,找到一个前面的数k,使得|a-k|最小。
注释:$n<=32767,ai<=10^6$。
想法:刚学Treap。这道算Treap的练习题里吧,对于新手来讲还是挺有意义的。首先,我们先来讲一讲Treap是个什么东西。
在这之前,我们声明:
1 2 3 4 5 6 | struct Node { int lson,rson; //lson和rson分别表示左右儿子的编号 int size,num; //size表示以当前节点为根节点的子树的节点数,num表示在Treap中有多少是和当前权值重复的 int val,rnd; //val表示Tree的权值,rnd表示Heap的随机数据 } |
1 2 3 4 | void push( int k) //push代表将子树的信息更新至根节点 { a[k].size=a[k].num+a[a[k].lson].size+a[a[k].rson].size; } |
Treap,是二叉搜索树(Tree)和堆(Heap)的合体,读音也是这两个东西捏在一起的(懵-)。我们来看一下这东西是怎么实现的。
Treap是一种二叉搜索树,但也是一种*衡树。我们显然知道,对于二叉搜索树来讲,如果插入的顺序是直接升序或者降序,二叉搜索树就会退化成一条链,查找和修改还有删除的时间复杂度都会从原本的期望logn滚粗会O(n)。这就很蛋疼,而且卡二叉搜索树的题有不少,那么我们可以用一种什么样的办法来使得二叉搜索树趋*于*衡的呢?这是一个问题。然后我们再来看另一个问题。
关于Heap来讲,升序或者降序对于堆的伤害显然是... ...没有的。我们尝试用Heap将二叉搜索树进行一定的改造。之前的二叉搜索树的节点维护的只是权值,现在,我们让它在多一个随机的rand。这样的好处是什么?就是可以用这个随机的rand来使得存在一个树,满足这个树对于原本二叉搜索树的权值来讲是满足二叉搜索树的,对于rand来讲是满足heap的(这里是大根堆还是小根堆是无所谓的)。如何来呈现一颗这样的树呢?我们通过旋转Treap的核心操作——旋转。如何进行旋转?首先,我们最主要的目的就在保证二叉搜索树的前提下完成对堆的构建。那么,我们来考虑在如何不改变原有二叉搜索树的前提下旋转堆。首先,二叉搜索树的大小排序是由中序遍历决定的,而堆的大小排序是由深度决定的。我们可以通过这样的操作:
1 2 3 4 5 6 7 | void lturn( int &k) //向左旋转 { int t=a[k].rson; a[k].rson=a[t].lson; a[t].lson=k; push(k);push(t);k=t; } |
没错,就是这样可以达到我们想要的效果。至于图解...菜逼的我并不会怎么把图粘上来,所以自己在纸上推演即可。然后,附上一些版子,知道了Treap的旋转之后就很好理解了。说一说插入,在插入节点时,先递归处理插入符合二叉搜索树的权值,然后取rand,通过递归旋转达到同时满足Tree和Heap的神奇的树即可。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | void update( int &k, int temp) { if (!k) { k=++tot; a[k].num=a[k].size=1; a[k].val=temp; a[k].rnd= rand (); return ; } a[k].size++; if (temp==a[k].val) a[k].num++; else if (temp<a[k].val) { update(a[k].lson,temp); if (a[a[k].lson].rnd<a[k].rnd) rturn(k); } else { update(a[k].rson,temp); if (a[a[k].rson].rnd<a[k].rnd) lturn(k); } } |
删除和插入的原理是大致相同的,不在赘述。向说一说找前驱和后继的事情。我是照着lijinnn的版子敲的,所以找前驱和后继是递归的,EdwardFrog说可以不递归,推荐看一下,在此贴递归版子前驱
1 2 3 4 5 6 7 8 9 10 | void ask_before( int k, int temp) { if (!k) return ; if (temp>a[k].val) { befor=max(befor,a[k].val); ask_before(a[k].rson,temp); } else ask_before(a[k].lson,temp); } |
然后,说一下这道题。由于求的是最小波动,我们考虑什么样的数在Treap上和当前输入的数的波动是最小的?显然,是前驱或者是后继即可。所以,我们只需要找到对应的前驱、后继,然后对于波动的绝对值取min即可。
最后,附上丑陋的代码... ...
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 | #include <iostream> #include <cstdio> #include <algorithm>//rand的时候会用到 using namespace std; int befor,aftr; int tot; int root; // int v[1000010]; int AbS( int x) //建议手写abs { if (x<0) return -x; else return x; } struct Node { int size; int lson,rson; int num,val,rnd; }a[50010]; void push( int k) //更新节点信息 { a[k].size=a[k].num+a[a[k].lson].size+a[a[k].rson].size; } void lturn( int &k) //向左旋转 { int t=a[k].rson; a[k].rson=a[t].lson; a[t].lson=k; push(k);push(t);k=t; } void rturn( int &k) //向右旋转 { int t=a[k].lson; a[k].lson=a[t].rson; a[t].rson=k; push(k);push(t);k=t; } void update( int &k, int temp) //在以k为根节点的树内插入一个权值为temp的节点 { if (!k) //如果已经到达了叶子结点,就新增节点 { k=++tot; a[k].num=a[k].size=1; a[k].val=temp; a[k].rnd= rand (); } a[k].size++; //显然,在以k为根节点的子树中添加节点,k的size必须+1 if (temp==a[k].val) a[k].num++; else if (temp<a[k].val) //左右递归处理即可 { update(a[k].lson,temp); if (a[a[k].lson].rnd<a[k].rnd) rturn(k); } else { update(a[k].rson,temp); if (a[a[k].rson].rnd<a[k].rnd) lturn(k); } } // int ask_rank(int k,int temp)//只是一个版子 // { // if(!k) return 0; // if(a[k].val==temp) return a[a[k].lson].size+1; // else if(temp>a[k].val) // return a[a[k].lson].size+a[k].num+ask_rank(a[k].rson,temp); // else return ask_rank(a[k].lson,temp); // } void ask_before( int k, int temp) //前驱 { if (!k) return ; if (temp>=a[k].val) { befor=max(befor,a[k].val); ask_before(a[k].rson,temp); } else ask_before(a[k].lson,temp); } void ask_after( int k, int temp) //后继 { if (!k) return ; if (temp<=a[k].val) { aftr=min(aftr,a[k].val); ask_after(a[k].lson,temp); } else ask_after(a[k].rson,temp); } int main() { int n; scanf ( "%d" ,&n); int All=0,ans; int x; scanf ( "%d" ,&x); All=x; update(root,x); // printf("root:%d\n",root); for ( int i=2;i<=n;i++) { scanf ( "%d" ,&x); ans=0x7f7f7f7f; befor=0xefefefef;aftr=0x7f7f7f7f; //防止在取前驱的时候使得没有比当前节点大或小时befor和aftr为0 ask_after(root,x); ask_before(root,x); ans=min(ans,AbS(befor-x)); ans=min(ans,AbS(aftr-x)); // printf("befor:%d aftr:%d ans:%d\n",befor,aftr,ans); All+=ans; update(root,x); } printf ( "%d\n" ,All); } |
小结:学到了一个超级强大的数据结构啊!!!。
错误:1.之前的做法是取rank,我们要明白,节点的编号对于Treap来讲,只有在对整个Treap进行修改时才有用,对于使用Treap来讲用处是极其小的。
2.update的!k并不是return,我们要明白update的最核心的一句话其实是第一句。只有在!k的时候才有可能对于整个Treap进行形式上的增加节点的操作(有可能之前有重复)。
| 欢迎来原网站坐坐! >原文链接<
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步