第模拟法
3 
章

模拟法(simulationmethod)通常基于问题描述,或完成简单的建模, 

或模拟过程的实现,是最简单的算法设计技术。有些问题很难通过数学推

导找到规律,一般采用模拟法直接模拟问题中事物的变化过程,从而完成

相应的任务。

3.概
1 
述


3.1.1 
模拟法的设计思想课件3-
1 
用模拟法求解问题的基本思想是对问题进行抽象,将现实世界的问题
映射成计算机能够识别的符号表示,将事物之间的关系映射成运算或逻辑
控制。模拟法求解的问题通常是对某一类事件进行描述,然后经过简单计
算给出符合要求的结果。

模拟法是算法设计的基本功,没有复杂的公式和技巧,只需读懂问题、

明确要求,照着逻辑整理步骤,基本都可以完成。需要注意的是,有些问题

的背景错综复杂,如果没有理顺逻辑就可能步入歧途。

3.1.2 
一个简单的例子
: 
鸡兔同笼问题
【问题】笼子里有若干只鸡和兔子,鸡有两只脚,兔子有四只脚,没有

例外情况。已知笼子里脚的数量,问笼子里至多有多少只动物? 至少有多

少只动物? 

【想法】对于同样数目的动物,鸡脚的总数肯定比兔子脚的总数要

少,因此在计算笼子里至多有多少只动物时,应该把脚都算作鸡脚,在计算

笼子里至少有多少只动物时,应该尽可能把脚都算作兔子脚。

【算法】设函数Fets实现鸡兔同笼问题,算法如下。

算法:鸡兔同笼问题Fets 
输入:脚的数量
n 


34 算法设计与分析(第3 版) 
输出:至多的动物数maxNum,至少的动物数minNum 
1.如果n 是奇数,则没有满足要求的解,maxNum=0,minNum=0; 
2.如果n 是偶数且能被4整除,则maxNum=n/2,minNum=n/4; 
3.如果n 是偶数但不能被4整除,则maxNum=n/2,minNum=(n-2)/4+1; 
4.输出maxNum 和minNum; 
【算法分析】 算法Feets只是进行了简单的判断和赋值,时间复杂度是O(1)。
【算法实现】 设形参maxNum 和minNum 以传引用方式接收求得的结果,程序
如下。 
void Feets(int n, int &maxNum, int &minNum) 
{ 
if (n % 2 != 0) {maxNum = 0; minNum = 0;} 
else if (n % 4 == 0) {maxNum = n/2; minNum = n/4;} 
else {maxNum = n/2; minNum = (n-2)/4 + 1;} 
} 
3.2 数学问题中的模拟法
3.2.1 约瑟夫环问题 
【问题】 约瑟夫环问题(Josephuscircleproblem)由古罗马史学家约瑟夫提出,他参
加并记录了公元66—70年犹太人反抗罗马的起义。约瑟夫作为一个将军,守住了裘达伯
特城达47天之久。在城市沦陷后,他和40名视死如归的将士在一个洞穴中避难,这些反
抗者表决说“要投降毋宁死”。于是,约瑟夫建议每个人轮流杀死他旁边的人,而这个顺序
是由抽签决定的。约瑟夫有预谋地抓到了最后一签,并且,作为洞穴中的两个幸存者之
一,他说服了同伴一起投降了罗马。
【想法】 将参与抽签的人从1至n 进行编号并构成一个环,从而将约瑟夫环问题抽
象为如图3-1所示数据模型。假设密码是m ,从第1个人开始报数,报到m 时停止报数, 
报m 的人出环;再从他的下一个人起重新报数,报到m 时停止报数,报m 的人出环。如
此下去,直至所有人全部出环。当任意给定n 和m 后,求n 个人出环的次序。
图3-1 约瑟夫环问题的数据模型(n=5,m =3时的出圈次序:3,1,5,2,4) 
【算法】 设函数Joseph求解约瑟夫环问题,用数组r[n]存储n 个人是否出列,下标
源代码3-1 
课件3-2

第3 章 模拟法 35 
表示人的编号,从1开始数到密码m 则将其出列。如果编号i 的人出列则将数组r[i]置
为1,用求模运算%实现下标在数组内循环增1。算法如下。
算法:约瑟夫环问题Joseph 
输入:参与游戏的人数n,密码m 
输出:最后一个出列的编号
1.初始化数组r[n]={0}; 
2.计数器count=0;下标i=-1;出列人数num=0; 
3.重复下述操作直到数组r[n]仅剩一个人: 
3.1当count< m 时重复下述操作: 
3.1.1i=(i+1)% n; 
3.1.2如果r[i]未出列,则计数器count++; 
3.2令r[i]=1;num++; 
4.查找并返回仅剩的编号; 
【算法分析】 步骤3在数组r[n]中反复查找待出列编号,外循环执行n-1次,内循
环执行m 次,时间复杂度为O(n×m )。
【算法实现】 设函数Joseph求解约瑟夫环问题,程序如下。 
int Joseph(int r[ ], int n, int m) 
{ 
int count, i = -1, num = 0; 
while (num < n - 1) 
{ 
count = 0; 
while (count < m) //查找报到m 的人 
{ 
i = (i+1) % n; 
if (r[i] != 1) count++; 
} 
r[i] = 1; num++; //标记出列的人 
} 
for (i = 0; i < n; i++) 
if (r[i] == 0) return i+1; //返回编号
} 
3.2.2 埃拉托色尼筛法
【问题】 埃拉托色尼筛法(thesieveofEratosthenes)简称埃氏筛法,是古希腊数学
家埃拉托色尼提出的算法,用于求一定区间内的所有素数。算法的基本思想是,从区间
[1,n]内的所有数中去掉所有合数,剩下的就是所有素数。判断合数的方法是从2开始
依次过筛,如果是2的倍数则该数不是素数,进行标记处理,直至将n/2过筛,将所有合数
源代码3-2

36 算法设计与分析(第3 版) 
打上标记。
【想法】 假设有一个筛子存放整数1~n,依次将2,3,5,…的倍数筛去(标记),最后
没有打上标记的数都是素数。埃拉托色尼筛法的计算过程如图3-2所示。
图3-2 埃拉托色尼筛法的计算过程
【算法】 设数组A[n]表示筛子,元素值全部初始化为0,依次将下标是2,3,5,…倍
数的元素值置1进行标记处理,最后所有元素值为0对应的下标都是素数,算法如下。
算法:埃拉托色尼筛EratoSieve 
输入:待确定素数的范围n,数组A[n] 
输出:区间[1,n]的所有素数
1.循环变量i 从2至n/2重复执行下述操作: 
1.1如果A[i]不等于0,说明整数i 不是素数,转步骤1.3取下一个素数; 
1.2将所有下标是i 的倍数的元素值置为1; 
1.3i++; 
2.输出数组A[n]中所有元素值为0对应的下标; 
【算法分析】 埃拉托色尼筛法实际上是一种空间换时间的算法优化,对于判断单个
数的素数性质来说,相对于朴素的算法没有优化,但是对于求解某一区间的素数问题,埃
拉托色尼筛法可以很快打印一份素数表,时间复杂度只有O(nloglogn)。
【算法实现】 设函数EratoSieve实现埃拉托色尼筛法,程序如下。 
void EratoSieve(int A[ ], int n) 
{ 
int i, j; 
for (i = 2; i <= n/2; i++) 
{ 
if (A[i] != 0) continue; 
else 
{ 
for (j = 2; i * j <= n; j++) 
源代码3-3

第3 章 模拟法 37 
A[i*j] = 1; 
} 
} 
} 
3.3 排序问题中的模拟法
3.3.1 计数排序 
【问题】 假设待排序记录均为整数且取自区间[0,k],计数排序①(countsort)的基
本思想是对每一个记录x,确定小于x 的记录个数,然后直接将x 放在应该的位置。例
如,小于x 的记录个数是10,则x 就位于第11个位置。
【想法】 对于待排序序列A[n]={2,1,5,2,4,3,0,5,3,2},k=5,首先统计值
为i(0≤i≤k)的记录个数存储在num[i]中,则num[k]={1,1,3,2,1,2},再统计小
于等于i(1≤i≤k)的记录个数存储在num[i]中,则num[k]={1,2,5,7,8,10},最后
反向读取数组A[n]填到数组B中,例如读取A[9]的值是2,则将num[2]减1,然后将2 
填到B[4]中,如图3-3所示。注意,统计小于i(1≤i≤k)的记录个数不能就地进行(利用
数组num),需要再设一个数组存放小于i 的记录个数,就可以正向读取数组A[n]。
图3-3 计数排序过程
【算法】 设函数CountSort实现计数排序,数组num[k+1]存储每个记录出现的次
数以及小于等于值为i 的记录个数,算法如下。
算法:计数排序CountSort 
输入:待排序记录序列A[n],记录的取值区间k 
输出:排序数组B[n] 
1.统计值为i 的记录个数存入num[i]; 
2.统计小于等于i 的记录个数存入num[i]; 
3.反向填充目标数组,将A[i]放在B[--num[A[i]]]中; 
4.输出数组B[n]; 
① 计数排序算法在1954年由HaroldH.Seward提出,可以在线性时间对取值范围为某一区间的记录序列进行排序。
课件3-3

38 算法设计与分析(第3 版) 
【算法分析】 计数排序是一种以空间换时间的排序算法,并且只适用于对一定范围
内的整数进行排序,时间复杂度为O(n+k),其中k 为序列中整数的范围。
【算法实现】 计数排序的关键在于确定待排序记录A[i]在目标数组B[n]中的位
置,由于数组元素num[i]存储的是A[n]中小于等于i 的记录个数,所以填充数组B[n] 
时要反向读取A[n]。程序如下。 
void CountSort(int A[ ], int n, int k, int B[ ]) 
{ 
int i, num[k+1] = {0}; 
for(i = 0; i < n; i++) 
num[A[i]]++; 
for (i = 1; i <= k; i++) 
num[i] = num[i] + num[i-1]; 
for (i = n-1; i >= 0; i--) 
B[--num[A[i]]] = A[i]; 
} 
3.3.2 颜色排序
【问题】 现有Red、Green和Blue三种不同颜色(色彩中不能再分解的三种颜色,称
为三原色)的小球,乱序排列在一起,请按照Red、Green和Blue顺序重新排列这些小球, 
使得相同颜色的小球排在一起。
【想法】 设数组a[n]存储Red、Green和Blue三种元素,设置三个参数i、j、k,其中
i 之前的元素(不包括a[i])全部为Red;k 之后的元素(不包括a[k])全部为Blue;i 和j 
之间的元素(不包括a[j])全部为Green;j 指向当前正在处理的元素。首先将i 初始化为
0,k 初始化为n-1,j 初始化为0。然后j 从前向后扫描,在扫描过程中根据a[j]的颜色, 
将其交换到序列的前面或后面,当j 等于k 时,算法结束。颜色排序的求解思想如图3-4 
所示。
图3-4 颜色排序的求解思想
注意,当j 扫描到Red时,将a[i]和a[j]交换,只有当前面全部是Red时,交换到位
置j 的元素是Red,否则交换到位置j 的元素一定是Green,因此交换后j 应该加1;当j 
扫描到Blue时,将a[k]和a[j]交换,Red、Green和Blue均有可能交换到位置j,则a[j]需
要再次判断,因此交换后不能改变j 的值。
【算法】 设数组a[n]有Red、Green和Blue三种元素,函数ColorSort实现颜色重排
问题,算法如下。
源代码3-4

第3 章 模拟法 39 
算法:颜色排序ColorSort 
输入:待排序记录序列a[n] 
输出:排好序的数组a[n] 
1.初始化i=0;k=n-1;j=0; 
2.当j≤k时,依次考查元素a[j],有以下三种情况: 
(1)如果a[j]是Red,则交换a[i]和a[j];i++;j++; 
(2)如果a[j]是Green,则j++; 
(3)如果a[j]是Blue,则交换a[k]和a[j];k--; 
【算法分析】 由于下标j 和k 整体将数组扫描一遍,因此时间复杂度为O(n)。
【算法实现】 由于数组a[n]只有三种元素,假设Red、Green和Blue三种颜色分别
用1、2、3来代替,程序如下。 
void ColorSort(int a[ ], int n) 
{ 
int i = 0, k = n - 1, j = 0, temp; 
while (j <= k) 
switch (a[j]) //考查当前元素 
{ 
case 1: temp = a[i]; a[i] = a[j]; a[j] = temp; i++; j++; break; 
case 2: j++; break; 
case 3: temp = a[j]; a[j] = a[k]; a[k] = temp; k--; break; 
} 
} 
3.4 拓展与演练
3.4.1 装箱问题 
【问题】 有一个工厂制造的产品形状都是长方体,一共有6种型号,每种型号长方体
的长和宽分别是1×1、2×2、3×3、4×4、5×5、6×6,高都是h。这些产品使用统一规格
的箱子进行包装,箱子的长、宽和高分别是6、6和h。对于每个订单工厂希望用最少的箱
子进行包装。每个订单包括用空格分开的6个整数,分别代表这6种型号的产品数量。
输出是包装需要箱子的个数。
【想法】 这个问题很难建立一个数学模型,只能模拟包装过程,分析装入6种产品后
箱子的剩余空间。装箱情况分析如表3-1所示。
源代码3-5 
课件3-4

40 算法设计与分析(第3 版) 
表3-1 6种产品占用箱子与剩余空间的装箱情况分析
1个箱子容纳产品剩余空间的装载情况
产品个数2×2 1×1 解 释
6×6 1 0 0 无剩余空间
5×5 1 0 11 11个长宽为1的产品
4×4 1 5 0 5个长宽为2的产品
3×3 1 5 7 5个长宽为2的产品和7个长宽为1的产品
3×3 2 3 6 3个长宽为2的产品和6个长宽为1的产品
3×3 3 1 5 1个长宽为2的产品和5个长宽为1的产品
3×3 4 0 0 无剩余空间
2×2 9 0 0 无剩余空间
1×1 36 0 0 无剩余空间 
【算法】 设k1、k2、k3、k4、k5 和k6 分别表示6种型号的产品数量,x 和y 分别表示
长宽为2和1的空位数量,n 表示需要的箱子个数,算法如下。
算法:装箱问题Packing 
输入:6种型号的产品数量k1、k2、k3、k4、k5、k6 
输出:箱子个数n 
1.n=装入长宽为3×3、4×4、5×5、6×6所需箱子数; 
2.x=n 个箱子剩余2×2的空位数; 
3.如果k2>x,则n=n+(k2-x 个产品需要的箱子数); 
4.y=n 个箱子剩余1×1的空位数; 
5.如果k1>y,则n=n+(k1-y 个产品需要的箱子数); 
6.输出箱子个数n; 
【算法分析】 算法Packing所有操作步骤都是简单的计算,时间复杂度为O(1)。
【算法实现】 设变量k1、k2、k3、k4、k5和k6分别表示6种型号的产品数量,变量x 
和y分别表示长宽为2和1的空位数量,变量n表示需要的箱子个数。设数组p2[4]存
储装入3×3的产品个数分别是4、1、2、3时箱子剩余2×2的空位数。注意,程序中所有
的整除都应该保证向上取整。程序如下。 
int Packing(int k1, int k2, int k3, int k4, int k5, int k6) 
{ 
int n, x, y; 
int p2[4] = {0, 5, 3, 1}; 
n = (k3 + 3)/4 + k4 + k5 + k6; 
x = 5 * k4 + p2[k3 % 4]; 
if (k2 > x) n += (k2 - x + 8) / 9; 
源代码3-6

第3 章 模拟法 41 
y = 36 * n - 36 * k6 - 25 * k5 - 16 * k4 - 9 * k3 - 4 * k2; 
if (k1 > y) n += (k1 - y + 35) / 36; 
return n; 
} 
3.4.2 数字回转方阵
【问题】 n 阶数字回转方阵是将数字1置于方阵的左上角,然后从1开始递增,将
n2 个整数填写到n 阶方阵中,偶数层从第1行开始,先向下再折转向左,奇数层从第1列
开始先向右再折转向上,呈首尾相接,图3-5所示为一个5阶数字回转方阵。
图3-5 5阶数字回转方阵
【想法】 根据问题描述的填数规则,找到下标变化规律, 
用模拟法直接求解。对于方阵的偶数行和列,填数的起始位置
是(1,i),然后列号不变行号加1,至位置(i,i)时折转,行号不
变列号减1,直至位置(i,1);对于方阵的奇数行和列,填数的
起始位置是(j,1),然后行号不变列号加1,至位置(j,j)时折
转,列号不变行号减1,直至位置(1,j)。
【算法】 设函数Full实现填写数字回转方阵,注意数组下
标从0开始,算法如下。
算法:数字回转方阵Full 
输入:方阵的阶数n 
输出:数字回转方阵z[n][n] 
1.z[0][0]=1,number=2; 
2.for(i=0,j=1;i< n&&j< n;); 
2.1填写偶数层: 
2.1.1填数直至i等于j: 
z[i++][j]=number++; 
2.1.2填数直至j等于0: 
z[i][j--]=number++; 
2.2填写奇数层: 
2.2.1填数直至i等于j: 
z[i][++j]=number++; 
2.2.2填数直至i等于0: 
z[i--][j]=number++; 
【算法分析】 算法Full需要依次填写矩阵的每一个元素,时间复杂度是O(n2)。
【算法实现】 注意每一层填数后都要调整下标,程序如下。 
void Full(int z[100][100], int n) 
{ 
int number, i, j; 源代码3-7

42 算法设计与分析(第3 版) 
z[0][0] = 1; number = 2; 
for (i = 0, j = 1; i < n && j < n; ) //依次填写每一层 
{ 
while (i < j) 
z[i++][j] = number++; 
while (j >= 0) 
z[i][j--] = number++; 
i++; j = 0; 
while (i > j) 
z[i][j++] = number++; 
while (i >= 0) 
z[i--][j] = number++; 
j++; i = 0; 
} 
} 
实验3 埃氏筛法的优化
【实验题目】 对于埃拉托色尼筛法,观察图3-2,很多合数被标记了不止1次,例如合
数6,在将2过筛时标记了1次,将3过筛时又标记一次,造成了不必要的重复标记。请
改进埃拉托色尼筛法,使得在线性时间内完成所有合数的标记。
【实验要求】 对于优化的埃拉托色尼筛法,要保证两点:①合数一定被标记;②每
个合数都没有被重复标记。
【实验提示】 注意到任何合数都能表示成一系列素数的乘积,如果保证每个合数只
被其最小质因数筛去,就能够达到不重复标记。对于整数i,对所有不超过i 的素数p,将
i *p 标记为合数,如果p 是i 的因子则将整数i 进行标记并停止过筛,直至p 等于i,说
明整数i 是素数。
习 题 3 
1.如果一个十进制的正整数能够被7整除,或者某个位置的数字是7,则称该正整数
为与7相关的数。求小于n(n<100)的所有与7无关的正整数的平方和。
2.对于一元二次方程ax2+bx+c=0,给定系数a、b 和c,求方程的根。要求所有实
数精确到小数点后5位。
3.恺撒加密由古罗马恺撒大帝在其政府的秘密通信中使用而得名,其基本思想是: 
将待加密信息(称为明文)的每个字母在字母表中向后移动常量key(称为密钥),得到加
密信息(称为密文)。假设字母表为英文字母表,对于给定的明文和密钥,请给出恺撒加密
后的密文。
4.校门外的树。校门外长度为L 的马路上有一排树,每两棵相邻的树之间的距离都

43
第3章模拟法

是1m 。可以把马路看成一个数轴,校门口在数轴0的位置,另一端在
L 
的位置,数轴上
每个整数点都有一棵树。马路上有一些区域要用来修建地铁,这些区域用数轴上的半开
区间[s,t)来表示,已知
s 
和
t 
都是整数,且区域之间可能有部分重叠。由于修建地铁要
把区域中的树全部移走,问马路上还剩下多少棵树? 

5.对于约瑟夫环问题, 请修改3.1节给出的算法。
假设每个人持有的密码不同,2.

6.桥牌共52张,没有大小王,按E、S、W、N的顺序把52 
张牌随机发给四个玩家,请列出每个玩家的发牌情况。
7.定义
n 
阶间断折叠方阵是把n2 个整数折叠填写到
n 
阶
方阵中,起始数1置于方阵的左上角,然后从起始数开始递增, 
每一层从第1行开始,先向下再折转向左,层层折叠地排列为
间断折叠方阵。图3-6所示为5阶间断折叠方阵。请构造并
输出任意
n 
阶间断折叠方阵。图3-
6 
5阶间断折叠方阵
8.泊松分酒。法国数学家泊松(Poison)提出的分酒趣
题:有一瓶12品脱(容量单位)的酒,同时有容积为5品脱和8品脱的空杯各一个,借助
这两个空杯,如何将这瓶12品脱的酒平分?