1. 程式人生 > >375. Guess Number Higher or Lower II (Python)

375. Guess Number Higher or Lower II (Python)

下一步 每次 ever 到你 寫代碼 however 思路 時序 簡單

375. Guess Number Higher or Lower II

Description

We are playing the Guess Game. The game is as follows:

I pick a number from 1 to n. You have to guess which number I picked.

Every time you guess wrong, I‘ll tell you whether the number I picked is higher or lower.

However, when you guess a particular number x, and you guess wrong, you pay $x. You win the game when you guess the number I picked.

Example:
n = 10, I pick 8.

First round:  You guess 5, I tell you that it‘s higher. You pay $5.
Second round: You guess 7, I tell you that it‘s higher. You pay $7.
Third round:  You guess 9, I tell you that it‘s lower. You pay $9.

Game over. 8 is the number I picked.

You end up paying $5 + $7 + $9 = $21.

Given a particular n ≥ 1, find out how much money you need to have to guarantee a win.

問題描述

我們正在玩一個猜數遊戲,遊戲規則如下:

我從 1 到 n 之間選擇一個數字,你來猜我選了哪個數字。

每次你猜錯了,我都會告訴你,我選的數字比你的大了或者小了。

然而,當你猜了數字 x 並且猜錯了的時候,你需要支付金額為 x 的現金。直到你猜到我選的數字,你才算贏得了這個遊戲。

示例:
n = 10, 我選擇了8.

第一輪: 你猜我選擇的數字是5,我會告訴你,我的數字更大一些,然後你需要支付5塊。
第二輪: 你猜是7,我告訴你,我的數字更大一些,你支付7塊。
第三輪: 你猜是9,我告訴你,我的數字更小一些,你支付9塊。

遊戲結束。8 就是我選的數字。

你最終要支付 5 + 7 + 9 = 21 塊錢。

給定一個 n ≥ 1,計算你至少需要擁有多少現金才能確保你能贏得這個遊戲。

問題分析

??剛開始看到這個題,感覺是個數學問題,可以直接計算得出。寫了很久都不AC,於是又老實寫算法了。從算法角度看,這個題就是很直白的暴力破解,中間用到了DP來減小運算復雜度,還包含了取最大值和最小值運算,所以不太好分類,但是思想還是能理明白。最重要的一步DP就是i+max(?[1,...,i-1]?,?[i+1,...,n]?)。這裏的[1,...,i-1]表示猜對這個短序列最少花的錢。
??下面舉例說明。

  • 當序列很短的時候,一眼就能看出來最少花多少錢。比如長度為2的短序列:

    1 2

    顯然猜1可以花錢最少,猜對不花錢,錯了花1塊,所以最多需要1塊錢。

    長度為3的短序列同理。比如:

    2 3 4
    顯然猜3可以花最少的錢,猜對不花錢,錯了花3塊,然後下一次一定可以猜對。所以最多需要3塊錢。
    對於這種短序列,我們很快可以得到答案,直接每個數試一試就能知道結果。具體這樣試,
    • 如果先猜2,錯了再猜3(知道3比4花錢少),那麽5塊錢可以保證一定猜對。
    • 如果先猜3,不管對錯,肯定能得到正確答案,那麽只需3塊錢。
    • 如果先猜4,錯了再猜2(知道2比3花錢少),那麽需要6塊錢。
    • 綜上,選最好的猜法,最少只要3塊錢即可。
      這其實是我們內心的真實邏輯,寫成代碼就簡單了,如果序列是[ i , i+1 ]和[ i-1 , i , i+1 ]這樣的,直接猜i就好了。
  • 對於中長序列,比如1到4的序列怎麽處理呢,其實邏輯一樣:

    1 2 3 4
    上面[ 2 , 3 , 4 ]的例子裏,猜2不對,我們知道該猜3而不是4。但對於這種長一點的序列,第一次沒猜對,我們也不容易看出第二次猜哪個數比較好。
    如果沒有策略,純暴力搜索把所有猜法都試一遍,復雜度是指數級的。於是我們可以在暴力搜索的基礎上將任務分解成一個個小任務,即用DP的思路,創建一個二維表格,記住小任務的解,最終得到大任務的解。
    比如前面[ 2 , 3 , 4 ]的例子裏思路也是一樣的。猜2不對,我們知道猜3而不是4,就是因為我們心裏默認知道[ 3 , 4 ]這個小任務只需要猜3就夠了,即[ 3 , 4 ]這個小任務只需要3塊錢而不是4塊錢。
    這裏也是同理:
    • 先猜1

      1 2 3 4
      那麽剩下的[ 2 , 3 , 4 ]猜什麽呢。上述已經知道這個序列猜3花費最少。那麽最終花費是1+3=4
    • 先猜2

      1 2 3 4
      要是大了,左邊剩一個數肯定是對的,那麽花費是0。要是小了,右邊的序列[ 3 , 4 ]花費3足矣。那麽最少需要花的錢就是2+max(左邊0,右邊3)=5。
      這裏有個取最大的操作,原因是為了把所有情況都考慮進去,這樣無論是哪一種情況,錢都是夠的
    • 先猜3

      1 2 3 4
      要是大了,左邊的序列[ 1 , 2 ]花費1足矣。要是小了,右邊只剩一個數,花費0。那麽這種情況下最少需要3+max(左邊1,右邊0)=4
    • 先猜4

      1 2 3 4
      剩下的[ 1 , 2 , 3 ]花費2,最終花費4+2=6。

      那麽最終至少要花的錢就是min(先猜1,先猜2,先猜3,先猜4)=min(4,5,4,6)=4 。
      這裏有個取最小值的操作,就是在所有暴力破解的方法中,找花錢最少的那種方法,這樣就能用最少的錢猜到正確數字
      另外可以看出,我們每次都把任務分解成左右兩個小任務,只要知道了小任務的最優值,就能求得大任務的最優值了。
  • 再舉一個長序列的例子說明任務分解,比如1到10:

    1 2 3 4 5 6 7 8 9 10
    做法和上述一樣,最外面的大循環直接for i in range(1,11)從頭試到尾,這樣把任務分成了兩個小任務,即[1,...,i-1]和[i+1,...,10],然後只要我們知道每個小任務的解,那麽先猜i的這個大任務的解就是i+max([1,...,i-1],[i+1,...,10])。
    • 比如我們先猜了6

      1 2 3 4 5 6 7 8 9 10
      任務被分解成1到5和7到10兩個小任務,如果我們已經用一個數組存儲了這兩個小任務的解,那麽這個大任務的解直接就是6+max([1,...,5],[7,...,10])
      所有1到10都試過之後,再取最小值,得到的就是花費最少的錢數了。即min( 先猜1花的錢 , ... , 先猜10花的錢 )。
  • 最後的問題是怎麽構造二維數組存儲小任務的解呢,也很簡單。這裏以1到6舉例,太長表格不好畫。

    1 2 3 4 5 6

    表格建立如下,記為T:

    1 2 3 4 5 6
    1
    2
    3
    4
    5
    6
    行代表任務序列的終點,列代表任務序列的起點(這裏行列表示含義互換也一樣,只是我自己寫代碼的時候這麽寫了懶得改了)。表格中的數代表猜對這個序列至少需要花多少錢。比如T[5][2]就表示從2到5,最少需要花多少錢可以猜對。我們的任務就是填滿這個表的下半部分。填滿之後,T[6][1]也就是我們要的解。那麽我們從頭到尾填一遍。
    註意:這裏我直接從1開始計數便於解釋說明,代碼的計數是從0開始的。
    • 首先初始化表格為0

      1 2 3 4 5 6
      1 0 0 0 0 0 0
      2 0 0 0 0 0 0
      3 0 0 0 0 0 0
      4 0 0 0 0 0 0
      5 0 0 0 0 0 0
      6 0 0 0 0 0 0
    • 任務終點為1的時候,此時序列為[ 1 ],不用花錢就能猜對,則有T[1][1]=0。
      終點為1填寫完畢。此時T沒變

      1 2 3 4 5 6
      1 0 0 0 0 0 0
      2 0 0 0 0 0 0
      3 0 0 0 0 0 0
      4 0 0 0 0 0 0
      5 0 0 0 0 0 0
      6 0 0 0 0 0 0
    • 任務終點為2的時候,
      任務起點先設為2,此時序列為[ 2 ],不用花錢,則有T[2][2]=0。
      下一步任務起點設為1,此時序列為[ 1 , 2 ],花1即可,則有T[2][1]=1。
      終點為2填寫完畢,此時T為

      1 2 3 4 5 6
      1 0 0 0 0 0 0
      2 1 0 0 0 0 0
      3 0 0 0 0 0 0
      4 0 0 0 0 0 0
      5 0 0 0 0 0 0
      6 0 0 0 0 0 0
    • 任務終點為3的時候,
      任務起點先設為3,此時序列為[ 3 ],不用花錢,則有T[3][3]=0。
      任務起點設為2,此時序列為[ 2 , 3 ],花錢2,則有T[3][2]=2。
      任務起點設為1,此時序列為[ 1 , 2 ,3 ],三個的短序列直接可得花錢為中間那個數,也就是2,則有T[3][1]=2。
      終點為3填寫完畢,此時T為

      1 2 3 4 5 6
      1 0 0 0 0 0 0
      2 1 0 0 0 0 0
      3 2 2 0 0 0 0
      4 0 0 0 0 0 0
      5 0 0 0 0 0 0
      6 0 0 0 0 0 0
    • 任務終點為4的時候,
      任務起點設為4,此時序列為[ 4 ],不花錢,T[4][4]=0。
      任務起點設為3,此時序列為[ 3 , 4 ],花錢3,則有T[4][3]=3。
      任務起點設為2,此時序列為[ 2 , 3 , 4 ],花錢3,則有T[4][2]=3。
      任務起點設為1,此時序列為[ 1 , 2 , 3 , 4 ],長度超過3了,要用中長序列的方式去計算。也就是前面舉例的方式,有T[4][1]=min(先猜1花的錢,...,先猜4花的錢)
      \[ \begin{array}{l} =min(1+[2,3,4] \ , \ 2+max([1],[3,4]) \ , \ 3+max([1,2],[4] \ , \ 4+[1,2,3])) \=min(1+T[4][2] \ , \ 2+max(T[1][1],T[4][3]) \ , \ 3+max(T[2][1],T[4][4]) \ , \ 4+T[3][1]) \=min(1+3 \ , \ 2+3 \ , \ 3+1 \ , \ 4+2) \=4 \end{array} \]
      即T[4][1]=4。
      終點為4填寫完畢,此時T為

      1 2 3 4 5 6
      1 0 0 0 0 0 0
      2 1 0 0 0 0 0
      3 2 2 0 0 0 0
      4 4 3 3 0 0 0
      5 0 0 0 0 0 0
      6 0 0 0 0 0 0
    • 任務終點為5的時候,
      任務起點設為5,此時序列為[ 5 ],不花錢,T[5][5]=0。
      任務起點設為4,此時序列為[ 4 , 5],花錢4,則有T[5][4]=4。
      任務起點設為3,此時序列為[ 3 , 4 ,5 ],花錢4,則有T[5][3]=4。
      任務起點設為2,此時序列為[ 2 , 3 , 4 ,5],有
      \[ \begin{array}{l} T[5][2] \=min(2+T[5][3],3+max(T[2][2],T[5][4]),4+max(T[3][2],T[5][5])) \=min(2+4,3+4,4+2) \=6 \end{array} \]
      任務起點設為1,此時序列為[ 1 , 2 , 3 , 4 , 5 ],有
      \[ \begin{array}{l} T[5][1] \=min(1+T[5][2],2+max(T[1][1],T[5][3]),3+max(T[2][1],T[5][4]),4+max(T[3][1],T[5][5]),5+T[4][1]) \=min(1+6,2+4,3+4,4+2,5+4) \=6 \end{array} \]
    • 終點為5填寫完畢,此時T為

      1 2 3 4 5 6
      1 0 0 0 0 0 0
      2 1 0 0 0 0 0
      3 2 2 0 0 0 0
      4 4 3 3 0 0 0
      5 6 6 4 4 0 0
      6 0 0 0 0 0 0
    • 任務終點為6的時候,
      任務起點設為6,此時序列為[ 6 ],不花錢,T[6][6]=0。
      任務起點設為5,此時序列為[ 5 , 6 ],花錢5,則有T[6][5]=5。
      任務起點設為4,此時序列為[ 4 , 5 , 6 ],花錢5,則有T[6][4]=5。
      任務起點設為3,此時序列為[ 3 , 4 , 5 , 6 ],有
      \[ \begin{array}{l} T[6][3] \=min(3+T[6][4],4+max(T[3][3],T[6][5]),5+max(T[4][3],T[6][6]),6+T[5][3]) \=min(3+5,4+5,5+3,6+4) \=8 \end{array} \]
      任務起點設為2,此時序列為[ 2 , 3 , 4 , 5 , 6 ],有
      \[ \begin{array}{l} T[6][2] \=min(2+T[6][3],3+max(T[2][2],T[6][4]),4+max(T[3][2],T[6][5]),5+max(T[4][2],T[6][6]),6+T[5][2]) \=min(2+8,3+5,4+5,5+3,6+6) \=8 \end{array} \]
      任務起點設為1,此時序列為[ 1 , 2 , 3 , 4 , 5 , 6 ],有
      \[ \begin{array}{l} T[6][1] \=min(1+T[6][2],2+max(T[1][1],T[6][3]),3+max(T[2][1],T[6][4]),4+max(T[3][1],T[6][5]),5+max(T[4][1],T[6][6]),6+T[5][1]) \=min(1+8,2+8,3+5,4+5,5+4,6+6) \=8 \end{array} \]
      終點為6填寫完畢,此時整個表格填寫完畢。
      有T為

      1 2 3 4 5 6
      1 0 0 0 0 0 0
      2 1 0 0 0 0 0
      3 2 2 0 0 0 0
      4 4 3 3 0 0 0
      5 6 6 4 4 0 0
      6 8 8 8 5 5 0
    • 到此,T[6][1]=8即為結果。再把上述思路轉為代碼就搞定了。

代碼

class Solution:
    def getMoneyAmount(self, n):
        """
        :type n: int
        :rtype: int
        """
        L = [[0]*(n+1) for i in range(n+1)]
        for i in range(1,n+1):
            for j in range(1,i)[::-1]:
                M = n*n
                if i-j<=2:
                    M = i-1
                else:
                    for k in range(j+1,i):
                        M = min(M,k+max(L[k-1][j],L[i][k+1]))
                L[i][j] = M
        return L[n][1]

測試結果

  • Runtime:692 ms
  • beats 69.5% of python3 submissions
    技術分享圖片

375. Guess Number Higher or Lower II (Python)