目录
一、实验目的
1. 掌握动态规划算法设计思想。
2. 掌握扔鸡蛋问题的动态规划法。
二、问题描述
扔鸡蛋问题是计算机程序设计中的一个经典问题。从一幢楼房的不同楼层往下扔鸡蛋,用最少的最坏情况试验次数,确定鸡蛋不会摔碎的最高安全楼层。仅有一个鸡蛋供试验时,只能采用顺序查找法。有足够多的鸡蛋时,可以采用二分查找法。有多于一个但数量有限的鸡蛋时,采用动态规划方法求解。双蛋问题(two-egg problem)是本问题的一个特例,曾出现于谷歌的程序员面试题中。
有一幢楼房高层。某人准备了N个鸡蛋供试验。他想知道鸡蛋从几层扔下不会摔碎,并确定出最高安全楼层。试验过程中,鸡蛋没有摔碎则可以继续使用,摔碎了则需要换一个鸡蛋继续试验。为保证试验成功,此人要设计一个程序,以最小化最坏情况的试验次数F(M, N)。作为一个数学抽象,本问题采用一些理想化假设:所有鸡蛋抗摔能力相同,不计重复坠地的累积损伤,且忽略试验结果的偶然性。试验成功的标准是在N个鸡蛋用完之前,精确确定最高安全楼层是哪一层。允许有鸡蛋剩余。
如果只有N=1个鸡蛋供试验,则为了保证试验成功,只能从一层开始逐层往上试验。这相当于采用顺序查找算法,最坏试验次数F(M, 1)=M。如果一层就碎了,则最高安全楼层为0。如果M层还不碎,则最高安全楼层为M。
三、实验要求
1. 给出解决问题的动态规划方程;
2. 理论分析该算法的时间复杂度;
3. 分别测试M=10000, 20000, …, 100000, N=20时以及M=50000, N=11, 12, …, 20时的算法运行时间,并分析实验结果;
4. 依次在终端输出M=10000, N=1~20时的F(M, N)值,实验课时检查该代码,限用C或C++语言编写。
四、算法思想和实验结果
1、动态规划法原理:
将复杂问题划分为更小的子问题,通过子问题的最优解来重构原问题的最优解。求解过程中,保存子问题的解,以便在需要时直接查表而不是重复计算,从而减少计算量。
2、解决方法:
2.1 方法一:常规动态规划
2.1.1 算法思想:
用二维数组k[m][n]表示从第m层扔n个鸡蛋的动态规划法最优解,即该实验所要求的最少的最坏情况试验次数。
对于m层、n个鸡蛋的求解,当尝试从第x层扔下一个鸡蛋时,有两种情况:
1)鸡蛋破碎,剩余n-1个鸡蛋,则在第1层至第x-1层(共x-1层)继续尝试,即k[x-1][n-1]。
2)鸡蛋未破碎,仍剩余n个鸡蛋,则在第x+1至m层(共m-x层)继续尝试,即k[m-x][n];
因为题目要求是最坏情况,所以应该取上述两种情况的较大者。又由于要最少实验次数,所以x应该取让前面的较大者尽量小些的值。
得动态规划方程如下:
k[m][n]=1+min{max{k[x-1][n-1],k[m-x][n]}}(x=1,2,3……,m)
实验时
1)对于m=0、m=1或n=1的情况取值依次为0、1、m(在创建**k时就赋值完成)。
2)对于其他值,用三层for循环自底向上依次确定k[i][j]的值。伪代码如下:
F1(m,n)
for i=2 to m
for j=2 to n
for x=1 to i
k[i][j]=1+max{k[x-1][j-1],k[i-x][j]}
return k[m][n]
2.1.2 时间复杂度分析
由前面伪代码可知,最外层m-1次,中间层n-1次,最里层i(i=2,3,……,m)次,则时间复杂度为O(n*m^2)。
2.1.3 时间效率分析
由于效率较低,这里降低数据规模,分别测试M=1000, 2000, …, 10000, N=20时以及M=10000,N=11, 12, …, 20时的算法运行时间(对每种情况都运行5次取平均值)。
1)N=20时,以M=1000为基准
M | 1000 | 2000 | 3000 | 4000 | 5000 | 6000 | 7000 | 8000 | 9000 | 10000 |
平均运行时间/ms | 47 | 190 | 434 | 814 | 1310 | 1964 | 2750 | 3706.2 | 4722.4 | 5802 |
理论时间/ms | 47 | 188 | 423 | 752 | 1175 | 1692 | 2303 | 3008 | 3807 | 4700 |
得下图:N=20时的实际效率曲线和理论效率曲线图。
两条曲线一开始较贴合,但随着M的增大,实际运行时间与理论运行时间的差(非负)呈现递增趋势。
2)M等于10000时,以N=11为基准:
N | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 |
平均运行时间/ms | 2890.5 | 3152.25 | 3443 | 3832 | 4092.5 | 4408 | 4704 | 5180 | 5478 | 5825 |
理论时间/ms | 2890.5 | 3153.27 | 3416.05 | 3678.82 | 3941.59 | 4204.36 | 4467.17 | 4729.91 | 4992.68 | 5255.45 |
得下图:M=10000时的实际效率曲线和理论效率曲线图。
与上一个图一样,两条曲线一开始较贴合,但随着N的增大,实际运行时间与理论运行时间的差(非负)呈现递增趋势。
可能原因是
k[i][j]=1+max{k[x-1][j-1],k[i-x][j]}中的数据访问的耗时实际是随着规模的增大而增大的,但分析时间复杂度和理论时间时忽略这个。(实际效率与理论效率之间差某些常数因子)
2.2 方法二:动态规划加二分查找最优x
2.2.1 算法思想:
对于
for x=1 to m
k[i][j]=1+max{k[x-1][j-1],k[i-x][j]}
当x增大时,k[x-1][j-1]是x相关的非递减函数,而k[i-x][j]是x相关的非递增函数。所以可以在前面的基础上,用二分法求出最优的x,而不是从1到m逐个的尝试、比较。
由于层数为整数,二分到最后有两种情况:
1)二分到最后low!=high,如下图,则应该取点1和点3中次数较小的那个(最坏情况下的最少测试次数),即
k[i][j]=min{k[i-low][j],k[high-1][j-1]}
2)分到最后刚好low=high,如下图:此时x=low=high,k[x-1][j-1=,k[i-x][j]。
伪代码如下:
F2(m,n)
for i=2 to m
for j=2 to n
l=1;
h=i;
while l+1<h
x=(l+h)/2
if k[x-1][j-1]<k[i-x][j]
l=x
else if k[x-1][j-1]>k[i-x][j]
h=x
else low=high=x
k=1+min{k[i-l][j],k[h-1][j-1]}
return k[m][n]
2.2.2 时间复杂度分析
原来求最优x需m次循环,现在为log m,所以时间复杂度变为O(n*m*log m)
2.2.3 时间效率分析
分别测试M=10000, 20000, …, 100000, N=20时以及M=50000, N=11, 12, …, 20时的算法运行时间(对每种情况都运行20次取平均值)。
1)N=20时,以M=10000为基准
M | 10000 | 20000 | 30000 | 40000 | 50000 | 60000 | 70000 | 80000 | 90000 | 100000 |
平均运行时间/ms | 5 | 9.5 | 14.5 | 19.05 | 29.95 | 34.45 | 41.5 | 47.45 | 57.5 | 62.93 |
理论时间/ms | 5 | 10.75 | 16.79 | 23.01 | 29.37 | 35.84 | 42.39 | 49.03 | 55.74 | 62.5 |
得下图:N=20时的实际效率曲线和理论效率曲线图。
两条曲线贴合度较高,说明算法效率基本符合关于m的mlog m级关系。
2)M等于50000时,以N=11为基准
N | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 |
平均运行时间/ms | 17 | 18.5 | 19.45 | 20 | 22.5 | 24 | 26.25 | 27.25 | 28.25 | 29 |
理论时间/ms | 17 | 18.55 | 20.09 | 21.64 | 23.18 | 24.73 | 26.27 | 27.83 | 29.36 | 30.91 |
得下图:M=50000时的实际效率曲线和理论效率曲线图。
两条曲线较贴合,说明算法效率基本符合关于N的线性关系。
2.3 方法三:动态规划加逆向求解
2.3.1 算法思想
不直接按照在m层、n个鸡蛋的情况下求解最少的最坏情况试验次数,而是逆向求解在n个鸡蛋、r次试验次数且最坏情况下最多能测多少层。找到层数大于m时的最小r。
此时扔鸡蛋,碎了,就继续测楼下;没碎,就继续测楼上。
总的可测楼层数 = 楼上的可测楼层数 + 楼下的可测楼层数 + 1(当前这层楼)。即
k[n][r]=k[n][r-1]+k[n-1][r-1]+1
此时的**k和前面的两种方法的不同,分配的空间不再是k[m+1][n+1],而是为k[n][max],max为一个较大的值(由于实验要求的最大规模M=100000、N=20的解为447,所以这里max取1000,实际未知该解时应取更大些)。
伪代码如下:
F3(m,n)
r=0
while k[n][r]<m //r不超过m
r++
for i=1 to n
k[i][r]=k[i][r-1]+k[i-1][r-1]+1
return r
2.3.2 时间复杂度分析
外层while循环(不大于)m次,内层for循环n次,所以时间复杂度为O(n*m)。
2.3.3 时间效率分析
效率较高,m=2000000000,n=20时运行时间仍然小于1ms。
3、结果输出示例
4、代码:
#include<iostream>
#include<ctime>
#include<cmath>
using namespace std;
int **k,M,N;
int ***v;//v[i][j][0]、v[i][j][1]、v[i][j][2]依次表示(1)当前i、j要从第几层扔;(2)、(3)前面扔时的层数和鸡蛋个数
void Create12(int m,int n){//方法1、2的输出
int i,j;
k=new int*[m+1];
for(i=0;i<=m;i++){
k[i]=new int[n+1];
for(j=0;j<=n;j++)
k[i][j]=1000000;//
}
for(i=0;i<=m;i++){
k[i][0]=0;
k[i][1]=i;
}
for(j=1;j<=n;j++){
k[0][j]=0;
k[1][j]=1;
}
v=new int**[m+1];
for(i=0;i<=m;i++){
v[i]=new int*[n+1];
for(j=0;j<=n;j++){
v[i][j]=new int[3];
v[i][j][0]=0;
v[i][j][1]=0;
v[i][j][2]=0;
}
}
}
void Create3(int m,int n){//方法3(逆向)的输出
int i,j;
k=new int*[n+1];
for(i=0;i<=n;i++){
k[i]=new int[1000000];
for(j=0;j<=1000000;j++)
k[i][j]=0;//
}
}
int F1(int m,int n){//--------------------动态规划法
int i,j,t,x,ju=0;
for(i=2;i<=m;i++){
for(j=2;j<=n;j++){
for(x=1;x<=i;x++){
if(k[x-1][j-1]>k[i-x][j]){//取更坏的情况
t=k[x-1][j-1]+1;
ju=0;
}
else {
t=k[i-x][j]+1;
ju=1;
}
if(t<k[i][j]){
k[i][j]=t;
v[i][j][0]=x;
if(ju){
v[i][j][1]=i-x;
v[i][j][2]=j;
}else{
v[i][j][1]=x-1;
v[i][j][2]=j-1;
}
}
}
}
}
return k[m][n];
}
int F2(int m,int n){//--------------------动态规划加二分查找法求解开始:
int i,j,t,x,ju=0,low,high;
for(i=2;i<=m;i++)
for(j=2;j<=n;j++){
low=1;
high=i;
while(low+1<high){
x=(low+high)/2;
if(k[x-1][j-1]<k[i-x][j])//
low=x;
else if(k[x-1][j-1]>k[i-x][j])
high=x;
else {
low=x;high=x;
}
}
if(k[i-low][j]<k[high-1][j-1]){//取较少的次数
t=k[i-low][j];
ju=1;
}
else {
t=k[high-1][j-1];
ju=0;
}
t++;
if(t<k[i][j]){
k[i][j]=t;
}
}
return k[m][n];
}
int F3(int m,int n){//--------------------动态规划加逆向求解法
int r=0,i;
while(k[n][r]<m){//鸡蛋个数、次数、层数
r++;
for(i=1;i<=n;i++)
k[i][r]=k[i][r-1]+k[i-1][r-1]+1;
}
return r;
}
int main(){
int m,n,num1,num2;//层数、鸡蛋数、实验次数、循环次数
double time,Time=0;//一次用时,平均用时
int res;//需要的次数
double start,end;
cout<<"请依次输入实验次数、循环次数:";
cin>>num1>>num2;
cout<<"动态规划法求解开始:"<<endl;
for(int i=1;i<=num1;i++){
cout<<"请依次输入层数、鸡蛋数:";
cin>>m>>n;
time=0;
for(int j=1;j<=num2;j++){
Create12(m,n);
start=clock();
res=F1(m,n);//
end=clock();
time+=1000*(end-start)/CLOCKS_PER_SEC;
}
cout<<"层数为"<<m<<"且鸡蛋个数为"<<n<<"时的最少的最坏情况试验次数为"<<res<<endl
<<"平均用时为"<<time/num2<<"ms"<<endl<<endl;
//Print(m,n);
}
cout<<"动态规划加二分查找法求解开始:"<<endl;
for(int i=1;i<=num1;i++){
cout<<"请依次输入层数、鸡蛋数:";
cin>>m>>n;
time=0;
for(int j=1;j<=num2;j++){
Create12(m,n);
start=clock();
res=F2(m,n);//
end=clock();
time+=1000*(end-start)/CLOCKS_PER_SEC;
}
cout<<"层数为"<<m<<"且鸡蛋个数为"<<n<<"时的最少的最坏情况试验次数为"<<res<<endl
<<"平均用时为"<<time/num2<<"ms"<<endl<<endl;
//Print(m,n);
}
cout<<"动态规划加逆向求解法求解开始:"<<endl;
for(int i=1;i<=num1;i++){
cout<<"请依次输入层数、鸡蛋数:";
cin>>m>>n;
time=0;
for(int j=1;j<=num2;j++){
Create3(m,n);
start=clock();
res=F3(m,n);//
end=clock();
time+=1000*(end-start)/CLOCKS_PER_SEC;
}
cout<<"层数为"<<m<<"且鸡蛋个数为"<<n<<"时的最少的最坏情况试验次数为"<<res<<endl
<<"平均用时为"<<time/num2<<"ms"<<endl<<endl;
}
//依次M=10000,N1~20
cout<<"实验要求的输出(M=10000,N=1~20):"<<endl;
for(int i=1;i<=20;i++){
Create12(10000,i);
cout<<F2(10000,i)<<endl;
//Print(m,n);
}
return 0;
}