目录
-
一、题目传送门
P6136 【模板】普通平衡树(数据加强版) - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)
https://www.luogu.com.cn/problem/P6136
-
二、问题代码
今天写了洛谷P6136。基本是一道平衡树的板子题,但有两个数据点一直超时。下面是我没通过的代码:
#include <bits/stdc++.h>
#define ull unsigned long long
#define maxn 1100005
#define INF 2147483647
#define ls tr[p].son[0]
#define rs tr[p].son[1]
using namespace std;
int n, m;
int sum = 0, R = 0;
struct treap{
int size, num, v, rd;
int son[2];
}tr[maxn];
int read(){
char ch = getchar();
int s = 0, f = 1;
while(ch < '0' || ch > '9'){
if(ch == '-')
f = -1;
ch = getchar();
}
while(ch >= '0' && ch <= '9'){
s = s*10 + (int)(ch - '0');
ch = getchar();
}
return s*f;
}
int Rand(){
ull a = 233;
a = a*23333%2147483647;
return a;
}
void pushup(int p){
tr[p].size = tr[ls].size + tr[rs].size + tr[p].num;
return ;
}
void rotate(int &p, int d){
int k = tr[p].son[d^1];
tr[p].son[d^1] = tr[k].son[d];
tr[k].son[d] = p;
pushup(p);
pushup(k);
p = k;
return ;
}
int New(int x){
tr[++sum].v = x;
tr[sum].rd = rand();
tr[sum].size = 1;
tr[sum].num = 1;
return sum;
}
void ins(int &p, int x){
if(!p){
p = New(x);
return ;
}
if(tr[p].v == x){
tr[p].size++;
tr[p].num++;
return ;
}
int d = (x > tr[p].v);
ins(tr[p].son[d], x);
if(tr[tr[p].son[d]].rd > tr[p].rd)
rotate(p, d^1);
pushup(p);
return ;
}
void del(int &p, int x){
if(!p)
return ;
if(x < tr[p].v){
del(ls, x);
}
else if(x == tr[p].v){
if(!ls && !rs){
tr[p].size--;
tr[p].num--;
if(tr[p].num == 0)
p = 0;
}
else if(ls && !rs){
rotate(p, 1);
del(rs, x);
}
else if(!ls && rs){
rotate(p, 0);
del(ls, x);
}
else if(ls && rs){
int d = (tr[ls].rd > tr[rs].rd);
rotate(p, d);
del(tr[p].son[d], x);
}
}
else if(x > tr[p].v){
del(rs, x);
}
pushup(p);
return ;
}
int Rank(int p, int x){
if(!p)
return 1;
if(x < tr[p].v)
return Rank(ls, x);
else if(x == tr[p].v)
return tr[ls].size + 1;
else if(x > tr[p].v)
return tr[ls].size + tr[p].num + Rank(rs, x);
}
int Find(int p, int x){
if(!p)
return 0;
if(x <= tr[ls].size)
return Find(ls, x);
else if(x <= tr[ls].size + tr[p].num)
return tr[p].v;
else
return Find(rs, x - tr[ls].size - tr[p].num);
}
int pre(int p, int x){
if(!p)
return -INF;
if(x <= tr[p].v)
return pre(ls, x);
else
return max(tr[p].v, pre(rs, x));
}
int suc(int p, int x){
if(!p)
return INF;
if(x >= tr[p].v)
return suc(rs, x);
else
return min(tr[p].v, suc(ls, x));
}
int main(){
int x, opt;
int last = 0, ans = 0;
n = read();
m = read();
for(int i = 1; i <= n; i++){
x = read();
ins(R, x);
}
for(int i = 1; i <= m; i++){
opt = read();
x = read();
x ^= last;
if(opt == 1){
ins(R, x);
}
else if(opt == 2){
del(R, x);
}
else if(opt == 3){
last = Rank(R, x);
ans ^= last;
}
else if(opt == 4){
last = Find(R, x);
ans ^= last;
}
else if(opt == 5){
last = pre(R, x);
ans ^= last;
}
else if(opt == 6){
last = suc(R, x);
ans ^= last;
}
}
cout<<ans;
return 0;
}
经过了反复(对比题解)检查和不懈的尝试,我把问题锁定在了自己写的Rand()函数上。将其换为c++自带的rand()函数或者将模数改为32679时,代码便能AC。明明是为了优化常数手写的Rand(),结果导致了超时。奇技淫巧害人不浅啊。
int Rand(){
ull a = 233;
a = a*23333%2147483647;//改为 a = a*23333%32769;
return a;
}
-
三、尝试与分析
询问过学长,c++自带的rand()函数在区间上是相对均匀的。从直觉上来看自己写的rand()函数是很容易只取某些值而进入循环的。于是我对这两个函数进行了统计,下面是一些分析:
![](https://img-blog.csdnimg.cn/91bd1c01706b45b19867eb705b1ff520.png?x-oss-process=image/watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBA5peg56m35bCP6I-c6bih,size_19,color_FFFFFF,t_70,g_se,x_16)
图一是模数取2147483647时的手写Rand(),运行1000万次,每1000个数的统计结果。从图中我们可以看出,Rand()的取值分布是相对不均匀的。又因为进入循环之后各个循环内的点的取到的次数之差应该在1。图中分布的次数在0,12,13,14,15,16,17,18,19,20。
![](https://img-blog.csdnimg.cn/a5433a1c29e3496ab43452e3f4605684.png?x-oss-process=image/watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBA5peg56m35bCP6I-c6bih,size_19,color_FFFFFF,t_70,g_se,x_16)
图二是模数取32769时,手写Rand()运行1000万次的统计数据。很明显Rand()的结果已经进入了循环,循环的次数略高于30000次,保守估计0-32768有只300多个数字被取到了,不均匀。但选择了32769作为模数,却对treap没有影响(至少没被评测机发现)。
这与treap的原理是相对应的。下面是查找到的外文文献,大意是:我们根的优先级是最大的,被放在了第一层。因为我们添加新点的时候,随机给了新点一个优先级,因而利用堆的性质排列的时候,我们的键值(key)也可以看作是随机的,于是根的键值有一半的概率在总数据范围的1/4~3/4。从期望的角度,每两层,树的规模就至少减少到了原先的3/4。因而总的树高是log(n)级的。
聪明的你是不是发现Rand()循环与否,均匀与否好像对treap没什么影响。(我才不会告诉你我在这里纠结了一天)
从这个角度分析,随机值似乎只要不同值的总数够大,能保证父子的优先级不一样就行了。
![](https://img-blog.csdnimg.cn/08501fb804494798ab91d762a73235e5.png?x-oss-process=image/watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBA5peg56m35bCP6I-c6bih,size_20,color_FFFFFF,t_70,g_se,x_16)
![](https://img-blog.csdnimg.cn/bbe0c87cf5554992be8dbaddf6bb442b.png?x-oss-process=image/watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBA5peg56m35bCP6I-c6bih,size_19,color_FFFFFF,t_70,g_se,x_16)
图四是将c++自带的rand()函数运行1000万次的结果写入文件,用python统计得到的统计数据。可以看出自带的rand()每个数值都能取到,并且次数上相差不是很多,相对均匀一些。
所以问题大概出在手写的Rand()随机出来的值太有特征,导致树退化了吧。(不会吧不会吧,不会有人手写的Rand()被卡掉了吧)
-
四、后续
一句话总结,rand()用自带的就好。看似白忙活了一天,但我加深了对treap的理解,也对rand()更加熟悉。在这条编程之路上,我一路向前。为别人填坑,同时也平了自己的道路。
PS:那个晚上,我突发奇想,把Rand()函数改成了下面这个样子。把0抠掉了。然后AC了,甚至比题解还快。(QWQ!!!)然而我之前记录的数据本来就是没有0的为了确定我又搜集数据绘制了图像。可以看出确实没有零。玄学错误果然是迈不过去的坎。(哭)有大佬知道为什么的请务必在评论部分告诉我。提前谢谢了!
![](https://img-blog.csdnimg.cn/3c4f14319e7b48f88d4d3b6836eaeedd.png?x-oss-process=image/watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBA5peg56m35bCP6I-c6bih,size_19,color_FFFFFF,t_70,g_se,x_16)
int Rand(){
ull a = 233;
a = (a*23333 - 1)%2147483647 + 1;
return a;
}
-
五、附录
一段不重要的代码:
# 这是用python运行自己写的rand(),然后绘制图像的代码
import numpy as np
import matplotlib.pyplot as plt
y = np.empty((214748364))
x = np.empty((30000))
y1 = np.empty((30000))
a = 233
def Rand():
global a
a = a * 23333 % 2147483647
return
for i in range(10000000):
if i%10000 == 0:
print(int(i/1000)/100, '%')
Rand()
if a < 100000000:
y[int(a%100000000)-1] = y[int(a%100000000)-1]+1
for i in range(30000): # 绘制前0-30000个点
y1[i] = y[i]
x[i] = i
plt.plot(x, y1)
plt.show()