[dp]雙蛋問題&&李永樂老師視頻

一、背景

今天看李永樂老師的科普視頻,裏面提到的雙蛋問題很有意思。思考了下發現是動態規劃問題,特此記錄。李老師的視頻視頻點擊這裏裏講解的很清楚,在此寫下自己的理解。

二、題意

假設有一座大樓,共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即可),再跟鯊魚繞圈使得鯊魚-圓心-人在同一直線上,之後再徑直朝岸邊跑去。這樣就不會被抓到。


如有問題請留言。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章