9.1.2 随机问题的建模与模拟

现实中有许多不确定的事件,称为随机事件。例如抛一枚硬币,结果是正面朝上还是反

面朝上,这是不确定的。研究随机事件的数学方法是统计,例如经过大量统计试验可以得出 结论:抛硬币时正面朝上和反面朝上的可能性是相等的,各占 50%。注意,说硬币正面朝 上和反面朝上的可能性各占 50%,并不意味着抛硬币试验将得到“正,反,正,反,…” 的结果,完全有可能出现一连串的正面或一连串的反面。

既然现实问题中存在随机事件,用计算机解决这类问题时就需要为随机事件建模,即程 序能够模拟随机事件的发生。例如,假设程序 P 能够模拟抛硬币并显示每次抛硬币的结果(“正”或“反”),则 P 应该具有这样的特性:每一次显示的结果是不可预测的,但多次运 行 P 之后“正”、“反”出现的次数应该是差不多的。可以设想 P 中有这样的语句:

  1. if 模拟抛硬币的结果是正面:
  2. print "正"
  3. else:
  4. print "反"

这里 if 后面的条件必须有时为真有时为假,但无法预测每一次运行时的真假;而多次运行 后,条件为真为假的次数应该基本相等。

我们知道,计算机是确定性的机器,它总是按照预定的指令行事。对于一个程序,只要 程序的初始输入是一样的,那么程序的运行结果就是确定的、可预测的。就拿程序 9.1 来说,尽管它产生了看上去不可预测的数值序列,但那并非真正的随机,因为按照程序 9.1 中的数 学式去计算,从相同的 x 开始必能得到完全一样的数值序列。所以,程序 9.1 所用的数学式 不能用于模拟随机事件,我们需要更好的能产生随机数的方法。

随机数生成函数

计算机科学家对随机数生成问题有很成熟的研究,他们设计了一些数学公式,通过计算

这些公式就能得到随机数。不过,既然是用确定的数学公式计算出来的数值,那就不可能是 数学意义上的随机,因此准确的名称实际上是“伪随机数”。伪随机数在实际应用中完全可 以当作真随机数来使用,因为它具有真随机数的统计分布特性,简单地说就是“看上去”是 随机的。

Python 语言提供了一个标准库模块 random,该模块中定义了若干种伪随机数生成函数。 这些伪随机数生成函数的计算与模块导入的时间(精度非常高)有关,因此每次运行函数所 产生的数值是不可预测的。random 模块中用得最多的随机数生成函数是 randrange 和 random。

randrange 函数生成一个给定范围内的整型伪随机数,范围由 randrange 的参数指定,具 体格式和 range 函数一样。例如,randrange(1,3)随机地产生 1 或 2,randrange(1,7)返回范围 [1,2,3,4,5,6]中的某个数值,而 randrange(2,100,2)返回小于 100 的某个偶数。例如:

  1. >>> from random import randrange
  2. >>> for i in range(20):
  3. print randrange(1,3),
  4. 1 2 2 2 1 1 2 2 2 1 1 2 1 2 2 1 1 1 1 1
  5. >>> for i in range(20):
  6. print randrange(1,7),
  7. 1 5 1 5 2 3 2 2 3 2 4 2 6 3 1 2 1 1 1 5

注意,由于试验次数较少(20 次),randrange 所生成的数值并未如我们期望的那样均匀分布, 但随着试验次数的增加,会发现 randrange 产生的值都具有差不多的出现次数。

random 函数可用来生成浮点型伪随机数,确切地说,该函数生成[0,1)区间中的浮点数。

random 不需要提供参数。例如:

  1. >>> from random import random
  2. >>> for i in range(5):
  3. print random()
  4. 0.35382204835
  5. 0.997559174002
  6. 0.672268961152
  7. 0.889307826404
  8. 0.246100521527

注意,random 模块名与 random 函数名恰巧相同,不要因此而误用。

模拟

有了随机数生成函数,我们就可以来模拟随机事件了。以抛硬币问题为例,前面我们给 出了如下形式的代码:

  1. if 模拟抛硬币的结果是正面:
  2. print "正"
  3. else:
  4. print "反"

并且指出 if 后面的条件的真假应该是不可预测、均匀分布的。

考虑 randrange(1,3):该函数产生随机数 1 或 2,每一次调用到底生成什么值是不可预测 的,并且大量调用后两个数值出现的机会是一样的。据此,randrange(1,3) == 1 正是我们所 需要的条件,此条件每一次计算时的真假是随机的,但长远来看真假情形各占 50%。将这 个条件代入上面的条件语句,即得

  1. if randrange(1,3) == 1:
  2. print "正"
  3. else:
  4. print "反"

这样,我们就通过调用合适的随机数生成函数的方式模拟了随机事件,这种模拟方法称为蒙特卡洛方法。 类似地,掷骰子也是现实中常见的随机问题,如果希望在程序中模拟掷骰子,可以这样做:

  1. value = randrange(1,7) print "你掷出的点数是:",value

再看个例子,两个运动员打乒乓球,谁能赢呢?胜负自然取决于球员的技术水平,但又 并非水平高的人必然赢,毕竟体育比赛和天时地利人和等各种因素有关。既然比赛结果有随 机性,我们就可以利用蒙特卡洛方法来模拟比赛。假设 A、B 两个球员相互之间的胜率大致 是 55%对 45%,那么他们打一次比赛(比赛的单位可以是 1 分、1 局或 1 盘,在此并不重要) 的结果可以用如下代码模拟:

  1. if random() < 0.55:
  2. print "A wins."
  3. else:
  4. print "B wins."

这里,由于 random()生成的随机数均匀地分布在[0,1)区间内,所以有 55%的值落在 0.55 的 左边,即 random() < 0.55 为真的可能性为 55%,为假(即 random() >= 0.55)的可能性为 45%, 这就恰当地模拟了 A 和 B 的胜率。注意,random() = 0.55 时应该算作 B 赢,因为 random() 生成的随机数包含 0 但不包含 1。