Google面試題之100層仍兩個棋子


一道Google面試題,題目如下:“有一個100層高的大廈,你手中有兩個相同的玻璃圍棋子。從這個大廈的某一層扔下圍棋子就會碎,用你手中的這兩個玻璃圍棋子,找出一個最優的策略,來得知那個臨界層面。“

版本一:

  為了得到兩個棋子的最優策略,我們先簡化問題,看看一個棋子的情況。如果手中只有一個棋子,為了得知臨界層面,你只有一種選擇:從2樓開始,一層一層地試,直到棋子被打碎,此時你站的樓層就是所求的臨界層面。在最差的情況下,我們需要投擲99-2+1=98次,你可能奇怪為什么不是100-2+1=99次,那是因為題目已經告訴我們“從這個大廈的某一層扔下圍棋子就會碎”,所以在99層扔下來還沒碎的話就不用去100層了——從那里扔它一定會碎。

  從一個棋子的策略我們可以看出,一個棋子就足以解答這個問題了。現在又多了一個棋子,該如何利用它呢很自然地,我們希望能通過這個棋子縮小這種一層一層查找的范圍。為了縮小范圍,我們將整個大廈的層數分成x段,在這x段中查找那個臨界段,然后在臨界段中再一層一層地找臨界層。比如可以將大樓分成4段,我們分別在25層、50層、75層投擲棋子,以確定臨界段;如果臨界段在25層到50層,我們再從26層開始一層一層查找臨界層。

  分析到這里,問題就轉化成了如何確定分段數x使棋子投擲的次數最少的問題。在最差的情況下,要確定臨界段,我們需要投擲100/x-1次;確定了臨界段之后要確定臨界層,我們需要再投擲x-1次。因此,問題就成了求函數f(x)=(100/x-1)+(x-1)的最小值問題。先對f(x)求導,f’(x)=1-100/x2,令f’(x)=0求出駐點x=10(x=-10舍去)。由於f(x)存在最小值且只有一個駐點,所以當x=10f(x)取得最小值,最小值為18。這樣就解答了這個問題。(不等式就可以求了)

  其實10這個結果也很容易直接看出來。在只有一個棋子時,我們相當於把整個大廈分成了一段,這一段有100層。在有兩個棋子時,我們有很多分法,但無論怎么分,如果分成k1段,每段有k2層,那么就有k1k2=100。在最壞的情況下,我們需要投擲(k1-1)+(k2-1)次。因此問題也可以表述成在k1k2=100的條件約束下,如何讓函數f(k1,k2)= k1+k2最小。在初等數學中,我們知道在矩形面積一定的情況下,正方形的周長最小。利用這個結論,我們可以直接得出結論k1=k2=10

  現在問題已經完滿解決,但我還想把這個問題擴展一下,把它變成“m層樓n個棋子”的情況。首先來看這樣一個問題,給定m層樓,多少個棋子就“足夠”了,也就是說,再多的棋子也不能加快查找的過程。在我所能想到的方法里,二分法應該是最優的,如果按二分法來查找,則需要ceiling(log2m)個棋子(ceiling是向上取整函數),超過這個數再多的棋子也無益。

  如果n>=ceiling(log2m),那就采用二分法,現在考慮n< ceiling(log2m)的情況。前面已經看到,當n=2時,問題可以表述成在k1k2=100的條件約束下,求函數f(k1,k2)= k1+k2的最小值。類似地,在n個棋子的情況下,問題可以表述成在k1k2…kn=m的條件約束下,求函數f(k1,k2,…,kn)=k1+k2+…+kn的最小值。利用拉格朗日乘數法,我們可以很容易地求出:當k1=k2=…=kn=nm時,這個多元函數取得最值。nm有可能不是整數,因此這只是一個理論上的結果。

  我們換一個思路考慮,m層樓n個棋子的問題其實就是如何將m分解成n個因子相乘,從而讓各個因子之和最小。如何分解m使得策略最優就成了問題的關鍵。前面得出的結論提示我們盡量讓各個因子相等或者相差較小,它們相加的結果才會較小。比如,100層樓3個棋子的情況,554應該是一個最優的選擇。

  考慮到這里,又有一個問題出現了:是不是m分解的越多越好呢?比如,將100分解成1010好呢,還是2510好?這個問題其實就是在問,兩個大於1的整數,它們的和大呢還是積大。很明顯,當然是積大,因此將m分解的越多越好。

  數論告訴我們,質數是整數的基礎,所有整數都可以分解成若干個質數的乘積。因此,如果將上面的方法發揮到極致,那就要求我們把m分解成質數的乘積。當然,如果棋子足夠多,這並不是最優的方法,對質數層樓的段,你仍然可以采用二分法。

------------------------------------------------------------------------------------------------------

  上文貼出之后,我又在CSDN和ChinaUnix的論壇看了一些網友的解法,發現上述方法並非最優。將大樓分段以縮小查找范圍的想法是沒錯的,問題在於是否應該均勻分段。

  題目要求我們總的投擲次數要最少。在分段之后,總的投擲次數就等於確定臨近段的次數加上確定臨界層的次數。如果我們均勻分段,則確定臨界層的最壞投擲數是固 定的(9次),隨着我們確定臨近段的投擲次數增加,總的投擲次數也在增加。這樣一來,隨着臨界段的不同,投擲次數也不同。

  這也就是為什么上述方法不是最優的原因:投擲次數分布不均。按最壞情況估計,這種方法就多做了幾次。為了使最壞情況的投擲數最小,我們希望無論臨界段在哪里,總的投擲數都不變,也就是說將投擲數均勻分布。

  接下來的解決方案就很容易想出了既然第一步(確定臨界段)的投擲數增加不可避免,我們就讓第二步(確定臨界層)的投擲數隨着第一步的次數增加而減少。第一步的投擲數是一次一次增加的,那就讓第二步的投擲數一次一次減少。假設第一次投擲的層數是f,轉化成數學模型,就是要求f+(f-1)+...+2+1>=99,即f(f+1)/2>=99,解出結果等於14。

  這種方法要推廣到n(n>2)個棋子的情況比較困難。我初步的想法是,先用均勻分段求出一個解,然后修正這個解使投擲次數均勻分布。如果你對此有興趣,不妨思考一下具體的解法。

版本二:

  如果是兩個玻璃球,最少次數m確定樓高為N的哪一層開始能使這個玻璃球摔碎這個問題,等價於求最小的m,使得 1+2+...+m >= N 。
假設N正好等於1+2+...+m,那么我覺得最有的策略就是第一個玻璃球扔在第m層,如果碎了,顯然需要剩下的m-1層從底往上一一嘗試,最壞情況就是m;假設m處沒有碎,問題等價於樓高N'=1+2+...+(m-1)的地方同樣的問題需要的次數m'+1 (1就是第一次在m層的嘗試),根據我們的遞歸,容易得到N'對應需要的次數正好是m-1次,因此總次數也是m。
  我們的二分應該傾向於不管失敗還是成功,兩種情況的總檢測次數相等。因此這應該是最優的算法。
  當然,當N不能表示成1+2+...+m使,我們只能找最小的m作為需要測試的次數。
  至於100層樓,顯然m=14,我們的第一次扔球應該分別在第14, 如果沒碎繼續在14+13=27,再沒有碎則扔在第27+12=39層,以此類推。最壞需要14次判斷。

版本三:

題目的意思應該是
找出一個固定的方案A, 使得對於目標t為任意[1,100]里的數的時候, 找出t要扔的最多的次數 最小
因為t不同的時候, 方案A要扔的次數不一定一樣, 我們就是要使得這個最多的次數最小
動態規划就好了

F(l,r,k)為 確定目標t是否在[l,r]中且手上有k個棋子的最少扔子次數, 有狀態轉移方程:
             /      (r-l+1)             (k = 1)
F(l,r,k) =   |      1                   (l = r)
             |      0                   (l > r)
             \      1+min{max{F(l,mid-1,k-1),F(mid+1,r,k)}}    (l<=mid<=r)
寫了程序算了一下,是14
#include "stdio.h"
#include "string.h"

const int INF = 200000000;
const int maxN = 110;

int f[maxN][maxN][maxN];
int n, k;

int
find(int l, int r, int k)
{
        if (l > r)
        {
                return 0;
        }
        if (k == 1)
        {
                return r - l + 1;
        }
        if (l == r)
        {
                return 1;
        }
        if (f[l][r][k] != -1)
        {
                return f[l][r][k];
        }
        int mid;
        int left, right, t;
        f[l][r][k] = INF;
        for (mid = l; mid <= r; ++mid)
        {
                left = find(l, mid - 1, k - 1);
                right = find(mid + 1, r, k);
                t = left > right ? left : right;
                f[l][r][k] <?= t;
        }
        return ++f[l][r][k];
}

int
main()
{
        memset(f, -1, sizeof(f));
        while (scanf("%d %d", &n, &k) == 2)
        {
                printf("%d\n", find(1, n, k));
        }
        return 0;
}


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM