題目的難度顏色使用 Luogu 上的分級,由簡單到困難分別為 🔴🟠🟡🟢🔵🟣⚫。
Problem Statement
題目簡述
給定長度為 N 的正整數序列 A=(A1,…,AN) 和一個正整數 M。
求滿足以下條件的非負整數序列 x=(x1,…,xN) 的個數:
i=1∑NAixi≤M
答案對 998244353 取模。
Constraints
約束條件
- 1≤N≤100
- 1≤Ai≤100
- 1≤M≤1018
思路:二進位拆分 + 0/1 背包
這道題目的難點在於 M 高達 1018,直接使用背包問題(完全背包)的時間複雜度為 O(N⋅M),無法接受。然而 N 和 Ai 都很小,這提示我們可以從 Ai 的總和或數位 DP 的角度入手。
1. 從完全背包到多重背包的思考
原問題等價於求有多少種非負整數解。如果 M 較小,這是一個標準的完全背包問題(每個物品 Ai 可以被選無限次)。
完全背包可以視為一種特殊的多重背包,其中每種物品的數量上限為 ⌊AiM⌋。
對於多重背包,一個經典的優化手段是二進位拆分。
2. 變數的二進位拆分
我們可以將每個未知數 xi 拆解成二進位形式:
xi=k=0∑60bi,k⋅2k,bi,k∈{0,1}
將其代入原不等式:
i=1∑NAi(k=0∑60bi,k2k)≤M
3. 交換求和順序與 0/1 背包
交換求和順序,按 2 的冪次(位數 k)分組:
k=0∑602kSk(i=1∑NAibi,k)≤M
觀察括號內的 Sk=∑i=1NAibi,k:
- 對於固定的層級 k,我們需要決定每個 xi 在這一位是 0 還是 1。
- 每個 Ai 在這一層只能被選一次(bi,k=1)或不選(bi,k=0)。
- 這正是一個標準的 0/1 背包問題。
因此,問題轉化為:我們從低位到高位逐層處理,每一層做一次 0/1 背包。
4. 層間的狀態傳遞與壓縮
僅僅做背包是不夠的,我們還需要滿足總和 ≤M 的限制。這可以通過類似「數位 DP」的進位思想來處理。
我們從低位(20)開始處理。設 dp[j] 表示在處理完當前層級的背包後,累積的數值(包含從更低位傳上來的進位以及當前層的選擇)為 j 的方案數。
狀態定義:
dp[j]:當前層級累積數值為 j 的方案數。
狀態轉移(背包階段):
在每一層 k,我們先進行一次 0/1 背包:
dp[j]←∑dp[j−Ai]
這代表我們決定了當前層 xi 的取值。
狀態壓縮(進位階段):
這是本題最關鍵的一步。假設當前層累積了數值 j,而 M 在當前位的值為 d=(M≫k)&1。
為了滿足不等式限制,當前層的數值 j 扣除 M 提供的額度 d 後,剩餘的部分必須由更高位來承擔。
由於更高位的權重是當前位的 2 倍(2k+1=2⋅2k),因此傳遞給下一層的數值 jnext 滿足:
j≤d+2⋅jnext
移項得:
2⋅jnext≥j−d⟹jnext=⌈2j−d⌉
為什麼需要壓縮?
如果不進行壓縮,隨著層數上升,j 的範圍會越來越大。但通過除以 2 的操作,我們將狀態空間始終控制在 ∑Ai+21∑Ai+41∑Ai+⋯=2∑Ai 的範圍內(大約 20000 左右),從而保證了複雜度。
5. 總結算法流程
- 初始化 dp[0]=1,其餘為 0。
- 對於 M 的每一個位元(從低到高):
- 背包:遍歷每個物品 Ai,更新 dp 陣列(0/1 背包)。
- 壓縮:根據 M 當前位 d,計算新狀態 jnext=⌈2j−d⌉,將 dp[j] 累加到 dpnext[jnext]。
- 更新:dp←dpnext,並將 M 右移一位。
- 最終 dp[0] 即為答案(表示所有位數處理完後,沒有溢出且滿足條件的方案數)。
參考資料
Code
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 30 31 32 33 34 35
| MOD = 998244353
def solve(): N, M = map(int, input().split()) A = list(map(int, input().split())) assert len(A) == N
MAX_W = sum(A) * 2 f = [0] * (MAX_W + 1) f[0] = 1
while M > 0: for x in A: for j in range(MAX_W, x - 1, -1): f[j] = (f[j] + f[j - x]) % MOD
nf = [0] * (MAX_W + 1) d = M & 1 for j in range(MAX_W + 1): nj = (j - d + 1) >> 1 nf[nj] = (nf[nj] + f[j]) % MOD
f = nf M >>= 1 print(f[0])
if __name__ == '__main__': solve()
|
寫在最後
PROMPT