一.实验目的
- 掌握动态规划算法设计思想。
- 掌握鸡蛋坠落问题的动态规划解法。
二.实验步骤与结果
1.动态规划原理
将问题划分为更小的子问题,通过子问题的最优解来重构原问题的最优解。动态规划将子问题的最优解存储在一些数据结构中,这样我们就不必在再次求解相同的子问题。任何重复调用相同输入的递归解决方案,我们都可以通过动态规划对其进行优化求解。
2.问题描述
我们需要用鸡蛋确认在多高的楼层鸡蛋落下来会破碎,这个刚刚使鸡蛋破碎的楼层叫门槛层。门槛楼层是鸡蛋开始破碎的楼层,在其上面所有楼层的鸡蛋也会破粹。另外,如果鸡蛋从门槛楼层以下的任何楼层掉落,则它不会破碎。
如果门槛层是第k层,一个鸡蛋,最少的实验次数是k次。反之,如果有e个鸡蛋,楼层数是0,则最少试验次数是0;楼层数是1,最少试验次数是1。
问题约束条件:从跌落中幸存下来的鸡蛋可以再次使用;破蛋必须丢弃;摔碎对所有鸡蛋的影响都是一样的;如果一个鸡蛋掉在地上摔碎了,那么它从高处掉下来也会摔碎;如果一个鸡蛋在跌落中幸存下来,那么它在较矮楼层的跌落中也能完整保留下来。
3.问题分析
采用动态规划模型求解,N个鸡蛋K层楼的情况下,用np(N,K)表示该问题的解。
将鸡蛋从x层扔下,有以下两种情形:
①鸡蛋摔碎了,此时剩下N-1个鸡蛋,需要考虑比x层低的楼层,即1,2 ,... , x-1层(比x层高的楼层扔下去必定也摔碎,故不考虑)。于是,这个问题变成np(N-1, x-1)。
②鸡蛋没有摔碎,此时剩下N个鸡蛋,需要考虑比x层高的楼层,即x+1,x+2 ,...,k层(比x层高的楼层扔下去必定不会摔碎,故不考虑)。于是,问题变成np(N, k-x).
对于以上两种情形,应该取两者的最大值。且x=1,2,3,...,k(从x层开始扔)。此有如下动态规划方程:
4.解决问题
表1 动态规划解决鸡蛋问题伪代码
np_1: |
function np(n,k): //n个鸡蛋,k层楼 if n=0 or k=0: return 0 else if n=1: return k else if k=1: return 1 return max(np(n-1,k-1),np(n,k-x)) main(n,k): //n个鸡蛋,k层楼 for(int x=1;x<=k;x++) //遍历比较得到最佳初始楼层 max(np(n-1,k-1),np(n,k-x))
|
时间复杂度为:O(K*N^2)
表2 (2个鸡蛋)算法实际时间与理论时间对比(s)
楼层数 | 100 | 1000 | 10000 | 100000 |
实际值(s) | 0.003 | 0.25 | 25.6 | 2584.4 |
理论值(s) | 0.003 | 0.25 | 25 | 2500 |
两个鸡蛋的测试情况,时间复杂度为2N^2,通过实际时间与理论时间对比发现,拟合效果较好。
表3 (3个鸡蛋)算法实际时间与理论时间对比(s)
2 | 3 | 4 | 5 | |
实际值 | 0.006 | 0.5 | 51.26 | 5220 |
理论值 | 0.005 | 0.5 | 50 | 5000 |
图2 (3个鸡蛋)算法实际时间与理论时间对比(s)
两个鸡蛋的测试情况,时间复杂度为2N^2,通过实际时间与理论时间对比发现,拟合效果较好。
4.小规模测试
(1)2个鸡蛋30层楼的测试情况:
图3 运行结果截图
方案如下:
表4 (2_30)测试情况
若鸡蛋没碎,扔鸡蛋的层数k | 若摔碎,则继续测试在第K’层扔鸡蛋 |
8 | 1-2-3-4-5-6-7 |
15 | 9-10-11-12-13-14 |
21 | 16-17-18-19-20 |
26 | 22-23-24-25 |
30 | 27-28-29 |
①解释:
2个鸡蛋30层楼的测试情况在最坏的情况下最少的检验次数为8。
第一次从8层扔下,如果鸡蛋没碎,从第15层扔下;如果碎了,则从1楼向上依次扔鸡蛋测试直到第9层。即,每次测试,如果鸡蛋没碎,则继续按照第一列的顺序依次测试;如果鸡蛋碎了,则按照第二列对应的顺序进行测试。
②分析:
如果第一次测试(第8层扔下)时碎了,鸡蛋此时只剩下1个了,为了保证能找到阈值,则用该鸡蛋从已知不会碎的最高低层或底层扔起,直到找到该阈值。
(2)2个鸡蛋50层楼的测试情况:
图4 运行结果截图
方案如下:
表5 (2_50)测试情况
若鸡蛋没碎,扔鸡蛋的层数k | 若摔碎,则继续测试在第K’层扔鸡蛋 |
10 | 1-2-3-4-5-6-7-8-9 |
19 | 11-12-13-14-15-16-17-18 |
27 | 20-21-22-23-24-25-26 |
34 | 28-29-30-31-32-33 |
40 | 35-36-37-38-39 |
45 | 41-42-43-44 |
49 | 46-47-48 |
50 |
①解释:
2个鸡蛋50层楼的测试情况在最坏的情况下最少的检验次数为10。
第一次从10层扔下,如果鸡蛋没碎,从第19层扔下;如果碎了,则从1楼向上依次扔鸡蛋测试直到第9层。即,每次测试,如果鸡蛋没碎,则继续按照第一列的顺序依次测试;如果鸡蛋碎了,则按照第二列对应的顺序进行测试。
②分析:
如果第一次测试(第10层扔下)时碎了,鸡蛋此时只剩下1个了,为了保证能找到阈值,则用该鸡蛋从已知不会碎的最高低层或底层扔起,直到找到该阈值。
5.算法优化
①找到最佳检测开始层x时:np(N,k-x)随x增大而减少,np(N-1,x-1)随x增加而增加
所以可以在原来遍历找最小值的方法上加以优化,即采用二分查找法找到最优的初始楼层x。
表6 优化算法伪代码(一)
二分查找找最优x: |
while(low+1<high) x=(low+high)/2 if np(n-1,x-1) < np(n,k-x); low=x; if np(n-1,x-1) > np(n,k-x); high=x; if np(n-1,x-1) = np(n,k-x); low= x; high=x; ans=1+max[np(n-1,low-1),np(n,k-low),np(n-1,high-1),np(n,k-high)] |
该优化算法的时间复杂度为:O(K*N*logN)
表7 优化算法实际时间与理论时间对比
鸡蛋数 | 楼层数 | 最坏情况下最少测试次数 | 运行时间(ms) |
2 | 10^3 | 45 | 1 |
2 | 10^4 | 141 | 1 |
2 | 10^5 | 447 | 16 |
2 | 10^6 | 1414 | 198 |
2 | 10^7 | 4472 | 2125 |
2 | 10^8 | 14142 | 25956 |
2 | 10^9 | 44721 | 343923 |
图5 实际时间与理论时间对比
两个鸡蛋的测试情况,时间复杂度为2NlogN,通过实际时间与理论时间对比发现,拟合效果较好。
②重新定义状态转移:
给定 K 鸡蛋,N 层楼,求最坏情况下最少的测试次数 m 等同于给定K个鸡蛋,测试 m 次,最坏情况下最多能测试 N 层楼。
即:无论你在哪层楼扔鸡蛋,鸡蛋只可能摔碎或者没摔碎,碎了的话就测楼下,没碎的话就测楼上。总的楼层数 = 楼上的楼层数 + 楼下的楼层数 + 1(当前这层楼)。
新的状态方程为:
dp[k][m] = dp[k][m - 1] + dp[k - 1][m - 1] + 1
表8 优化算法伪代码(二)
重新定义状态转移 |
int superEggDrop(int K, int N) // m 最多不会超过 N 次(线性扫描) // 初始化dp[K+1][N+1]的数组全都为0 while (dp[K][m] < N) // m = 0 m++; for (int k = 1; k <= K; k++) dp[k][m] = dp[k][m - 1] + dp[k - 1][m - 1] + 1; return m; |
该优化算法的时间复杂度为O(KN)
6.随机产生n,k的值:
表9 随机n,k最坏情况下的最小测试次数值结果
鸡蛋数\楼层数 | 10000 | 20000 | 30000 | 40000 | 50000 | 60000 | 70000 | 80000 | 90000 | 100000 |
2 | 141 | 200 | 245 | 283 | 316 | 346 | 374 | 400 | 424 | 447 |
3 | 40 | 50 | 57 | 63 | 67 | 72 | 75 | 79 | 82 | 85 |
4 | 23 | 27 | 30 | 32 | 34 | 36 | 37 | 38 | 39 | 40 |
5 | 18 | 20 | 22 | 23 | 24 | 25 | 26 | 27 | 27 | 27 |
6 | 16 | 17 | 18 | 19 | 20 | 20 | 21 | 21 | 22 | 22 |
7 | 15 | 16 | 17 | 17 | 18 | 18 | 19 | 19 | 19 | 20 |
8 | 14 | 15 | 16 | 17 | 17 | 17 | 18 | 18 | 18 | 18 |
9 | 14 | 15 | 16 | 16 | 16 | 17 | 17 | 17 | 18 | 18 |
10 | 14 | 15 | 15 | 16 | 16 | 17 | 17 | 17 | 17 | 17 |
图6 不同鸡蛋个数和不同楼层情况下最坏情况的最少检查次数
通过图标得出以下结论:
1.相同鸡蛋个数,楼层数越大,最差情况下的最少检查次数越多。
2.相同楼层数,鸡蛋个数越多,最差情况下的最少检查次数越少。
表10 随机n,k的测试算法的时间效率(ms)
鸡蛋数n\楼层数k | 10000 | 20000 | 30000 | 40000 | 50000 | 60000 | 70000 | 80000 | 90000 | 100000 |
2 | 1.4 | 3 | 4.5 | 6.05 | 7.65 | 9.3 | 11.05 | 12.75 | 14.25 | 16.05 |
3 | 1.3 | 2.9 | 4.6 | 6.25 | 7.8 | 9.6 | 11.7 | 13.1 | 15.05 | 17.15 |
4 | 0.95 | 1.9 | 2.95 | 4.4 | 5.85 | 7 | 8.15 | 10.1 | 11.35 | 12.9 |
5 | 0.55 | 1.1 | 1.85 | 2.65 | 3.65 | 3.75 | 4.4 | 5.45 | 6.6 | 7.95 |
6 | 0.45 | 0.6 | 1 | 1.25 | 1.9 | 2.6 | 2.65 | 3.1 | 3.35 | 3.85 |
7 | 0.25 | 0.45 | 0.7 | 1.05 | 0.9 | 1.6 | 1.65 | 1.85 | 1.75 | 2.45 |
8 | 0.05 | 0.25 | 0.8 | 0.6 | 0.45 | 0.9 | 1.1 | 1.05 | 1.1 | 1.2 |
9 | 0 | 0 | 0.85 | 0.3 | 0.7 | 0.35 | 0.45 | 0.7 | 1.6 | 0.95 |
10 | 0 | 0 | 0.25 | 0.1 | 0.3 | 0.45 | 0.15 | 0.25 | 0.4 | 0.7 |
图7 不同鸡蛋数下不同楼层规模的计算时间
7.最大规模:
表11 优化前不同规模效率(2个鸡蛋)
鸡蛋个数 | 楼层数 | 测试运行时间(s) | 理论运行时间(s) |
2 | 100 | 0.003 | 0.003 |
2 | 1000 | 0.25 | 0.25 |
2 | 10000 | 25.6 | 25 |
2 | 100000 | 2584.4 | 2500 |
表12 优化前不同规模效率(3个鸡蛋)
鸡蛋个数 | 楼层数 | 测试运行时间(s) | 理论运行时间(s) |
3 | 100 | 0.006 | 0.005 |
3 | 1000 | 0.5 | 0.5 |
3 | 10000 | 51.26 | 50 |
3 | 100000 | 5220 | 5000 |
由不同规模的时间效率及其时间复杂度O(K*N^2)可推断出其他运行规模的时间效率。
(优化算法一优化后)
表13 优化后不同规模效率(2个鸡蛋)
鸡蛋个数 | 楼层数 | 测试运行时间(s) | 理论运行时间(s) |
2 | 10000 | 0.001 | 0.00132 |
2 | 100000 | 0.016 | 0.0165 |
2 | 1000000 | 0.198 | 0.198 |
2 | 10000000 | 2.125 | 2.31 |
2 | 100000000 | 25.956 | 26.4 |
2 | 1000000000 | 343.923 | 297 |
由不同规模的时间效率及其时间复杂度O(K*N*logN)可推断出其他运行规模的时间效率。
(优化算法二)
优化算法二的时间复杂度只有KN,所以具有极好的效率优势,可以处理很大规模的鸡蛋掉落问题。
三.实验心得
本次实验我研究解决了鸡蛋掉落问题,利用动态规划解决该问题,动态规划的核心在于通过把原问题分解为相对简单的子问题的方式求解复杂问题的方法。动态规划常常适用于有重叠子问题和最优子结构性质的问题。该方法在许多问题求解中展现了特别的优势。在实验过程中,由于时间复杂度较高,我还找到了两种优化方法,实验证明,两种算法优化具有很好的效果。