Tuyển sinh 🎓 Mình nhận đào tạo lập trình giải thuật thi đấu: thi vào lớp 10 chuyên tin · thi học sinh giỏi tin học cấp tỉnh/thành · thi vào đội tuyển quốc gia · Olympic tin học sinh viên / ICPC · phỏng vấn thuật toán. Bấm vào đây để xem thông tin khóa học → Xem khóa học

Cái túi 1 ver 2

Cái túi 1 ver 2
Nguồn: CLUE OJ – Chương 20: Quay lui (VinhDinhCoder – BTKCATU1) Độ khó: Trung bình Quay lui Cái túi

Đề bài

Trong siêu thị tại gian hàng Black Friday có $n$ đồ vật, đồ vật thứ $i$ có trọng lượng là $W[i]$ và giá trị $V[i]$. Bạn được một phiếu mua hàng đặc biệt khi vào mua hàng tại gian hàng Black Friday ở siêu thị này. Khi sử dụng phiếu mua hàng này bạn được phép chọn một số đồ vật bất kỳ trên gian hàng miễn sao trọng lượng tối đa là $M$. Hỏi bạn có thể lấy đi những mặt hàng nào để giá trị bạn nhận được là tối đa.

Bạn hãy lập trình giải quyết bài toán trên với dữ liệu cho là danh sách $n$ mặt hàng tại gian hàng mà bạn có thể chọn.

Dữ liệu nhập:

  • Dòng đầu tiên là hai số nguyên dương $n$ và $M$ $(1 \le n \le 25;\ 1 \le M \le 10^{11})$.
  • Dòng thứ $i$ trong $n$ dòng tiếp theo chứa hai số nguyên $w[i]$ và $v[i]$ là trọng lượng và giá trị của đồ vật thứ $i$ $(1 \le w[i], v[i] \le 10^9)$.

Kết quả: in ra một số nguyên duy nhất là tổng giá trị tối đa của các vật phẩm mà bạn có thể chọn.

Ghi chú: ảnh đề gốc ghi giới hạn thời gian 3 giây, 128 MB; trên CLUE OJ bài đặt giới hạn 1 giây, 256 MB.

Ví dụ – input
5 20
7 1
5 8
1 1
7 8
9 7
Ví dụ – output
18

Giải thích ví dụ: lấy 4 đồ vật đầu tiên, tổng trọng lượng $7 + 5 + 1 + 7 = 20 \le 20$, tổng giá trị $1 + 8 + 1 + 8 = 18$. Không có cách chọn nào được nhiều hơn (ví dụ lấy đồ vật 2, 4, 5 thì nặng $5 + 7 + 9 = 21 > 20$).

Lời mở đầu

Bài cái túi (knapsack) thường được giải bằng quy hoạch động theo trọng lượng, nhưng ở đây $M$ lên tới $10^{11}$ nên cách đó không dùng được. Bù lại $n \le 25$ rất nhỏ: mỗi đồ vật chỉ có 2 lựa chọn lấy hoặc không lấy, tổng cộng $2^{25} \approx 3{,}4 \cdot 10^7$ cách chọn – vừa đủ để quay lui thử hết. Mình sẽ so sánh cách duyệt bitmask (chậm hơn một chút vì mỗi cách chọn phải cộng lại $n$ số) với cách quay lui (cộng dồn khi đi xuống cây, lại cắt được nhánh quá tải).

Dành cho học viên · Ưu tiênNộp bài trên hệ thống CLUE OJ

Link này chỉ học viên đã tham gia tổ chức mới vào được. Học viên nên ưu tiên nộp ở đây vì sẽ được chấm nhanh hơn.

Nộp bài trên CLUE OJ

Lộ trình lời giải, bấm vào từng phần để nhảy tới:

Bộ testBộ test chính thức có gì?

Test$n$$M$ lớn nhất$w$ lớn nhất$v$ lớn nhấtĐáp án lớn nhất
1–305–1029101052
31–5015–15989.867.216997.543.379942.203.3201.697.237.779
51–7020–20397.248.584935.832.228961.185.8072.210.867.315
71–10025–25959.216.042999.840.189999.247.9765.002.061.563

Đề không ghi subtask, nhưng bộ test chia rất rõ thành 4 mức theo $n$, mình tự đặt tên là các mức dưới đây. Đáp án có thể vượt $2^{31}$ (tổng của tối đa 25 giá trị $10^9$), nên tổng trọng lượng, tổng giá trị đều phải là long long. Riêng $M$ tới $10^{11}$ – không làm quy hoạch động theo trọng lượng được, đây đúng là bài cho quay lui.

MứcĐiều kiệnTestCách 1 (bitmask, C++)Cách 2 (quay lui, C++)
1$n \le 10$1–30quaqua
2$n = 15$31–50quaqua
3$n = 20$51–70quaqua
4$n = 25$71–100quá thời gian (~2,8 s)qua (≤ 0,14 s)

Ý tưởngCây nhị phân lấy / không lấy

Xét lần lượt đồ vật 1, 2, ..., $n$. Với mỗi đồ vật ta rẽ thành 2 nhánh: bỏ nó hoặc lấy nó. Đi hết $n$ tầng ta được một cách chọn, đem tổng giá trị so với kỷ lục best. Hàm quay lui:

Try(i, W, V) – đang xét đồ vật i, đã lấy tổng trọng lượng W, tổng giá trị V
  • Nếu $i > n$: cập nhật best = max(best, V).
  • Nhánh bỏ: Try(i + 1, W, V).
  • Nhánh lấy – chỉ khi $W + w_i \le M$: Try(i + 1, W + w[i], V + v[i]). Nếu túi đã không chứa nổi thì cả cây con bên dưới bị cắt bỏ.

Em nhập các đồ vật (mỗi đồ vật 2 số $w\ v$, tối đa 5 đồ vật) rồi bấm Tiếp. Mỗi nút ghi W/V hiện tại, nhánh trên là "bỏ", nhánh dưới là "lấy":

nút đang xétlá lập kỷ lục mớiquá M → cắt nhánhviền cam: đang trên ngăn xếp
Ví dụ nhanh:

Cách 1Duyệt mọi tập con bằng bitmask (qua mức 1–3)

Mỗi số nguyên mask từ $0$ đến $2^n - 1$ ứng với một cách chọn: bit thứ $i$ bằng 1 nghĩa là lấy đồ vật $i$. Với mỗi mask, cộng trọng lượng và giá trị các đồ vật được chọn, nếu không quá $M$ thì cập nhật đáp án.

C++▶ Chạy tay từng dòng codeCách 1: duyệt mọi mask từ 0 đến 2^n − 1, cộng lại trọng lượng và giá trị.
Dữ liệu mẫu:
Các bước dừng ở dòng if(mask >> i & 1) cho từng bit (gộp luôn vòng for i).
Không gian$O(n)$
Thời gian$O(2^n \cdot n)$

Kết quả chạy thật: đúng 70/100 test (mức 1, 2, 3). 30 test cuối có $n = 25$: $2^{25} \cdot 25 \approx 8{,}4 \cdot 10^8$ phép tính, chạy khoảng 2,8 giây → quá thời gian.

Cách 2Quay lui cộng dồn + cắt nhánh quá tải (full điểm)

Điểm yếu của cách 1 là mỗi tập con lại cộng lại từ đầu $n$ số. Quay lui thì cộng dồn: đi xuống một tầng chỉ tốn 1 phép cộng, nên tổng chi phí chỉ là số nút của cây ($\approx 2^{n+1}$). Thêm nữa, nhánh "lấy" chỉ được đi khi túi còn chứa nổi, nên rất nhiều cây con bị cắt bỏ.

C++▶ Chạy tay từng dòng codeCách 2: Try(i, W, V) rẽ nhánh bỏ / lấy, cắt nhánh lấy khi quá tải.
Dữ liệu mẫu:
Khung bên phải là ngăn xếp lời gọi: mỗi tầng ghi đồ vật đang xét và nhánh đang đi (lấy/bỏ).
  1. Tham số W, V truyền theo giá trị nên khi quay lui về không cần "trừ lại" – mỗi lời gọi giữ bản sao riêng.
  2. Dùng long long cho M, W, V, best: $M$ tới $10^{11}$, tổng giá trị tới $2{,}5 \cdot 10^{10}$.
  3. Độ sâu đệ quy chỉ $n + 1 = 26$ tầng, không lo tràn ngăn xếp.
Không gian$O(n)$ (độ sâu đệ quy)
Thời gian$O(2^n)$, thực tế ít hơn nhờ cắt nhánh

Kết quả chạy thật: đúng 100/100 test, chậm nhất 0,14 giây. Kể cả trường hợp xấu nhất không cắt được nhánh nào ($2^{26}$ lời gọi) thì vẫn chỉ khoảng 0,3 giây.

Python thì sao?

Bản Python của cách 2 (khung code trên) đúng 91/100 test: 9 test $n = 25$ mà túi rộng (ít nhánh bị cắt) chạy 1,6–3,7 giây → quá thời gian. Để Python qua hết, mình dùng thêm kỹ thuật chia đôi tập (meet in the middle): chia đồ vật thành 2 nửa, mỗi nửa liệt kê tối đa $2^{13} = 8192$ tập con. Sắp xếp các tập của nửa phải theo trọng lượng, tính giá trị lớn nhất tiền tố; với mỗi tập của nửa trái (trọng lượng $W$) chặt nhị phân tìm tập nửa phải nặng nhất mà vẫn $\le M - W$. Độ phức tạp $O(2^{n/2} \cdot n)$.

Kết quả chạy thật: bản chia đôi tập bằng Python đúng 100/100 test, chậm nhất 0,033 giây.

Ghi chú của mình: C++ – cách 1 (bitmask) được 70/100, quá thời gian ở 30 test $n = 25$; cách 2 (quay lui) 100/100, chậm nhất 0,14 giây. Python: quay lui 91/100 (TLE 9 test n = 25), chia đôi tập (meet in the middle) 100/100.
Rất vui khi anh em nào đã đọc đến những dòng cuối cùng này. Anh em đọc chắc cũng mỏi mắt luôn chứ 😅 vậy anh em nghĩ người viết ra hết đống ở trên còn thế nào nữa 🤪. Vậy nên đừng tiếc gì cho mình một lời bình luận hoặc chia sẻ bài viết để động viên mình nè. Chúc anh em học tốt và gặt hái nhiều thành công 😇

Nhận xét