Problem Statement
題目簡述
Alice 和 Bob 進行射箭比賽。比賽共有 12 個得分區域,分值為 0 0 0 到 11 11 1 1 。
給定箭的總數 numArrows \text{numArrows} numArrows ,以及 Alice 在每個區域射中的箭數陣列 aliceArrows \text{aliceArrows} aliceArrows 。
比賽規則如下:
針對第 k k k 個區域(分值為 k k k ),如果 Bob 射中的箭數大於 Alice(即 bobArrows [ k ] > aliceArrows [ k ] \text{bobArrows}[k] > \text{aliceArrows}[k] bobArrows [ k ] > aliceArrows [ k ] ),則 Bob 獲得該區域的 k k k 分;否則 Alice 獲得該區域的分數(若皆為 0 0 0 則無人得分)。
Bob 射出的箭總數必須恰好為 numArrows \text{numArrows} numArrows 。
請返回一個陣列 bobArrows \text{bobArrows} bobArrows ,表示能讓 Bob 獲得最大總分 的箭數分佈。如果有多種方案能獲得相同的最大分數,返回其中任意一種 即可。
Constraints
約束條件
numArrows ∈ [ 1 , 1 0 5 ] \text{numArrows} \in [1, 10^5] numArrows ∈ [ 1 , 1 0 5 ]
aliceArrows.length = 12 \text{aliceArrows.length} = 12 aliceArrows.length = 1 2
∑ aliceArrows [ i ] = numArrows \sum \text{aliceArrows}[i] = \text{numArrows} ∑ aliceArrows [ i ] = numArrows
aliceArrows [ i ] ≥ 0 \text{aliceArrows}[i] \ge 0 aliceArrows [ i ] ≥ 0
思路:決策優化與背包模型
核心觀察
對於每一個得分區域 i i i (分值為 i i i ),Bob 只有兩種理性決策:
爭奪並贏下 :投入比 Alice 多 1 1 1 支的箭,即消耗代價 w i = aliceArrows [ i ] + 1 w_i = \text{aliceArrows}[i] + 1 w i = aliceArrows [ i ] + 1 支箭,獲得價值 v i = i v_i = i v i = i 的分數。
放棄 :投入 0 0 0 支箭,消耗代價 0 0 0 ,獲得分數 0 0 0 。
任何介於 1 1 1 到 aliceArrows [ i ] \text{aliceArrows}[i] aliceArrows [ i ] 之間的投箭數都是不划算的,因為消耗了資源卻無法得分;同樣地,投入超過 aliceArrows [ i ] + 1 \text{aliceArrows}[i] + 1 aliceArrows [ i ] + 1 支箭也是多餘的浪費。
因此,問題本質上是:我們要在總箭數限制下,選擇贏下哪些得分區域的集合,使總得分最大。
方法一:二進制枚舉 (Bitmask)
由於得分區域數量 n = 12 n = 12 n = 1 2 非常小,所有可能贏下的區域子集只有 2 12 = 4096 2^{12} = 4096 2 1 2 = 4 0 9 6 種。
如此微小的搜尋空間,直接利用二進制狀態壓縮(Bitmask)進行暴力列舉是最直觀的做法。
由於只有 12 12 1 2 個得分區域,我們可以把「要贏下哪些區域」的選擇狀態,壓縮在一個整數的二進制位中。在遍歷所有的選擇可能時,如果當前組合所需的總箭數沒有超過限制,且總得分比之前記錄的更大,我們就更新最優得分與對應的射箭方案。
題目要求必須射出恰好 numArrows \text{numArrows} numArrows 支箭。如果在確定了最優得分組合後,手裡還有剩餘的箭,我們可以直接把剩餘的箭全部分配到任意區域(例如分值為 0 0 0 的區域),這既滿足了題目規定的總箭數約束,又不會影響我們已經算好的得分。
複雜度分析
時間複雜度:O ( 2 n ⋅ n ) \mathcal{O}(2^n \cdot n) O ( 2 n ⋅ n ) ,其中 n = 12 n = 12 n = 1 2 是得分區域數量。總共列舉 2 12 = 4096 2^{12} = 4096 2 1 2 = 4 0 9 6 個狀態,每個狀態需要 O ( n ) \mathcal{O}(n) O ( n ) 時間計算得分與箭數分配。
空間複雜度:O ( n ) \mathcal{O}(n) O ( n ) ,用於存儲答案分配陣列。
方法二:0-1 背包 DP 與方案構造
1. 從遞迴到遞推:如何想到 0-1 背包?
雖然本題 n = 12 n = 12 n = 1 2 極小,二進制枚舉足以過關。但如果 n n n 擴大(例如 n = 100 n = 100 n = 1 0 0 ),2 n 2^n 2 n 的指數級複雜度將會失效。
仔細思考,我們對每個得分區域 i i i 的決策是獨立的:要麼不選(花費 0 0 0 支箭,得 0 0 0 分),要麼選(花費 aliceArrows [ i ] + 1 \text{aliceArrows}[i] + 1 aliceArrows [ i ] + 1 支箭,得 i i i 分)。
這是一個非常標準的 0-1 背包問題 (0-1 Knapsack Problem):
背包容量 :箭的總數 numArrows \text{numArrows} numArrows 。
物品 :n n n 個得分區域。
物品體積(代價) :w i = aliceArrows [ i ] + 1 w_i = \text{aliceArrows}[i] + 1 w i = aliceArrows [ i ] + 1 。
物品價值(收益) :v i = i v_i = i v i = i 。
2. 狀態定義與轉移
設 f [ i ] [ j ] f[i][j] f [ i ] [ j ] 表示考慮前 i i i 個得分區域,在剩餘 j j j 支箭時能獲得的最大得分。
對於第 i i i 個區域(成本 w = aliceArrows [ i ] + 1 w = \text{aliceArrows}[i] + 1 w = aliceArrows [ i ] + 1 ,價值 v = i v = i v = i ):
不選 :f [ i + 1 ] [ j ] = f [ i ] [ j ] f[i + 1][j] = f[i][j] f [ i + 1 ] [ j ] = f [ i ] [ j ]
選 (前提是 j ≥ w j \ge w j ≥ w ):f [ i + 1 ] [ j ] = f [ i ] [ j − w ] + v f[i + 1][j] = f[i][j - w] + v f [ i + 1 ] [ j ] = f [ i ] [ j − w ] + v
兩者取最大值:f [ i + 1 ] [ j ] = { max ( f [ i ] [ j ] , f [ i ] [ j − w ] + v ) , if j ≥ w f [ i ] [ j ] , if j < w f[i + 1][j] = \begin{cases} \max(f[i][j], f[i][j - w] + v), & \text{if } j \ge w \\ f[i][j], & \text{if } j < w \end{cases}
f [ i + 1 ] [ j ] = { max ( f [ i ] [ j ] , f [ i ] [ j − w ] + v ) , f [ i ] [ j ] , if j ≥ w if j < w
其中 f [ 0 ] [ j ] = 0 f[0][j] = 0 f [ 0 ] [ j ] = 0 。最終的最大得分為 f [ n ] [ numArrows ] f[n][\text{numArrows}] f [ n ] [ numArrows ] 。
3. 方案重構(路徑回溯)
對於需要輸出具體方案的背包問題,通常保留二維 DP 陣列更方便。原因是每個狀態 f [ i + 1 ] [ j ] f[i + 1][j] f [ i + 1 ] [ j ] 都記錄了它是從「不選第 i i i 個區域」還是「選第 i i i 個區域」轉移過來的,我們可以沿著這條轉移路徑倒推出答案。
具體做法是從終點 f [ n ] [ numArrows ] f[n][\text{numArrows}] f [ n ] [ numArrows ] 往回走。設當前剩餘箭數為 r e m = numArrows rem = \text{numArrows} r e m = numArrows ,倒序枚舉 i i i 從 n − 1 n-1 n − 1 到 0 0 0 :
若 r e m ≥ w rem \ge w r e m ≥ w 且 f [ i + 1 ] [ r e m ] = = f [ i ] [ r e m − w ] + v f[i + 1][rem] == f[i][rem - w] + v f [ i + 1 ] [ r e m ] = = f [ i ] [ r e m − w ] + v ,說明最優解中選了第 i i i 個區域,令 bobArrows [ i ] = w \text{bobArrows}[i] = w bobArrows [ i ] = w ,並更新 r e m ← r e m − w rem \gets rem - w r e m ← r e m − w 。
否則,說明第 i i i 個區域沒有被選,bobArrows [ i ] \text{bobArrows}[i] bobArrows [ i ] 保持為 0 0 0 。
最後如果還有剩餘箭數 r e m rem r e m ,直接加到第 0 0 0 個區域即可。第 0 0 0 個區域本身不貢獻分數,所以這一步只負責滿足「恰好射出 numArrows \text{numArrows} numArrows 支箭」的要求,不會改變最優得分。
複雜度分析
時間複雜度 :O ( n ⋅ numArrows ) \mathcal{O}(n \cdot \text{numArrows}) O ( n ⋅ numArrows ) ,其中 n = 12 n = 12 n = 1 2 。DP 狀態數為 12 × numArrows 12 \times \text{numArrows} 1 2 × numArrows ,每個狀態轉移只需 O ( 1 ) \mathcal{O}(1) O ( 1 ) ,方案構造只需 O ( n ) \mathcal{O}(n) O ( n ) 。
空間複雜度 :O ( n ⋅ numArrows ) \mathcal{O}(n \cdot \text{numArrows}) O ( n ⋅ numArrows ) ,需要完整的二維 DP 陣列來支持路徑回溯。
Code
方法一:二進制枚舉 (Bitmask)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 class Solution : def maximumBobPoints (self, numArrows: int , aliceArrows: List [int ] ) -> List [int ]: n = len (aliceArrows) ans = [0 ] * n mx = 0 for msk in range (1 << n): cur = 0 rem = numArrows for i, a in enumerate (aliceArrows): if (msk >> i) & 1 : cur += i rem -= a + 1 if rem >= 0 and cur > mx: mx = cur for i, a in enumerate (aliceArrows): ans[i] = a + 1 if (msk >> i) & 1 else 0 ans[0 ] += rem return ans
方法二:0-1 背包 DP
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 class Solution : def maximumBobPoints (self, numArrows: int , aliceArrows: List [int ] ) -> List [int ]: n = len (aliceArrows) f = [[0 ] * (numArrows + 1 ) for _ in range (n + 1 )] for i, a in enumerate (aliceArrows): v = i w = a + 1 for j in range (numArrows + 1 ): if j >= w: f[i + 1 ][j] = max (f[i][j], f[i][j - w] + v) else : f[i + 1 ][j] = f[i][j] ans = [0 ] * n rem = numArrows for i in range (n - 1 , -1 , -1 ): v = i w = aliceArrows[v] + 1 if rem >= w and f[i + 1 ][rem] == f[i][rem - w] + v: ans[i] = w rem -= w ans[0 ] += rem return ans