Hướng dẫn thuật toán

Trùm CUỐI 2025-12-02 1:22:24 2025-12-02 1:25:16

Link đề bài

Nhận xét

Đây là một bài toán kinh điển trong lập trình thi đấu, thường được giải quyết bằng phương pháp Quy hoạch động sử dụng Bitmask (Bitmask Dynamic Programming) hoặc Đệ quy có nhớ (Memoization) do giới hạn của N khá nhỏ ( N \le 21 ).


1. Phân tích đề bài

  • Yêu cầu: Đếm số cách ghép cặp hoàn hảo ( N chàng trai với N cô gái) sao cho tất cả các cặp đều hợp nhau (tương ứng với a[i][j] = 1 ).
  • Đầu vào: Số N ( 1 \le N \le 21 ) và ma trận tương hợp A[N][N] .
  • Đầu ra: Số lượng cách ghép cặp hợp lệ.
  • Ràng buộc quan trọng: N \le 21 . Ràng buộc này cho phép chúng ta sử dụng các thuật toán có độ phức tạp mũ, cụ thể là O(N \cdot 2^N) .

2. Ý tưởng thuật toán: Quy hoạch động Bitmask

N nhỏ, ta có thể dùng một Bitmask (mặt nạ bit) để biểu diễn trạng thái của các cô gái đã được ghép cặp.

  • Ta sẽ cố gắng ghép cặp lần lượt từng chàng trai một, từ Chàng trai 1 đến Chàng trai N .
  • Bitmask sẽ là một số nguyên có N bit, trong đó:
    • Bit thứ j bằng 1 nếu Cô gái j đã được ghép cặp.
    • Bit thứ j bằng 0 nếu Cô gái j chưa được ghép cặp.

Định nghĩa trạng thái DP

Ta định nghĩa mảng DP như sau:

\text{DP}[\text{mask}] = \text{Số cách ghép cặp hợp lệ cho } k \text{ chàng trai đầu tiên với tập hợp } k \text{ cô gái được biểu diễn bởi } \text{mask}.

Trong đó, k chính là số lượng bit 1 (set bits) trong mask.

3. Các bước cơ bản của thuật toán

Bước 1: Khởi tạo

  • Khai báo mảng dp có kích thước 2^N .
  • Khởi tạo dp[0] = 1. (Trạng thái ban đầu: 0 chàng trai được ghép với 0 cô gái, có 1 cách duy nhất).
  • Khởi tạo các giá trị dp khác bằng 0.

Bước 2: Chuyển trạng thái (Transition)

Ta sẽ duyệt qua tất cả các trạng thái (mask) từ 0 đến 2^N - 2 .

  1. Duyệt qua các mask: for mask from 0 to (1 << N) - 2

  2. Xác định chàng trai tiếp theo:

    • Gọi i là chỉ số của chàng trai tiếp theo cần ghép cặp.
    • i chính là số lượng bit 1 hiện có trong mask. (Ví dụ: Nếu mask có 3 bit 1, tức là 3 cô gái đã được ghép, thì chàng trai tiếp theo cần ghép là chàng trai thứ 4, hay chỉ số i=3 nếu ta dùng chỉ số từ 0).
    • Lưu ý: Nếu bạn dùng chỉ số 1-based cho chàng trai, thì i sẽ là \text{popcount}(\text{mask}) + 1 .
  3. Duyệt qua các cô gái chưa được ghép:

    • for j from 0 to N - 1
    • Kiểm tra điều kiện: a. Cô gái j chưa được ghép: (Bit thứ j trong mask là 0). Tức là: (mask & (1 << j)) == 0. b. Chàng trai i và Cô gái j hợp nhau: A[i][j] == 1.
  4. Cập nhật DP:

    • Nếu cả hai điều kiện trên đều thỏa mãn, ta có thể ghép (i, j) .
    • Trạng thái mới: new_mask = mask | (1 << j).
    • Cập nhật số cách:

      \text{DP}[\text{new_mask}] = \text{DP}[\text{new_mask}] + \text{DP}[\text{mask}]

Bước 3: Kết quả

Kết quả cuối cùng là giá trị của \text{DP}[2^N - 1] (mask mà tất cả các bit đều là 1, tức là tất cả N cô gái đã được ghép).

4. Lưu ý khi triển khai bằng C++ hoặc Python

Mục C++ Python
Kiểu dữ liệu Dùng long long cho mảng dp vì số cách có thể lớn. Số nguyên Python tự động lớn, không cần lo lắng.
Kích thước mảng DP long long dp[1 << 21]; dp = [0] * (1 << N)
Đếm số bit 1 Sử dụng hàm có sẵn: __builtin_popcount(mask) (rất nhanh). Dùng phương thức: bin(mask).count('1') hoặc tự viết hàm đếm.
Truy cập ma trận Cần đảm bảo chỉ số i (chàng trai) và j (cô gái) khớp với cách bạn lưu ma trận A (0-based hay 1-based). Tương tự C++.

Ví dụ về cách xác định chàng trai i trong vòng lặp DP (C++):

// i là chỉ số của chàng trai tiếp theo (0-based)
int i = __builtin_popcount(mask); 

// Duyệt qua các mask
for (int mask = 0; mask < (1 << N) - 1; ++mask) {
    if (dp[mask] == 0) continue; // Bỏ qua trạng thái không thể đạt được

    int boy_index = __builtin_popcount(mask); // Chàng trai thứ "popcount(mask)" (0-based)

    // Duyệt qua các cô gái
    for (int girl_index = 0; girl_index < N; ++girl_index) {
        // 1. Cô gái chưa được ghép VÀ 2. Hai người hợp nhau
        if (!(mask & (1 << girl_index)) && A[boy_index][girl_index] == 1) {
            int new_mask = mask | (1 << girl_index);
            dp[new_mask] += dp[mask];
        }
    }
}