1. 示例:
下面是一个C语言中使用随机函数的例子:
上面这个例子每次运行得到的结果是一样的, 这是因为对于同样的种子(seed)而言, rand产生的序列是一样的, 所以如果想每次产生的结果不一样的话,需要给srand赋予不同的种子, 比如可以取当前时间作为种子。
2. 原理
实现srand和rand并不困难, 可以采用linear congruential generator(线性同余)的方法, 用下面的公式表示:
Ij+1 = aIj + c (mod m)
下面的是POSIX.1-2001给出的示范实现:
/* RAND_MAX assumed to be 32767 */
int myrand(void) {
next = next * 1103515245 + 12345;
return((unsigned)(next/65536) % 32768);
}
void mysrand(unsigned seed) {
next = seed;
}
3. VC9和GLIBC2.3的实现
VC9里面CRT代码中可以查到rand和srand的实现, 摘录录下:
void __cdecl srand (
unsigned int seed
)
{
_getptd()->_holdrand = (unsigned long)seed;
}
int __cdecl rand (
void
)
{
_ptiddata ptd = _getptd();
return( ((ptd->_holdrand = ptd->_holdrand * 214013L
+ 2531011L) >> 16) & 0x7fff );
}
可以看出VC里面rand函数返回的最大值是0x7fff,即32767, 由上面的代码可以理解为什么给定同样的seed, 得出的序列是一样的。
Glibc2.3里面关于rand和srand的实现要复杂很多, 采用的non-linear additive feedback random number generator, rand返回的最大值是2147483647 (2**31 -1), 下面是参考代码:
void
__srandom (x)
unsigned int x;
{
__libc_lock_lock (lock);
(void) __srandom_r (x, &unsafe_state);
__libc_lock_unlock (lock);
}
int
__srandom_r (seed, buf)
unsigned int seed;
struct random_data *buf;
{
int type;
int32_t *state;
long int i;
long int word;
int32_t *dst;
int kc;
if (buf == NULL)
goto fail;
type = buf->rand_type;
if ((unsigned int) type >= MAX_TYPES)
goto fail;
state = buf->state;
/* We must make sure the seed is not 0. Take arbitrarily 1 in this case. */
if (seed == 0)
seed = 1;
state[0] = seed;
if (type == TYPE_0)
goto done;
dst = state;
word = seed;
kc = buf->rand_deg;
for (i = 1; i < kc; ++i)
{
/* This does:
state[i] = (16807 * state[i - 1]) % 2147483647;
but avoids overflowing 31 bits. */
long int hi = word / 127773;
long int lo = word % 127773;
word = 16807 * lo - 2836 * hi;
if (word < 0)
word += 2147483647;
*++dst = word;
}
buf->fptr = &state[buf->rand_sep];
buf->rptr = &state[0];
kc *= 10;
while (--kc >= 0)
{
int32_t discard;
(void) __random_r (buf, &discard);
}
done:
return 0;
fail:
return -1;
}
int
rand ()
{
return (int) __random ();
}
long int
__random ()
{
int32_t retval;
__libc_lock_lock (lock);
(void) __random_r (&unsafe_state, &retval);
__libc_lock_unlock (lock);
return retval;
}
int
__random_r (buf, result)
struct random_data *buf;
int32_t *result;
{
int32_t *state;
if (buf == NULL || result == NULL)
goto fail;
state = buf->state;
if (buf->rand_type == TYPE_0)
{
int32_t val = state[0];
val = ((state[0] * 1103515245) + 12345) & 0x7fffffff;
state[0] = val;
*result = val;
}
else
{
int32_t *fptr = buf->fptr;
int32_t *rptr = buf->rptr;
int32_t *end_ptr = buf->end_ptr;
int32_t val;
val = *fptr += *rptr;
/* Chucking least random bit. */
*result = (val >> 1) & 0x7fffffff;
++fptr;
if (fptr >= end_ptr)
{
fptr = state;
++rptr;
}
else
{
++rptr;
if (rptr >= end_ptr)
rptr = state;
}
buf->fptr = fptr;
buf->rptr = rptr;
}
return 0;
fail:
__set_errno (EINVAL);
return -1;
}
在有些rand函数的实现里面,高位比低位的随机性更强, 所以如果要产生1-10之间的随机数,最好用:
j = 1 + (int) (10.0 * (rand() / (RAND_MAX + 1.0)));
而不要用:
j = 1 + (rand() % 10);
4. 多线程安全版本
rand不是多线程安全也不是可重入的,在多线程环境下,给定同样的种子,跑出来的序列可能是不一样的,因为实现里面用到了静态变量,在多线程环境下应该使用rand_r函数 (windows上使用rand_s), 下面是glibc的rand_r实现:
next *= 1103515245;
next += 12345;
result = (unsigned int) (next / 65536) % 2048;
next *= 1103515245;
next += 12345;
result <<= 10;
result ^= (unsigned int) (next / 65536) % 1024;
next *= 1103515245;
next += 12345;
result <<= 10;
result ^= (unsigned int) (next / 65536) % 1024;
*seed = next;
return result;
}
5. 其他考虑
游戏里面大量的应用到了随机函数,比如彩票,炼器功能,有意思的是玩家对这些随机时间会比较迷信,经常有各种搞笑的说话,比如在某个NPC下面炼器成功概率高啊, 炼器的时候骂人会提高成功率...
这里有个问题,玩家炼器经常采用垫装备的方法,这个方法简单而言就是先拿几个烂的装备等连续炼失败了几次以后再去上一个好装备,这样会不会提高最后那个好装备的成功概率呢?
个人觉得这个和实现相关,假如每次成功的概率都是50%, 如果前面N次序列都是小于50%,那么第N+1次概率是有可能受影响的,也即随机序列里面的元素并不是完全独立,序列之间是有关系,个人觉得通过这种方面在某种程度能提高成功的概率,但是还要考虑到会受游戏中别的玩家的干扰以及实现的时候可能每次使用不同的种子等的影响。
6. 推荐书籍
Numerical Recipes in C: The Art of Scientific Computing Second Edition: 第七章