HIHO Drinking Game

小Hi和小Ho正在玩這樣一個遊戲,在每局遊戲的開始,小Hi手持一瓶可以認爲是無窮無盡的飲料,而小Ho手中有一個空杯子。

一局遊戲分爲N輪,在每輪行動中,小Hi先向小Ho手中的杯子倒入T個單位的飲料(倒入的數量在一局遊戲開始之前約定好且在整局遊戲中固定),然後小Ho擲出一個均勻的K面骰子得到一個1..K之間的數d,如果杯中飲料的單位數小於等於d,則小Hi記一分,且小Ho將杯中剩餘飲料一飲而盡,否則小Ho記一分,小Ho喝掉杯中d個單位的飲料。在N輪結束後,分高者獲勝。

那麼問題來了,如果小Ho能夠預測這局中每輪自己所擲出的點數,那麼最小的能使得小Ho獲勝的T(每輪小Hi倒入小Ho杯子的飲料的單位數)是多少?

算法分析

本題需要我們求的是最小滿足要求的 T 值,使得在該 T 值下,小Ho獲得的分數高於小Hi。

因爲小Hi和小Ho分數之和一定爲 n,所以小Ho獲勝的條件可以改爲小Ho的分數score大於n/2

通過分析題意,我們可以知道score和 T 之間滿足一定的關係,score會隨着 T 值的變化而變化,則可以假設有:

score = f(T)

我們根據題目描述的遊戲規則構造出f(T)函數:

f(T):
    rest = 0;     // 當前杯中剩餘的飲料體積
    score = 0;    // 小Ho的得分
    for i = 1 .. n
        rest = rest + T;    // 小Hi向杯子中倒入T單位飲料
        if (rest > d[i])    // 若杯子中飲料大於第i輪的d
            score = score + 1;     // 小Ho獲得一分
            rest = rest - d[i];    // 小Ho喝掉d個單位飲料
        else
            rest = 0;       // 小Ho喝掉全部的飲料
        end if
    end for
    return score;

每次執行f(T)函數花費的時間代價爲 O(n)


f(T)進一步研究,我們可以發現:

  • 當 T = 0 時,score = f(0) = 0
  • 當 T = K 時,score = f(K) = n

我們可以猜想在 T 從 0 到 K 的過程中,小Ho獲得的分數score是單調遞增的。

而要證明f(T)函數確實滿足遞增的性質,只需證明對於 T 和 T' (T < T'),每一輪開始時小Ho的得分和剩餘的飲料體積,T 對應的數值都不超過 T' 對應的數值。

我們設s[i]r[i]表示第i輪開始,還沒有添加 T 單位飲料時,小Ho的得分和剩餘飲料的體積;s'[i]r'[i]表示第i輪開始,還沒有添加 T' 單位飲料時,小Ho的得分和剩餘飲料的體積。

我們要證明:對於i = 1..N+1,都有s[i] ≤ s'[i]r[i] ≤ r'[i]

利用數學歸納法,i = 1 時,s[i] = s'[i] = 0r[i] = r'[i] = 0,結論成立。

假設i = n 時結論成立,那麼當i = n+1 時:r[n+1] = max(r[n]+T-d, 0),r'[n+1] = max(r'[n]+T'-d, 0)

由於r[n] ≤ r'[n]T < T',所以r[n]+T < r'[n]+T'

  • d < r[n]+T < r'[n]+T時,r[n+1] = r[n]+T-dr'[n+1] = r'[n]+T'-ds[n+1] = s[n]+1,s'[n+1] = s'[n]+1,易知結論成立;
  • r[n]+T ≤ d < r'[n]+T時,r[n+1] = 0r'[n+1] = r'[n]+T'-d > 0s[n+1] = s[n],s'[n+1] = s'[n]+1,易知結論成立;
  • r[n]+T < r'[n]+T ≤ d時,r[n+1] = r'[n] = 0s[n+1] = s[n]s'[n+1] = s'[n],易知結論成立。

綜上所述,對於i = 1..N+1,都有s[i] ≤ s'[i]r[i] ≤ r'[i]。而f(T) = s[N+1] ≤ s'[N+1] = f(T'),所以函數f(T)是單調遞增的。

score首次超過n/2時的 T 值,也就是我們要求的最小值,不妨記爲 M


那麼接下來要考慮的就是如何快速的求得 M 值。

一個簡單的想法是從 0 開始依次枚舉,直到score大於n/2,這樣可以保證在第一時間計算出 M值。一共需要執行 M 次f(T)函數,所以其時間複雜度爲 O(nM)。對於足夠強的數據這顯然是會超時的,必須降低執行f(T)函數的次數。

我們再一次觀察f(T)函數:

hiho69.png

我們隨機找一個 T 值,並計算出其f(T)。根據f(T)n/2的大小關係,我們可以判斷出當前計算出的 T 值是小於 M,亦或是大於 M

由這個性質,我們可以得到一個區間逼近的算法:

  1. 初始化 T 可能的取值區間[left, right],保證f(left) < n/2, f(right) ≥ n/2。這裏我們取[0,M]
  2. left + 1 == right,跳轉第四步。否則繼續第三步。
  3. 取 mid = (left + right) / 2,並計算出f(mid)

    假設f(mid) < n/2,由f(T)爲單調遞增函數,則對於任意一個 T 屬於[left, mid]f(T) < n/2

    因此 M 一定不在[left, mid]內,M 一定在[mid, right]的區間內。因此我們令left = mid,並回到第二步。

    同理,若f(mid) ≥ n/2,則 M 一定在[left, mid]。此時我們令right = mid,並回到第二步。

  4. 由於left + 1 == right,且f(left) < n/2, f(right) ≥ n/2。因此right即爲所求的 M 值。

這個算法滿足每一次將區間縮小一半,因此總的時間複雜度爲 O(nlogK)。其僞代碼:

left = 0;
right = K;
while (left + 1 < right)
    mid = (left + right) / 2;
    if (f(mid) < n/2) left = mid;
    else right = mid;
end while


至此我們得到了該題的解決辦法:利用二分縮小答案的區間,並利用答案本身去判定最優解的範圍。

這樣的算法我們一般稱之爲"二分答案",其明顯的標誌有兩個:

  • 求可行解中的最優解
  • 能夠構造出關於解的f(T)函數,並且f(T)函數滿足單調性

只要能夠熟練的發現這兩個標誌,對於同類的問題也就能夠迎刃而解了。

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