利用一个产生随机数的函数生成另一个产生随机数的函数

数据结构与算法 专栏收录该内容
100 篇文章 0 订阅

        最近做了一些Tencent及几家公司的面试题,发现有一种关于产生随机数的类型的题目。看到多有大牛们做出来,而且效率很高,也有不知道怎么做的,最近根据几个产生随机数的题目整理一下,发现所有的类似题目可以用一种万能钥匙解决。故分享,欢迎发表不同看法,欢迎吐槽。

题目一:给定能随机生成整数1到5的函数,写出能随机生成整数1到7的函数。

        利用随机函数rand()函数生成一个等概率随机生成整数1到5的函数Rand5(),然后根据Rand5()生成Rand7(),代码如下:

#include <iostream>
using namespace std;

int Rand5()
{
	int n =1 + rand()%5;
	return n;
}

int Rand7()
{	
	int n ,tmp1 ,tmp2;	
	do 
	{
            tmp1 = Rand5();
            tmp2 = Rand5();
            n = (tmp1-1)*5+tmp2;//n是可以取1~25的随机的数。
	} while (n>21);//当n>21舍去,这样n只能取1~21,对7取模就能取1~7之间的随机数

	return 1+n%7;
}

int main()
{
	for (int i = 0 ; i < 100 ; i++)
	{
		cout<<Rand5()<<" ";
	}
	cout<<endl;
	for (int j = 0 ; j < 100 ; j++)
	{
		cout<<Rand7()<<" ";
	}
	cout<<endl;
	return 0;
}

        算法的关键就是两次运用Rand5(); tmp1 = Rand5();tmp2 = Rand5();n = (tmp1-1)*5+tmp2;n的最大值为25,为了满足产生的1到7等概率,所以n最大应该取7的倍数,所以当n>21时应舍去,为了测试是否概率真的相等,写一个测试函数:

int main()
{
	const int Max = 10000000;
	int a[7] = {0};
	for (int ii = 0 ; ii < Max ; ++ii)
	{
		switch (Rand7())
		{
		case 1:a[0]++;break;
		case 2:a[1]++;break;
		case 3:a[2]++;break;
		case 4:a[3]++;break;
		case 5:a[4]++;break;
		case 6:a[5]++;break;
		case 7:a[6]++;break;
		default:cerr<<"Error!"<<endl;exit(-1);
		}
	}
	for (int r = 0 ; r<7 ; r++)
	{
		cout<< r+1<<":"<<setw(6)<<setiosflags(ios::fixed)<<setprecision(2)<<double(a[r])/Max*100<<"%"<<endl;
	}
	return 0;
}

题目二:已知rand7() 可以产生 1~7 的7个数(均匀概率),利用rand7()  产生rand10()   1~10(均匀概率)

        解法与上面类似,同样只用两个rand7()生成rand10()即可。各位可以自己试试。另外,看见一个大牛的方法,似乎比以上更为简单,现贴出代码,供各位欣赏:

int rand10()
{
	int temp1;
	int temp2;

	do
	{
		temp1 = rand7();
	}while(temp1>5);

	do
	{
		temp2 = rand7();
	}while(temp2>2);

	return temp1+5*(temp2-1);
}

        temp1只取1到5,temp2只取1到2,即可等概率取到1到10。个人觉得两种方法有异曲同工之妙,所以大多数利用一个等概率随机数构造另外一个等概率随机数,只需两次使用概率函数即可。

该随机数生成更通用的题目如下:

        已知一随机发生器,产生0的概率是p,产生1的概率是1-p,现在要你构造一个发生器,使得它构造0和1的概率均为1/2;构造一个发生器,使得它构造1、2、3的概率均为1/3;...,构造一个发生器,使得它构造1、2、3、...n的概率均为1/n,要求复杂度最低。

  分析:

        在这里有三个小问题,但是都可以归结到n的情况。

(1)首先针对1/2的情况,我们可以产生两位随机数,如果是00或者11则丢弃,如果是01或者10则保留,他们的概率均为p*(1-p),因此两者概率相等为1/2。

(2)对于1/3的情况,同样可以产生三位随机数,保留100、010、001,舍弃其他的,他们的概率均为p*(1-p)^2。

(3)思维扩展一下,对于生成1,2,...n的概率分别是1/n,只需要每个数的概率相等即可。自然想到产生n位随机数,保留只有一位为1的组合,舍弃其余的。虽然这样可以产生1到n的随机数,但是效率明显十分低.假设产生一位0,1的随机数需一个单位的时间,那么产生n位随机数需要o(n)时间,期望循环次数E = 1/(n/2^n) = 2^n/n次,所以时间复杂度为o(2^n)。那么怎样才能降低时间复杂度呢?自然会想到选取1的位数不止1位,假设选取x位为1,总位数为y位,那么需要满足C(x,y) >= n.并且可以计算得到这些概率为p^x*(1-p)^y.注意到题目中需要复杂度最低,因此考虑到组合数中中间值取到最大C(x/2,x),所以只需要取最小的x使得C(x/2,x) >= n就能达到最小得复杂度。

        举个例子:n = 7,我们可以取到最小的x = 5,使得C(2,5) >= 7。这样共有10个组合他们的概率相等:00011,00101,00110,01001,01010,01100,10001,10010,10100,11000.

        舍弃后面三个组合,就可以得到概率相等的7个组合。

        接下来就是如何对其中一个组合赋值,比如:00011对应随机数1,01010对应随机数5.其实发现其中的规律我想了有点久,最后发现了一个规律,我举两个例子吧,比如找01010对应的数,可以找最高位的1所在的位置3,然后取组合数C(2,3),接着往低位找第二个1所在位置1,取组合数C(1,1),最后把这两个数加起来再加1.

        找11000对应的数,同样可以计算得到组合数:C(2,4),C(1,3),加起来为6+3+1 = 10,当然这个数大于n(7),所以被舍弃。

        在贴代码之前我再讲一个写代码过程碰到的一个很奇怪的问题,这个和我之前对Java中的Random这个类不是很熟悉有关。

        在我第一次写下面这个产生随机数0和1的方法时,每次都产生一个Random对象,导致了在循环中调用的时候产生的随机数并不随机,这个和Random使用的种子是当前系统的时间有关,可以想象在循环中如果多次调用,由于系统时间变化很小,所以产生的随机数不随机。这个问题让我检查了好几遍代码都发现不了问题,特此mark一下,大神们可以忽略,不过对于之前不怎么清楚的朋友,给个提醒吧。

public static int randomWithN(int n, Random random) {
		int x = 2, y = 1;
		while (combine(x, x / 2) < n) {
			x++;
		}
		y = x / 2;
		boolean[] a = new boolean[x];
		int count = 0;
		do {
			count = 0;
			for (int i = 0; i < a.length; i++) {
				a[i] = random0Or1(random);
				if (a[i]) {
					count++;
					if (count > y) {
						break;
					}
				}
			}
		} while (count != y);

		int sum = 0;
		for (int i = a.length - 1; i >= 0 && y != 0; i--) {
			if (a[i]) {
				sum += combine(i, y);
				y--;
			}
		}
		sum++;
		if (sum > n) {
			return randomWithN(n, random);
		}
		return sum;
	}

	public static int combine(int n, int r) {
		if (r > n) {
			return 0;
		}
		if (r > n / 2) {
			r = n - r;
		}
		double s = 0;
		for (int i = n - r + 1; i <= n; i++) {
			s += Math.log(i);
		}
		for (int i = 2; i <= r; i++) {
			s -= Math.log(i);
		}
		return (int) (Math.pow(Math.E, s) + 0.1);
	}

	public static boolean random0Or1(Random random) {
		double p = 0.1;
		if (random.nextDouble() <= p) {
			return false;
		} else {
			return true;
		}
	}

 

  • 1
    点赞
  • 0
    评论
  • 6
    收藏
  • 一键三连
    一键三连
  • 扫一扫,分享海报

©️2021 CSDN 皮肤主题: 大白 设计师:CSDN官方博客 返回首页
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值