[dp]双蛋问题&&李永乐老师视频

2022.10.1更新,此题目在leetcode.cn上有原题,并且有大量详细的题解,见 887. 鸡蛋掉落

一、背景

今天看李永乐老师的科普视频,里面提到的双蛋问题很有意思。思考了下发现是动态规划问题,特此记录。李老师的视频视频点击这里里讲解的很清楚,在此写下自己的理解。

二、题意

假设有一座大楼,共T层(T>=1),你有N个鸡蛋(N>=1),可以从任意楼层的窗户往下扔鸡蛋。并且存在一个边界楼层数X,当在小于X楼层扔鸡蛋时鸡蛋不会碎,在X及大于X楼层扔时鸡蛋会碎。请问至少需要扔几次鸡蛋才能保证确定X等于几?

三、解析

假设楼高T=100,鸡蛋数N=1。首先边界楼层是X随机的,我们只能从1楼开始挨个往上试,假如在第10层扔时没碎,而第11层扔时碎了那么可以确定X=11,但是也可能在第99层没碎在第100层才碎,所以至少需要扔100次。
假如楼高T=100,鸡蛋数为很多个,比如N=100。我们可以想到使用二分法求边界楼层X;这样至少需要扔lg100≈7次。
但是鸡蛋的个数并不能一定满足使用二分法的要求(因为鸡蛋会碎掉),那么我们可以使用动态规划的思想求得解。
(视频中还提到了一种思路用来确定最少需要扔鸡蛋的次数,但对鸡蛋个数有要求,也不是最优解,这里不再描述)。
假设dp[i][j] 表示i层楼,j个鸡蛋时最少扔鸡蛋的次数。
在只有1个鸡蛋时,显然只能挨着从1楼开始扔,然后再从2楼,3楼扔着试,那么dp[i][1] = i;
在楼只高1层(平房)时,显然扔一次就可以确定,那么dp[1][j] = 1(j>=1);
这样我们就得到了状态转移的初始状态。
在求dp[i][j]时,我们模拟扔鸡蛋的过程,首先第一个鸡蛋可以在k(k>=1,k<=i)层扔下去,这样第一个鸡蛋有两种结果,碎和不碎。
假如碎了,说明边界楼层在k之下,那么问题就变成了求在楼高k,有j-1个鸡蛋时的dp[k][j-1],即dp[i][j]=dp[k][j-1]+1。
假如没碎,说明边界楼层在k之上,那么问题就变成了求在楼高i-k,有j个鸡蛋时的dp[i-k][j],即dp[i][j]=dp[i-k][j]+1。
因为我们求得解要满足最坏的情况,所以dp[i][k]=max{dp[k][j-1]+1, dp[i-k][j]+1}。
这样就得到了状态转移方程。但是第一个鸡蛋到底在那一层扔最好我们还不知道,所以需要让k从1到i遍历。
所以最后得到状态转移方程为:
dp[i][j] = min{
max{dp[1][j-1]+1, dp[i-1][j]+1}//此时k=1,
max{dp[2][j-1]+1, dp[i-2][j]+1}//此时k=2,
max{dp[3][j-1]+1, dp[i-3][j]+1}//此时k=3,

max{dp[i][j-1]+1, dp[i-i][j]+1}//此时k=i
}

四代码

// Double egg

#include <iostream>
#include <vector>
int main() {
    int T, N;
    std::cin >> T >> N;
    std::vector<std::vector<int >> dp(T + 1, std::vector<int>(N + 1));
	// 初始化初始状态,注意dp[0][]和dp[][0]也要赋值为0
    for(int j=0; j<=N; j++){
        dp[0][j] = 0;
    }
    for(int i=0; i<=T; i++){
        dp[i][0] = 0;
    }
    for (int j = 1; j <= N; j++) {
        dp[1][j] = 1;
    }
    for (int i = 1; i <= T; i++) {
        dp[i][1] = i;
    }
	// 动态规划求解
    for (int i = 2; i <= T; i++) {
        for (int j = 2; j <= N; j++) {
        
            // 这里求当第一个鸡蛋从k=1一直到k=i层扔时的情况
            std::vector<int> mk(i + 1);
            for (int k = 1; k <= i; k++) {
                mk[k] = std::max(1 + dp[k-1][j-1], 1+dp[i-k][j]);
            }
        	//     
            int min = mk[1];
            for(int k=1; k<=i; k++){
                min = std::min(min, mk[k]);
            }
            dp[i][j] = min;
        }
    }
    
	// 输出
    for(int i=1; i<=T; i++){
        for(int j=1; j<=N; j++){
            std::cout<<dp[i][j]<<" ";
        }
        std::cout<<std::endl;
    }
    return 0;
}


ps:

另外李永乐老师在视频最后提出了一个课后题:
假如一个人在一个圆形小岛屿的中心,岛屿周围有一条鲨鱼,鲨鱼移动的速度是人速度的四倍,而且鲨鱼总是会游到距离人最近的位置。请问人应该如何移动才能在不被鲨鱼抓到的情况下到达岛屿岸边。
这里参考原视频评论区给出我的解答:
假设人的速度为v,那么鲨鱼的速度为4v。我们把岛屿简化成一个圆(大圆)半径为rbig,以大圆半径的1/4为半径画一个同心圆,小圆半径为rsmall
我们将人的移动分为两个阶段:第一个阶段先不知道怎么跑,但是要求最终结果是鲨鱼-圆心-人在同一直线上。
第二阶段人全力向岸边跑去。如果人不被鲨鱼抓到,那么第二阶段开始时要满足,人距离岸边d2/v <= pi * rbig/(4v)解得d2 <= pi * rbig/4,即不管第一阶段如何跑,只要使得第二阶段初始时鲨鱼-圆心-人在同一直线上,同时人距离岸边小于pi * rbig/4 ≈ 0.785rbig就可以不被抓到。
现在我们来求第一阶段人跑的轨迹:当人在小圆边缘上时,只要人绕着圆心转圈,人的角速度为wman=v/rsmall=v/(rbig/4)=4v/rbig,鲨鱼的角速度为wfish=4v/rbig=wman,即人的角速度等于鲨鱼的角速度。那么只要人在小圆之内,wman>wfish,不管鲨鱼怎么游人都能通过绕着圆心转圈最终使得鲨鱼-圆心-人在一条直线上。
所以第一阶段人可以先超任意方向移动rbig-d2=rbig-pi * rbig/4≈(1-3.14/4) * rbig=0.215rbig再移动一点点比如0.216rbig,此时人距离圆心为0.216rbig<rbig/4,即人在小圆里,同时距离岸边为rbig-0.216rbig=0.784rbig小于0.785rbig,不管此时鲨鱼在什么位置都可以通过绕着圆心转圈的方式使得鲨鱼-圆心-人在一条直线上,这样第一阶段结束人与鲨鱼的位置满足第二阶段的初始要求。
综上,人可以先往任意方向径直移动0.216rbig(只要满足大于1 - pi * rbig/4,小于0.25rbig即可),再跟鲨鱼绕圈使得鲨鱼-圆心-人在同一直线上,之后再径直朝岸边跑去。这样就不会被抓到。


如有问题请留言。

posted on 2020-03-15 11:57  刘好念  阅读(36)  评论(0编辑  收藏  举报  来源