LeetCode 3180. Maximum Total Reward Using Operations I
周賽 401。又在卡常數,連續兩場都這樣搞,真的會被氣死。
題目
輸入長度 n 的整數陣列 rewardValues,代表每個獎勵的價值。
最初你的總獎勵為 x = 0,且所有獎勵都是未標記的。
你可以執行以下操作任意次:
- 選擇未標記的索引 i
- 若 rewardValues[i] 大於當前總獎勵 x,則可以使 x 增加 rewardValues[i],並標記索引 i
求可以收集的總獎勵最大值。
解法
rewardValues 好長,以下簡稱 r。
首先題目並沒有規定獎勵的收集順序。先來看看怎樣收比較划算。
設有兩個數 a, b 且 a > b:
- 先拿 a,總獎勵肯定會超過 b,之後就不能拿 b。
- 先拿 b,總獎勵不一定超過 a,之後還有機會拿 a。
得到一個貪心的結論,先拿較小的獎勵更好。
先將 r 排序。
雖然知道了從小的開始拿,但不到哪些該拿。
因此考慮 dp,枚舉選或不選。
定義 dp(i, j):當前總獎勵為 j,在剩餘 r[i..N-1] 可選的情況下,所能得到的最大獎勵。
轉移:決定當前元素 x = r[i] 選或不選。
- 選:若滿足 x > j,則 dp(i + 1, j + x) + x
- 不選:dp(i + 1, j)
base:當 i = N 時,沒有元素可選,回傳 0。
至於 j 究竟會變得多大?假設 r 之中最大的數字是 R,那麼能選擇 R 所能持有的最大獎勵數為 R - 1。
所以 j 最大只會變成 R 的兩倍。
時間複雜度 O(N * MX),其中 MX = max(r)。
空間複雜度 O(N * MX)。
很可惜 python 記憶化又沒辦法通過 2000 * 4000 的計算量,有夠扯。
class Solution:
def maxTotalReward(self, rewardValues: List[int]) -> int:
r = rewardValues
r.sort()
N = len(r)
@cache
def dp(i, j):
if i == N:
return 0
x = r[i]
# no take
res = dp(i + 1, j)
# take
if x > j:
res = max(res, dp(i + 1, j + x) + x)
return res
ans = dp(0, 0)
dp.cache_clear()
return ans
改成遞推版本才能過。
class Solution:
def maxTotalReward(self, rewardValues: List[int]) -> int:
r = rewardValues
r.sort()
N = len(r)
MX = max(r) * 2
dp = [[0] * (MX) for _ in range(N + 1)]
for i in reversed(range(N)):
x = r[i]
for j in range(MX):
# no take
res = dp[i + 1][j]
# take
if x > j and j + x < MX:
res = max(res, dp[i + 1][j + x] + x)
dp[i][j] = res
return dp[0][0]
觀察發現,dp[i][j] 會重複使用到 dp[i + 1][j] 的值,第一個空間維度可以優化掉。
這邊就不寫了。
上述做法都是以子序列的總和狀態為重點的 dp。
也可以將本題視作一個背包問題:
背包初始容量為 j,可以裝入滿足 x > j 的物品 x
求背包最大可以裝多少容量
需要維護的是能夠湊出的容量總和。
初始容量只有 0 一種,之後每個 x,能夠和能與原有的容量 j 變成新的容量 x + j。
例如:
r = [1,3,4]
最初 dp = {0}
x = 1 可以和 0 組成新的容量
dp = {0,1}
x = 3 可以和 0,1 組成新的容量
dp = {0,1,3,4}
x = 4 可以和 0,1,3 組成新的容量
dp = {0,1,3,4,5,8}
雖然複雜度一樣沒變,但執行時間從上面的 5000ms 降低到 600ms,效率提高非常多。
class Solution:
def maxTotalReward(self, rewardValues: List[int]) -> int:
r = rewardValues
r.sort()
MX = max(r) * 2
dp = set()
dp.add(0)
for x in r:
for j in list(dp):
if x > j and x + j < MX:
dp.add(x + j)
return max(dp)
對於 Q4 更大的測資範圍,N * MX 肯定是不能過的,需要有更神奇的優化方式。
雖然很確定需要優化枚舉 j 的過程,但是我在比賽中想了好久,除了 bitmask 以外,好像沒什麼資料結構可以單次修改不連續區間。
後來看別人題解才認識新朋友 bitset。
基本用法就和 bitmask 一樣,但可以有更多位元。
如果沒有內建 bitset 的語言可以找 bigint 之類的東西,至於 python 本身就支援超大數,非常方便。
知道能當作 bitmask 用就很簡單了。第 i 個位元若代表背包總和 i 是否可湊成。
初始化 bitset = 1,代表只有 0 這個總和是合法的。
遍歷每個 x,我們只能取用小於 x 的值。先拿只有最右連續 x - 1 個 1 位元的 mask 篩選出目標 todo。
之後再把 todo 全部加上 x,也就是左移 x 次,方可和原 bitset 合併。
bitset 的最高位就是答案。
時間複雜度 O(N * MX / W),其中 MX = max(r),W = 64 或 32。
空間複雜度 O(MX / W)。
class Solution:
def maxTotalReward(self, rewardValues: List[int]) -> int:
r = rewardValues
r.sort()
bitset = 1
for x in r:
mask = (1 << x) - 1
todo = mask & bitset
bitset |= todo << x
return bitset.bit_length() - 1