我们看一道2022 CCF CSP-J1的阅读程序题:
一、题目
#include<algorithm>
#include<iostream>
#include<limits>
using namespace std;
const int MAXN = 105;
const int MAXK = 105;
int h[MAXN][MAXK];
int f(int n, int m) {
if (m == 1)
return n;
if (n == 0)
return 0;
int ret = numeric_limits<int>::max();
for (int i = 1; i <= n; i++)
ret = min(ret, max(f(n - i, m), f(i - 1, m - 1)) + 1);
return ret;
}
int g(int n, int m) {
for (int i = 1; i <= n; i++)
h[i][1] = i;
for (int j = 1; j <= m; j++)
h[0][j] = 0;
for (int i = 1; i <= n; i++) {
for (int j = 2; j <= m; j++) {
h[i][j] = numeric_limits<int>::max();
for (int k = 1; k <= i; k++)
h[i][j] = min(h[i][j], max(h[i - k][j], h[k - 1][j - 1]) + 1);
}
}
return h[n][m];
}
int main() {
int n, m; //假设n、m是不超过100的正整数
cin >> n >> m;
cout << f(n, m) << endl;
cout << g(n, m) << endl;
return 0;
}
这段代码实现了一个经典的动态规划问题,通常称为“鸡蛋掉落问题”或“高楼扔鸡蛋问题”。问题描述如下:
假设你有 mmm 个鸡蛋和一栋 nnn 层的建筑。你需要找到一种策略,确定从哪一层开始丢鸡蛋,使得在最坏情况下、鸡蛋碎掉的情况下需要的最少实验次数是最小的。
鸡蛋会在某一层碎掉,碎掉的层数以下的所有层鸡蛋都不会碎,但碎掉的层数以上的所有层鸡蛋都会碎。目标是找到这一层,或者知道所有鸡蛋不会碎。
核心问题
假设你有 mmm 个鸡蛋和 nnn 层建筑,问题是找到一个策略,使得无论鸡蛋从哪个楼层摔碎,你所需的实验次数在最坏情况下是最小的。
代码解释
全局变量
const int MAXN = 105;
const int MAXK = 105;
int h[MAXN][MAXK];
MAXN
和MAXK
定义了 nnn 和 mmm 的最大值为 105。h
是一个二维数组,用来存储动态规划的中间结果。
函数 f
的解释
int f(int n, int m) {
if (m == 1)
return n;
if (n == 0)
return 0;
int ret = numeric_limits<int>::max();
for (int i = 1; i <= n; i++)
ret = min(ret, max(f(n - i, m), f(i - 1, m - 1)) + 1);
return ret;
}
-
基本情况:
m == 1
:如果只剩下一个鸡蛋,你需要从 111 层到 nnn 层依次尝试,因此最坏情况下最多需要 nnn 次尝试。n == 0
: 如果没有层数,则不需要尝试,返回 000。
-
递归计算:
- 对于每一层
i
(从 1 到 nnn),你有两种情况:- 鸡蛋从第
i
层掉下去,没有碎,那么问题变成在剩下的 n−in - in−i 层建筑和 mmm 个鸡蛋下的情况。 - 鸡蛋从第
i
层掉下去,碎了,那么问题变成 i−1i - 1i−1 层和 m−1m - 1m−1 个鸡蛋的情况。
- 鸡蛋从第
- 取这两种情况下的最大值(即最坏情况),并对所有可能的
i
取最小值,以找到最佳的层数 iii,使得最坏情况下需要的尝试次数最少。
- 对于每一层
函数 g
的解释
int g(int n, int m) {
for (int i = 1; i <= n; i++)
h[i][1] = i;
for (int j = 1; j <= m; j++)
h[0][j] = 0;
for (int i = 1; i <= n; i++) {
for (int j = 2; j <= m; j++) {
h[i][j] = numeric_limits<int>::max();
for (int k = 1; k <= i; k++)
h[i][j] = min(h[i][j], max(h[i - k][j], h[k - 1][j - 1]) + 1);
}
}
return h[n][m];
}
- 初始化:
h[i][1] = i
表示当只有一个鸡蛋时,最坏情况下需要尝试 iii 次。h[0][j] = 0
表示当没有层数时,不需要尝试。
- 动态规划计算:
- 外层循环遍历所有可能的楼层数 iii 和鸡蛋数 jjj 。
- 内层循环遍历在第
k
层扔鸡蛋的情况,更新h[i][j]
为在 jjj 个鸡蛋和 iii 层建筑下所需的最少尝试次数。
- 返回值:
- 最后返回
h[n][m]
,即在 nnn 层建筑和 mmm 个鸡蛋下的最小尝试次数。
- 最后返回
主函数 main
的解释
int main() {
int n, m;
cin >> n >> m;
cout << f(n, m) << endl;
cout << g(n, m) << endl;
return 0;
}
main
函数从标准输入中读取n
和m
的值,然后调用f(n, m)
和g(n, m)
,分别输出这两个函数的结果。
小结
f(n, m)
使用递归的方式解决问题,而g(n, m)
使用动态规划来解决问题。两者的核心思想类似,都是在找出最优的扔鸡蛋策略,但动态规划的g
通常会比递归的f
更高效,因为它避免了重复计算。
二、示例分析
假定 n = 10
和 m = 2
时,你可以用 f
或 g
函数来求解,计算结果是相同的。让我们手动分析这个情况。
示例
n = 10
表示有 10 层楼。m = 2
表示有 2 个鸡蛋。
目标是找到一个策略,确定从哪一层开始扔鸡蛋,以使得在最坏情况下,鸡蛋碎掉的情况下需要的最少实验次数是最小的。
解法
对于 2 个鸡蛋和 10 层楼,最佳策略是如下:
- 首先,从第 4 层开始扔鸡蛋。如果鸡蛋碎了,你还有 1 个鸡蛋,问题就变成了在前 3 层楼内查找鸡蛋会碎的最高楼层。这最多需要 3 次尝试(分别尝试第 1 层、第 2 层和第 3 层)。
- 如果第 4 层鸡蛋没有碎,那么你接下来去第 7 层扔鸡蛋。如果碎了,你还有 1 个鸡蛋,问题就变成了在第 5 层到第 6 层内查找,这最多需要 2 次尝试。
- 如果第 7 层鸡蛋没有碎,那么你去第 9 层扔鸡蛋。如果碎了,你还有 1 个鸡蛋,需要在第 8 层检查(1 次)。
- 如果第 9 层鸡蛋没有碎,最后就尝试第 10 层。
这意味着最坏情况下需要的尝试次数为 4。
小结
因此,n = 10
和 m = 2
时的解是 4。
三、是否存在一个递推公式?
我们可能会有一个问题:能否求出一个关于n和m的递推公式,从而得到问题的解?
对于给定的鸡蛋数 mmm 和楼