实验目的
(1)掌握动态规划算法设计思想。
(2)掌握鸡蛋坠落问题的动态规划解法。
实验内容与结果
动态规划:
将问题划分为更小的子问题,通过子问题的最优解来重构原问题的最优解。动态规划中的子问题的最优解存储在一些数据结构中,这样我们就不必在再次需要时重新处理它们。任何重复调用相同输入的递归解决方案,我们都可以使用动态规划对其进行优化。
鸡蛋掉落问题:
用鸡蛋确认在多高的楼层鸡蛋落下来会破碎,这个恰好使鸡蛋破碎的楼层叫门槛层。门槛楼层以下的任何楼层掉落的鸡蛋不会破碎。给定建筑物的一定数量的楼层(比如 f 层)和一定数量的鸡蛋(比如 e 鸡蛋),找出门槛层必须执行的最少的鸡蛋掉落试验的次数。
问题约束条件:
- 从跌落中幸存下来的鸡蛋可以再次使用。
- 破蛋必须丢弃。
- 摔碎对所有鸡蛋的影响都是一样的。
- 如果一个鸡蛋掉在地上摔碎了,那么它从高处掉下来也会摔碎。
- 如果一个鸡蛋在跌落中幸存下来,那么它在较短的跌落中也能完整保留下来。
实验要求:
1、给出解决问题的动态规划方程;
2、随机产生f,e的值,对小数据模型利用蛮力法测试算法的正确性;
3、随机产生f ,e的值,对不同数据规模测试算法效率,并与理论效率进行比对,请提供能处理的数据最大规模,注意要在有限时间内处理完;
4、该算法是否有效率提高的空间?包括空间效率和时间效率。
实验步骤:
1、给出解决问题的动态规划方程;
状态转移方程:dp[i][j] = 1 + min(max(dp[x-1][j-1], dp[i-x][j])),其中x的取值范围为[1, i]
状态转移方程的含义是,在确定dp[i][j]时,需要选择一个最优的x值,使得在两种情况下所需的尝试次数最小化。
鸡蛋破碎:在第 x 层扔鸡蛋,鸡蛋破碎,需要在[1, x-1]的楼层范围内继续使用j-1个鸡蛋进行尝试,即dp[x-1][j-1]。
鸡蛋未破碎:在第 x 层扔鸡蛋,鸡蛋未破碎,需要在[x+1, i]的楼层范围内继续使用j个鸡蛋进行尝试,即dp[i-x][j]。
在这两种情况中,选择使得尝试次数最大的情况,并在所有可能的x值中取最小值。
2、随机产生f,e的值,对小数据模型利用蛮力法测试算法的正确性;
蛮力法思想:从第K层向上进行尝试,如果鸡蛋碎了,则向下尝试;若没碎,则向上尝试。
核心伪代码如下
可以看出,蛮力法需要尝试的次数是非常多的,当数据规模较大时,非常耗时。
3、随机产生f ,e的值,对不同数据规模测试算法效率,并与理论效率进行比对,请提供能处理的数据最大规模,注意要在有限时间内处理完;
核心伪代码如下
对于不同的数据规模,取十组数据耗时的平均值为最终耗时。
在动态规划算法中,由于对前面运算的结果会进行存储,所以会极大的提高后面的数据的运算效率。如下图所示,第3组和第4组甚至不需要计算就直接得到了结果。
运算结果如下
当数据规模达到5000时,很长时间都未有结果。
算法复杂度分析:
1. 填充dp数组的循环嵌套:
- 外层循环从1到N,共N次迭代。
- 内层循环从2到K,共K-1次迭代。
- 内部还有一个循环从1到i,共i次迭代。
- 总迭代次数为 ∑(i=1 to N) ∑(j=2 to K) i = N * (K-1) * (N+1) / 2。
2. 总体复杂度:
- 时间复杂度:O(N * K * (N+1)/2),近似为 O(N^2 * K)。
- 空间复杂度:O(N * K),需要使用二维数组dp[N+1][K+1]存储结果。
当N和K较小时,算法的时间复杂度还在可接受的范围内。然而,如果N和K的值非常大,可能会导致较长的运行时间和较大的空间开销。因此,在实际应用中,需要对算法进行进一步优化,以降低时间和空间复杂度。
表格如下
绘图如下
由图可知,理论时间和实际时间基本吻合,说明时间复杂度分析正确。之所以实际时间会大于理论时间,一个原因可能是作为基准的首个点存在误差,另一个原因时间复杂度在近似时减少了些许。
4、该算法是否有效率提高的空间?包括空间效率和时间效率。
为了优化鸡蛋掉落问题的动态规划算法以降低时间和空间复杂度,可以采用以下两种常见的优化方法:
1. 二分查找优化:
在填充dp数组时,可以使用二分查找来寻找最佳的尝试楼层。
对于固定的i和j,假设我们选择在第x层进行尝试(1 <= x <= i)。
如果鸡蛋在第x层破碎了,那么剩下的问题转化为在前x-1层楼使用j-1个鸡蛋的子问题,即dp[x-1][j-1]。
如果鸡蛋在第x层没有破碎,那么剩下的问题转化为在第x层及以上楼层使用j个鸡蛋的子问题,即dp[i][j] = dp[i-x][j]。
因此,在当前层x的两种情况下,尝试次数的最大值为1 + max(dp[x-1][j-1], dp[i-x][j])。
通过二分查找在区间[1, i]内找到使得尝试次数最小的x值,可以减少内层循环的次数。
2. 空间优化:
在原始的动态规划解法中,使用了一个二维数组dp[N+1][K+1]来存储结果,占用了O(N * K)的空间。
可以进行空间优化,仅使用两个一维数组dp1[K+1]和dp2[K+1]来交替存储状态,减少空间占用。
在状态转移过程中,当前状态仅依赖于上一层的状态,因此只需要两个数组来保存当前层和上一层的状态即可。
通过应用这些优化方法,可以显著降低算法的时间和空间复杂度,提高算法的效率。
实验总结
蛮力法是非常耗时,效率非常低的。
动态规划法将问题划分为更小的子问题,通过子问题的最优解来重构原问题的最优解。
动态规划法可以存储之前的计算结果,可以极大的提高后面数据的运算效率。
对于一些问题,可以将大问题转为若干个子问题,并利用动态规划思想进行运算可以很大程度上降低运行时间。
实验伪代码
eggDropbao(N,K) //暴力法
attempt = 0,
breakfloor = N
for i=K~N
attempt++
if i=breakfloor
return attempt
eggDropdp(N,K) //动态规划法
for i=1~N //更新dp数组
for j=1~K
if dp[i][j] != INT_MAX
continue
for x=1~i
attempts = 1 + max(dp[x - 1][j - 1], dp[i - x][j])
dp[i][j] = min(dp[i][j], attempts)
if N > n //更新n,k值
n = N
if K > k
k = K
return dp[N][K]
实验代码
#include <iostream>
#include <cstdlib>
#include <ctime>
#include <algorithm>
using namespace std;
#define LIMIT 4000 //N,K上限
int **dp;
int n=0, k=0; //记录已填到的n,k值
void firstsetdp()
{
dp = new int* [LIMIT + 1];
for (int i = 0; i <= LIMIT; i++) //动态创建数组并初始化
{
dp[i] = new int[LIMIT + 1];
for (int j = 0; j <= LIMIT; j++)
dp[i][j] = INT_MAX;
}
for (int i = 0; i <= LIMIT; i++)
{
dp[i][0] = 0; // 0个鸡蛋时,尝试次数为0
dp[i][1] = i; // 1个鸡蛋时,逐层尝试
}
for (int j = 1; j <= LIMIT; j++)
{
dp[0][j] = 0; // 0层楼时,不需要尝试
dp[1][j] = 1; // 1层楼时,不需要尝试
}
}
int eggDropdp(int N, int K)
{
//更新dp数组
for (int i = 1; i <= N; i++)
for (int j = 1; j <= K; j++)
{
if (dp[i][j] != INT_MAX)
continue;
for (int x = 1; x <= i; x++)
{
int attempts = 1 + max(dp[x - 1][j - 1], dp[i - x][j]);
dp[i][j] = min(dp[i][j], attempts);
}
}
//更新n,k值
if (N > n)
n = N;
if (K > k)
k = K;
return dp[N][K];
}
int eggDropbao(int N, int K)
{
int attempt = 0, breakfloor = N;
for (int i = K; i <= N; i++)
{
attempt++;
if (i == breakfloor)
return attempt;
}
}
int main()
{
srand(time(0));
int k = 1; //记录第几组N,K值
firstsetdp();
int shi = 0;
int zushu = 10;
for(int q=1;q<=zushu;q++)
{
// 随机生成楼层数N和鸡蛋个数K
int N=0, K=0;
while (N == 0 || K == 0)
{
N = rand() % LIMIT;
if(N!=0)
K = rand() % N;
}
cout << "当前组数为:" << q<< endl;
cout << "楼层数N:" << N << endl;
cout << "鸡蛋个数K:" << K << endl;
int qi = clock();
int minAttempts;
//minAttempts = eggDropbao(N, K); //暴力法
minAttempts = eggDropdp(N, K); //动态规划法
int zhi = clock();
shi += zhi - qi;
cout << "最少尝试次数:" << minAttempts << endl;
cout << "耗时:" << zhi - qi << " ms " << endl;
cout << endl;
}
cout << "数据规模:" << LIMIT << endl;
cout<< "平均耗时:" << shi/zushu << " ms " << endl;
}
(by 归忆)