小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] = 0
,r[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-d
,r'[n+1] = r'[n]+T'-d
,s[n+1] = s[n]+1
,s'[n+1] = s'[n]+1
,易知結論成立; - 當
r[n]+T ≤ d < r'[n]+T
時,r[n+1] = 0
,r'[n+1] = r'[n]+T'-d > 0
,s[n+1] = s[n]
,s'[n+1] = s'[n]+1
,易知結論成立; - 當
r[n]+T < r'[n]+T ≤ d
時,r[n+1] = r'[n] = 0
,s[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)
函數:
我們隨機找一個 T 值,並計算出其f(T)
。根據f(T)
與n/2
的大小關係,我們可以判斷出當前計算出的 T 值是小於 M,亦或是大於 M。
由這個性質,我們可以得到一個區間逼近的算法:
- 初始化 T 可能的取值區間
[left, right]
,保證f(left) < n/2, f(right) ≥ n/2
。這裏我們取[0,M]
。 - 若
left + 1 == right
,跳轉第四步。否則繼續第三步。 -
取
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
,並回到第二步。 - 由於
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)
函數滿足單調性
只要能夠熟練的發現這兩個標誌,對於同類的問題也就能夠迎刃而解了。