某幢大樓有100層。你手裏有兩顆一模一樣的玻璃珠。當你拿着玻璃珠在某一層往下扔的時候,一定會有兩個結果,玻璃珠碎了或者沒碎。這幢大樓有個臨界樓層。低於它的樓層,往下扔玻璃珠,玻璃珠不會碎,等於或高於它的樓層,扔下玻璃珠,玻璃珠一定會碎。玻璃珠碎了就不能再扔。現在讓你設計一種方式,使得在該方式下,最壞的情況扔的次數比其他任何方式最壞的次數都少。也就是設計一種最有效的方式。
解決方案1(一個非常容易理解的方案,但是時間複雜度較高)
- 算法分析
最容易理解的方法是遞歸。
當樓層層高爲n時,我們使用第一個玻璃珠檢查第i層是否能摔碎。
- 如果摔碎了,我們只好謹慎的使用第二個玻璃珠,從第1層開始檢查,直到第i-1層。共檢查了1+(i-1)次。
- 如果木有摔碎,我們需要對上面的(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)。