DP SOS: Quy hoạch động tổng tập con
Experiments (1)
DP SOS (Sum over Subsets) tính tổng giá trị của mọi tập con cho từng tập hợp. Bài học bắt đầu từ cách duyệt toàn bộ cặp mask, submask, sau đó giảm công việc lặp lại bằng quy hoạch động. Bạn sẽ thấy cùng một đáp án qua ba mức thời gian: , và .
Bài viết được biên soạn lại từ DP SOS của SPyofgame trên es.ac.vn, với biên tập viên tuandebu và người đóng góp ngpin04. Phần định nghĩa, hai cách cày trâu và các ví dụ đến từ bản gốc; phần duyệt tập con và truy hồi SOS là phần bổ sung của bản này. Xem nguồn và ghi chú biên soạn.
Kiến thức cần có
Section titled “Kiến thức cần có”- Mảng, vòng lặp, hàm đệ quy và phép cộng số nguyên trong C++
- Biểu diễn nhị phân: bit thứ có trọng số , đánh số từ
- Phép AND
&, OR|, XOR^và dịch trái<< - Ý tưởng quy hoạch động: lưu và tái sử dụng kết quả của các bài toán con
Nếu cần ôn C++, bắt đầu từ bài học nhập và xuất. Bài này thuộc nhóm quy hoạch động của CP Wiki.
Bài toán tổng tập con
Section titled “Bài toán tổng tập con”Xét tập nền . Một số nguyên mask trong biểu diễn một tập con của : bit bằng khi tập đó chứa . Ví dụ, biểu diễn tập .
Cho mảng có phần tử. Với mỗi mask, cần tính
Ký hiệu so sánh hai tập hợp được biểu diễn bằng bit, tương đương với (submask & mask) == submask. Nó không có nghĩa là submask <= mask: chẳng hạn , nhưng không phải tập con của .
Tập rỗng có mã , nên được tính trong mọi tổng. Nếu bài toán không tính tập rỗng, có thể đặt trước khi chạy. Nếu mảng ban đầu chỉ có phần tử, chủ động thêm số vào các vị trí .
Ví dụ nhỏ
Section titled “Ví dụ nhỏ”Với và :
mask | Biểu diễn tập hợp | Các submask hợp lệ | Tổng |
|---|---|---|---|
00 | 00 | ||
01 | 00, 01 | ||
10 | 00, 10 | ||
11 | 00, 01, 10, 11 |
Kết quả là . Giá trị a[mask] là trọng số gắn với cả tập hợp, không phải tổng trọng số của từng bit trong tập hợp đó.
Ví dụ từ sơ đồ gốc
Section titled “Ví dụ từ sơ đồ gốc”Với mask = 26, chỉ các bit được bật. Tám tập con có mã . Các giá trị trong hình gốc là:
submask | 0 | 2 | 8 | 10 | 16 | 18 | 24 | 26 |
|---|---|---|---|---|---|---|---|---|
| 8 | 9 | 9 | 10 | 12 | 4 | 6 | 14 |
Do đó .
flowchart TD
total["F[26] = 72"] --> low["Không có bit 4: 36"]
total --> high["Có bit 4: 36"]
low --> l0["a[0] + a[2] = 17"]
low --> l1["a[8] + a[10] = 19"]
high --> h0["a[16] + a[18] = 16"]
high --> h1["a[24] + a[26] = 20"]Ready when requested
Sơ đồ chia tám tập con thành hai nhóm theo bit , rồi chia tiếp theo bit . Bảng và phép cộng phía trên vẫn cung cấp toàn bộ ví dụ khi chưa mở sơ đồ.
Cách cày trâu
Section titled “Cách cày trâu”Khởi tạo . Duyệt mọi mask và mọi submask trong ; nếu submask là tập con của mask, cộng vào .
Cày trâu bằng vòng lặp
Section titled “Cày trâu bằng vòng lặp”Hàm nhận mảng đã được thêm số cho đủ phần tử. Các ví dụ C++ trong bài dùng long long; mọi tổng trung gian phải nằm trong miền biểu diễn của kiểu này.
#include <cstddef>#include <vector>
std::vector<long long> brute_sos(const std::vector<long long>& a) { std::vector<long long> result(a.size(), 0); for (std::size_t mask = 0; mask < a.size(); ++mask) { for (std::size_t submask = 0; submask < a.size(); ++submask) { if ((mask & submask) == submask) { result[mask] += a[submask]; } } } return result;}Có cặp cần kiểm tra. Phép AND kiểm tra tập con trong thời gian hằng số khi mask nằm trong một từ máy. Nếu thay phép AND bằng vòng lặp kiểm tra từng bit, thời gian có thể tăng thành .
Cày trâu bằng đệ quy
Section titled “Cày trâu bằng đệ quy”Với mỗi vị trí bit, xét bốn cặp giá trị (bit của mask, bit của submask): (0,0), (0,1), (1,0), (1,1). Khi đã chọn xong bit, kiểm tra quan hệ tập con rồi cộng vào kết quả.
#include <cstddef>#include <vector>
void brute_recursive(int bit, std::size_t mask, std::size_t submask, const std::vector<long long>& a, std::vector<long long>& result) { if (bit < 0) { if ((mask & submask) == submask) { result[mask] += a[submask]; } return; } const std::size_t flag = std::size_t{1} << bit; brute_recursive(bit - 1, mask, submask, a, result); brute_recursive(bit - 1, mask, submask | flag, a, result); brute_recursive(bit - 1, mask | flag, submask, a, result); brute_recursive(bit - 1, mask | flag, submask | flag, a, result);}Trước khi gọi, tạo result gồm số , rồi gọi brute_recursive(k - 1, 0, 0, a, result). Điều kiện bit < 0 phải được xét trước phép dịch bit. Với , lời gọi đi thẳng vào trường hợp cơ sở và cho .
Cây đệ quy có lá. Tổng số nút là , nên thời gian là ; ngăn xếp có độ sâu , ngoài mảng kết quả .
Chỉ duyệt các tập con hợp lệ
Section titled “Chỉ duyệt các tập con hợp lệ”Phần bổ sung này loại các cặp không hợp lệ ngay từ khi duyệt. Bắt đầu ở submask = mask, rồi lặp submask = (submask - 1) & mask để sinh tập con kế tiếp theo thứ tự giảm dần.
#include <cstddef>#include <vector>
std::vector<long long> submask_sos(const std::vector<long long>& a) { std::vector<long long> result(a.size(), 0); for (std::size_t mask = 0; mask < a.size(); ++mask) { std::size_t submask = mask; while (true) { result[mask] += a[submask]; if (submask == 0) break; submask = (submask - 1) & mask; } } return result;}Phép trừ tắt bit thấp nhất và bật các bit thấp hơn; phép AND loại những bit không thuộc mask. Ví dụ, với mask = 10 (1010), thứ tự là 1010, 1000, 0010, 0000. Dừng sau khi xử lý để vừa tính tập rỗng vừa tránh quay lại mask.
Trong một cặp hợp lệ, mỗi bit có ba khả năng: không thuộc cả hai tập, chỉ thuộc mask, hoặc thuộc cả hai. Vì thế tổng số cặp trên mọi mask là . Có thể đạt cùng giới hạn bằng cách bỏ nhánh (0,1) khỏi hàm đệ quy.
Quy hoạch động theo từng bit
Section titled “Quy hoạch động theo từng bit”Phần bổ sung này giải thích cách tái sử dụng các tổng. Gọi là tổng của những thỏa:
submasklà tập con củamask- Các bit từ trở lên của
submaskgiốngmask - Chỉ các bit được phép thay đổi từ thành
Khi chưa xử lý bit nào, chỉ có chính mask, nên . Khi đã xử lý cả bit, mọi tập con đều được tính, nên .
Công thức truy hồi và tính đúng đắn
Section titled “Công thức truy hồi và tính đúng đắn”Nếu bit của mask bằng , mọi tập con cũng có bit này bằng , nên không có nhóm mới. Nếu bit đó bằng , chia các tập con thành hai nhóm rời nhau: giữ bit và bỏ bit .
Hai nhóm bao phủ toàn bộ các tập con được phép sau bước và không giao nhau. Từ trường hợp cơ sở, quy nạp theo cho thấy mỗi giá trị được cộng đúng một lần vào tổng cần thiết.
Có thể ghi đè trên một mảng: tại bước , chỉ các mask có bit bị cập nhật. Mọi ô được đọc thêm đều có bit , nên chưa bị thay đổi trong bước đó. Vì vậy thứ tự duyệt mask trong cùng một bước bit không ảnh hưởng kết quả.
Theo dõi một ví dụ
Section titled “Theo dõi một ví dụ”Vẫn dùng :
| Bước | 00 | 01 | 10 | 11 |
|---|---|---|---|---|
| Khởi tạo | 1 | 2 | 3 | 4 |
| Xử lý bit 0 | 1 | 3 | ||
| Xử lý bit 1 | 1 | 3 |
Phải đặt vòng lặp bit ở ngoài khi ghi đè theo cách này. Nếu đổi sang mask ở ngoài, với ví dụ trên có thể thu được thay vì ở mask 11, vì bị cộng lặp.
Chương trình C++17
Section titled “Chương trình C++17”Chương trình đọc , tiếp theo là đúng số nguyên. Giới hạn và là lựa chọn cho ví dụ này: tổng trị tuyệt đối không vượt , nằm trong long long. Đây không phải giới hạn toán học của SOS DP.
#include <cstddef>#include <iostream>#include <string>#include <vector>
std::vector<long long> sos_dp(std::vector<long long> values, int k) { for (int bit = 0; bit < k; ++bit) { const std::size_t flag = std::size_t{1} << bit; for (std::size_t mask = 0; mask < values.size(); ++mask) { if ((mask & flag) != 0) { values[mask] += values[mask ^ flag]; } } } return values;}
int main() { int k; if (!(std::cin >> k) || k < 0 || k > 20) { std::cerr << "Expected integer k in [0, 20]\n"; return 1; } const std::size_t count = std::size_t{1} << k; std::vector<long long> a(count); for (long long& value : a) { if (!(std::cin >> value) || value < -1000000000LL || value > 1000000000LL) { std::cerr << "Expected 2^k integers in [-10^9, 10^9]\n"; return 1; } } std::string extra; if (std::cin >> extra) { std::cerr << "Unexpected extra input\n"; return 1; } for (long long value : sos_dp(a, k)) { std::cout << value << '\n'; }}Các hàm ở trên giả sử độ dài mảng bằng . Hàm main bảo đảm điều kiện này; khi dùng riêng hàm sos_dp, người gọi phải bảo đảm hợp lệ, đúng kích thước mảng và không tràn tổng.
Đối chiếu các ví dụ đầu vào
Section titled “Đối chiếu các ví dụ đầu vào”Ví dụ đầu tiên của bản gốc:
227 2 2004 0Kết quả, theo thứ tự mask từ đến :
272920312033Các ví dụ còn lại giúp phân biệt quy ước mảng và dữ liệu nhập không hợp lệ. Chương trình mới báo lỗi với dữ liệu thiếu hoặc thừa; muốn thêm số thì phải làm rõ điều đó trước khi tính.
| Tình huống từ bản gốc | Cách dùng trong bản này |
|---|---|
, dãy 1 2 3 4 5 | Báo thừa dữ liệu; dãy hợp lệ 1 2 3 4 cho 1 3 4 10 |
, chỉ có 27 2 2004 | Báo thiếu dữ liệu; thêm năm số thì được 27 29 2031 2033 27 29 2031 2033 |
| , không có phần tử nào | Vẫn cần một phần tử ; nhập 0 rồi 0 thì kết quả là 0 |
| Báo lỗi trước khi dịch bit hoặc cấp phát | |
| Vượt giới hạn của chương trình, báo lỗi trước khi cấp phát |
Không thể suy ra rằng mọi trình biên dịch sẽ phát sinh cùng một ngoại lệ cho phép dịch với âm hoặc quá lớn. Tránh thực hiện phép dịch không hợp lệ ngay từ đầu.
Độ phức tạp và các lỗi dễ gặp
Section titled “Độ phức tạp và các lỗi dễ gặp”| Cách tính | Thời gian | Bộ nhớ ngoài đầu vào |
|---|---|---|
| Hai vòng lặp, kiểm tra bằng AND | ||
| Đệ quy bốn nhánh | ||
| Duyệt trực tiếp từng submask | ||
| SOS DP theo bit | với bản sao kết quả |
Với , vòng DP kiểm tra mask và thực hiện đúng phép cộng. Với , chỉ có một phần tử và không có bước bit nào. Các giới hạn trên dùng mô hình phép toán số nguyên có kích thước cố định; số nguyên lớn cần tính thêm chi phí từng phép cộng.
- Không quên tập rỗng:
- Cày trâu bắt đầu từ mảng kết quả toàn ; SOS DP bắt đầu từ bản sao của
- Nếu , thì , không phải
- Cấp phát phần tử tăng rất nhanh: riêng số nguyên 64 bit cần GiB, chưa tính mảng khác
- Chỉ kiểm tra giới hạn phép dịch là chưa đủ; cần xét cả bộ nhớ và thời gian
- Nếu tính theo modulo, áp dụng modulo khi cộng và chuẩn hóa số âm theo yêu cầu bài toán
Tự kiểm tra
Section titled “Tự kiểm tra”- Với , hãy tính bằng tay
- Với
mask = 5(101), liệt kê mọi tập con theo thứ tự giảm dần - Nếu mọi , phụ thuộc vào số bit bật như thế nào?
Đáp án
101,100,001,000, tức- , vì mỗi bit bật có hai lựa chọn: giữ hoặc bỏ
Nguồn và ghi chú biên soạn
Section titled “Nguồn và ghi chú biên soạn”- Bản gốc:
es.ac.vn/docs/wiki/dp/dp_sos/dp_sos.mdvà các tệp được include; frontmatter ghi giấy phép Public domain - Tác giả bản gốc: SPyofgame; biên tập viên: tuandebu; người đóng góp: ngpin04
- Tham khảo đối chiếu phần bổ sung: USACO Guide: Sum over Subsets DP, gồm duyệt submask và cập nhật tổng theo bit
Bản này giữ các ví dụ số của nguồn, viết lại sơ đồ thành cây chia nhóm nhỏ, sửa đối số bị thiếu trong lời gọi đệ quy và phân biệt lỗi nhập liệu với việc chủ động thêm số . Các khối MkDocs, trường NULL và lời khẳng định kiểm thử không kèm bằng chứng của bản gốc được thay bằng nội dung đọc được trong trình đọc hiện tại. Đây là bản biên soạn hỗ trợ bằng AI; chưa ghi nhận phê duyệt chuyên môn độc lập cho bản biên soạn này.