洛谷【P6136】 手写的rand()函数和c++自带的rand()函数的区别

目录

一、题目传送门

二、问题代码

三、尝试与分析

四、后续

五、附录


  • 一、题目传送门

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()函数是很容易只取某些值而进入循环的。于是我对这两个函数进行了统计,下面是一些分析:

图一

         图一是模数取2147483647时的手写Rand(),运行1000万次,每1000个数的统计结果。从图中我们可以看出,Rand()的取值分布是相对不均匀的。又因为进入循环之后各个循环内的点的取到的次数之差应该在1。图中分布的次数在0,12,13,14,15,16,17,18,19,20。

图二

        图二是模数取32769时,手写Rand()运行1000万次的统计数据。很明显Rand()的结果已经进入了循环,循环的次数略高于30000次,保守估计0-32768有只300多个数字被取到了,不均匀。但选择了32769作为模数,却对treap没有影响(至少没被评测机发现)。

        这与treap的原理是相对应的。下面是查找到的外文文献,大意是:我们根的优先级是最大的,被放在了第一层。因为我们添加新点的时候,随机给了新点一个优先级,因而利用堆的性质排列的时候,我们的键值(key)也可以看作是随机的,于是根的键值有一半的概率在总数据范围的1/4~3/4。从期望的角度,每两层,树的规模就至少减少到了原先的3/4。因而总的树高是log(n)级的。

        聪明的你是不是发现Rand()循环与否,均匀与否好像对treap没什么影响。(我才不会告诉你我在这里纠结了一天)

        从这个角度分析,随机值似乎只要不同值的总数够大,能保证父子的优先级不一样就行了。

图三 treap指数级层数的保证
图四

        图四是将c++自带的rand()函数运行1000万次的结果写入文件,用python统计得到的统计数据。可以看出自带的rand()每个数值都能取到,并且次数上相差不是很多,相对均匀一些。

        所以问题大概出在手写的Rand()随机出来的值太有特征,导致树退化了吧。(不会吧不会吧,不会有人手写的Rand()被卡掉了吧

  • 四、后续

        一句话总结,rand()用自带的就好。看似白忙活了一天,但我加深了对treap的理解,也对rand()更加熟悉。在这条编程之路上,我一路向前。为别人填坑,同时也平了自己的道路。

        PS:那个晚上,我突发奇想,把Rand()函数改成了下面这个样子。把0抠掉了。然后AC了,甚至比题解还快。(QWQ!!!)然而我之前记录的数据本来就是没有0的为了确定我又搜集数据绘制了图像。可以看出确实没有零。玄学错误果然是迈不过去的坎。(哭)有大佬知道为什么的请务必在评论部分告诉我。提前谢谢了!

图五

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()

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值