使用递归和动态规划求解“鸡蛋掉落问题”

我们看一道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];
  • MAXNMAXK 定义了 nnnmmm 的最大值为 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 - ini 层建筑和 mmm 个鸡蛋下的情况。
      • 鸡蛋从第 i 层掉下去,碎了,那么问题变成 i−1i - 1i1 层和 m−1m - 1m1 个鸡蛋的情况。
    • 取这两种情况下的最大值(即最坏情况),并对所有可能的 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 函数从标准输入中读取 nm 的值,然后调用 f(n, m)g(n, m),分别输出这两个函数的结果。

小结

  • f(n, m) 使用递归的方式解决问题,而 g(n, m) 使用动态规划来解决问题。两者的核心思想类似,都是在找出最优的扔鸡蛋策略,但动态规划的 g 通常会比递归的 f 更高效,因为它避免了重复计算。

二、示例分析

假定 n = 10 m = 2 时,你可以用 fg 函数来求解,计算结果是相同的。让我们手动分析这个情况。

示例

  • 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 = 10m = 2 时的解是 4

三、是否存在一个递推公式?

我们可能会有一个问题:能否求出一个关于n和m的递推公式,从而得到问题的解?

对于给定的鸡蛋数 mmm 和楼

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值