題目的難度顏色使用 Luogu 上的分級,由簡單到困難分別為 🔴🟠🟡🟢🔵🟣⚫。

🔗 P1049 [NOIP 2001 普及组] 装箱问题

Problem Statement

題目簡述

給定一個容量為 VV 的箱子與 nn 個物品,每個物品都有自己的體積且最多只能選一次。
請選擇若干物品放入箱子,在總體積不超過 VV 的前提下,讓箱子的剩餘空間最小。

Constraints

約束條件

  • 1V200001 \le V \le 20000
  • 1n301 \le n \le 30
  • 每個物品體積皆為正整數

思路:位元集合最佳化 0/1 背包

核心轉換

剩餘空間最小,等價於在不超過容量的前提下,讓放入箱子的總體積最大。

因此只要記錄「哪些總體積可以被湊出」,最後取不超過容量的最大可達體積即可。

核心觀察

只關心某個總體積能不能湊出,不需要記錄具體選了哪些物品。

位元集合表示狀態

一般 0/1 背包可用布林陣列表示可達體積,但我們也可以把一個大整數或bitset當作布林陣列的壓縮表示:第 ii 位為 11 代表總體積 ii 可達,為 00 代表不可達。

若用一般 0/1 背包表示,令狀態代表「體積 jj 是否可達」,處理一件體積為 xx 的物品時,轉移為:

dpjdpjdpjxdp_j \leftarrow dp_j \lor dp_{j - x}

意思是:體積 jj 原本可達,或是原本能湊出 jxj - x,再放入這件物品後變成 jj

觀察這個轉移,所有「由 jxj - x 轉移到 jj」其實就是把整個可達狀態往右邊平移 xx 格。若把布林陣列壓成 bitset,第 ii 位代表體積 ii 是否可達,那麼這個平移就可以直接用左移完成:

statestate(statex)state \leftarrow state \lor (state \ll x)

其中原本的 state 表示「不選這件物品」,state << x 表示「選這件物品」後新增的可達體積。兩者合併後,就得到處理完這件物品後的所有可達體積。

左移後可能出現超過容量的狀態,這些不可能成為答案,所以用遮罩只保留 00VV 的 bit。

取出答案

所有物品處理完後,最高的可達體積就是能塞入的最大總體積,因此答案為:

V最大可達體積V - \text{最大可達體積}

在 Python 中可以用 bit_length() 找出最高可達位。

複雜度分析

  • 時間複雜度:O(nVw)\mathcal{O}(n \cdot \dfrac{V}{w}),其中 ww 為機器字長,通常為 32326464
  • 空間複雜度:O(Vw)\mathcal{O}(\dfrac{V}{w})

Code

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
def solve():
V = int(input())
n = int(input())
A = [int(input()) for _ in range(n)]

f = 1
U = (1 << (V + 1)) - 1
for x in A:
f |= f << x
f &= U
print(V - (f.bit_length() - 1))


if __name__ == "__main__":
solve()