【算法】一個小算法的非遞歸方式的兩種實現

某幢大樓有100層。你手裏有兩顆一模一樣的玻璃珠。當你拿着玻璃珠在某一層往下扔的時候,一定會有兩個結果,玻璃珠碎了或者沒碎。這幢大樓有個臨界樓層。低於它的樓層,往下扔玻璃珠,玻璃珠不會碎,等於或高於它的樓層,扔下玻璃珠,玻璃珠一定會碎。玻璃珠碎了就不能再扔。現在讓你設計一種方式,使得在該方式下,最壞的情況扔的次數比其他任何方式最壞的次數都少。也就是設計一種最有效的方式。


解決方案1(一個非常容易理解的方案,但是時間複雜度較高)

  • 算法分析

最容易理解的方法是遞歸。


當樓層層高爲n時,我們使用第一個玻璃珠檢查第i層是否能摔碎。
  1. 如果摔碎了,我們只好謹慎的使用第二個玻璃珠,從第1層開始檢查,直到第i-1層。共檢查了1+(i-1)次。
  2. 如果木有摔碎,我們需要對上面的(n-i)層進行檢查,此時與檢查一個層高爲(n-i)的樓的方法是一樣的。我們假設對上面的(n-i)層的檢查的最佳方法是P(n-i)。共檢查了1+P(n-i)次。

因此從i層開始檢查的這種方式的最差情況是1+(i-1)與1+P(n-i)較差的那個(次數較多的那個)。


我們 依次令i=1,2,…n,可以得到一個最差情況下檢查次數最少的i。這個i就是最佳情況中第一次檢查的樓層。

以此類推,直到第一個玻璃球摔碎或檢查到只剩一層或兩層爲止。我們就可以得出最佳情況下每次檢查的樓層,以及總共檢查的次數。


得出遞歸式:

對於n=1或2, P(n)=1

當n>2, P(n)=min[ max(1,1+P(n-1)), max(2,1+P(n-2)), …, max((n-1), 1+P(1)) ]


當然,我們可以通過遞歸式來編寫遞歸的方法求出層高爲100時的最佳方法。但是明顯這種方法會重複對某種層高進行計算。爲了減少時間複雜度,我們開闢了O(n)的空間,將每種層高的最佳方式從小到大先計算出來存儲在數組中。然後在需要使用遞歸調用的地方直接取數組中的值進行計算即可。

  • 算法實現
public class GlassBallAlgorithm {  
    public static void main(String[] args) {  
        GlassBallAlgorithm glassBallAlgorithm=new GlassBallAlgorithm();  
        glassBallAlgorithm.FindBestWays();  
    }  
    //樓層層高  
    static final int n=100;  
    //層高爲index(1,2...n)時,最佳方法的最壞試驗次數  
    static int[] maxTime=new int[n+1];//爲了方便計算,從index=1開始存儲  
    //層高爲index(1,2...n)時,最佳方法中第一個檢查的樓層  
    static int[] bestLayer=new int[n+1];//爲了方便計算,從index=1開始存儲  
    //爲每種層高找到最佳方法  
    void FindBestWays(){  
        for(int i=1;i<maxTime.length;i++){  
            maxCount(i);  
        }  
        print();
    }  
    //計算最高位n層的樓的最佳檢查方法  
    void maxCount(int n)  
    {  
        if(n<=2){  
            maxTime[n]=n;//這裏我們假設有可能100層都摔不壞,因此當n=2時,如果檢查了其中的一層,還需要檢查另一層  
            bestLayer[n]=1;  
        }  
        else{  
            maxTime[n]=n;//最佳方法的最大檢查次數  
            for(int i=1;i<n;i++){  
                int longerSide=(1+maxTime[n-i]>i)?(1+maxTime[n-i]):i;  
                if(maxTime[n]>longerSide){  
                    maxTime[n]=longerSide;   
                    bestLayer[n]=i;//最佳方法中第一次檢查的層  
                }  
             }  
        }  
    }
    void print(){
        System.out.println("使用最佳方法在最壞情況下需要檢查"+maxTime[n]+"次。");  
        int floor=n;//最高層數  
        int i=0;//基數  
        //打印最好情況下的檢查過程  
        System.out.println("最佳方法在最壞情況下的檢查順序爲:");  
        while(floor>1){  
            System.out.println("=>檢查第"+(bestLayer[floor]+i)+"層");  
            i+=bestLayer[floor];  
            floor=floor-bestLayer[floor];  
        }  
        System.out.println("=>檢查第"+n+"層"); 
    }
} 

對於n=100,輸出的結果爲:

使用最佳方法在最壞情況下需要檢查14次。
最佳方法在最壞情況下的檢查順序爲:
=>檢查第9層
=>檢查第22層
=>檢查第34層
=>檢查第45層
=>檢查第55層
=>檢查第64層
=>檢查第72層
=>檢查第79層
=>檢查第85層
=>檢查第90層
=>檢查第94層
=>檢查第97層
=>檢查第99層
=>檢查第100層

  • 算法複雜度

空間複雜度爲O(n)

時間複雜度爲1+1+2+3+…+(n-2)+(n-1),明顯爲O(n^2)。


解決方案2

寫完方案1,查看了輸出結果,才發現自己2了=. =

其實算法可以很簡單。

我們假設最壞情況下的最佳檢查次數爲n。

  • 首先檢查第m1層(此時共檢查1次),m1必須小於等於n。(因爲如果m1層摔碎了第一個玻璃珠,那麼需要使用第二個玻璃珠依次檢驗1到m1之間的樓層,總檢查次數不能大於n)
  • 如果沒摔碎則向上檢查第m2層(此時共檢查了2次),此時m1到m2之間最多有n-2個樓層。(因爲如果m2層摔碎了第一個,那麼需要使用第二個玻璃球依次檢驗m1與m2之間的樓層,總檢查次數不能大於n)。
  • 以此類推,m2到m3之間最多有n-3個樓層...

如果將上面的思路反向思考。最好的解決方式就是:在最壞情況下,最後檢查第100層,倒數第二檢查第99層,第99-2層,第(99-2)-3層,第(99-2-3)-4層...

  • 算法實現
import java.util.ArrayList;  
public class EasySolution {  
    static final int n=100;  
    static ArrayList<Integer> selectedLayers=new ArrayList<Integer>();//反向存儲最壞情況下所有選擇檢查的樓層  
    static int count=0;//最壞情況下檢查的次數  
    public static void main(String[] args) {  
        EasyWay();  
    }  
    static void EasyWay(){  
        int m=n;  
        for(int i=0;m>0;i++,m-=i){  
            selectedLayers.add(m);  
            count++;  
        }  
        print();      
    }  
    static void print(){  
        //打印最優方案  
        System.out.println("最優方法在最差情況下需要檢查"+count+"次。");  
        System.out.println("最佳方法在最壞情況下的檢查順序爲:");    
        for(int i=selectedLayers.size()-1;i>=0;i--){  
            System.out.println("=>檢查第"+selectedLayers.get(i)+"層");      
        }    
    }  
}
  • 複雜度

這種方法時間空間複雜度都爲O(n)。



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