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

Chi Phí Ít Nhất Để Kết Nối Hai Nhóm Điểm

Chi Phí Ít Nhất Để Kết Nối Hai Nhóm Điểm
Nguồn: Codeforces Group Độ khó: Khó Quy hoạch động Bitmask

Đề bài

Bạn được cho hai nhóm điểm: nhóm thứ nhất có size1 điểm, nhóm thứ hai có size2 điểm, và size1 ≥ size2.

Chi phí kết nối giữa hai điểm bất kỳ được cho trong một ma trận size1 × size2, trong đó cost[i][j] là chi phí để kết nối điểm $i$ của nhóm thứ nhất với điểm $j$ của nhóm thứ hai.

Hai nhóm được coi là kết nối nếu mỗi điểm trong cả hai nhóm đều được kết nối với một hoặc nhiều điểm ở nhóm đối diện. Nói cách khác:

  • Mỗi điểm ở nhóm thứ nhất phải được kết nối với ít nhất một điểm ở nhóm thứ hai.
  • Mỗi điểm ở nhóm thứ hai phải được kết nối với ít nhất một điểm ở nhóm thứ nhất.

Yêu cầu: hãy tìm chi phí tối thiểu để kết nối hai nhóm điểm.

Input

  • Dòng đầu tiên chứa hai số nguyên size1 và size2 ($1 \le$ size2 $\le$ size1 $\le 12$) — kích thước của hai nhóm.
  • size1 dòng tiếp theo, mỗi dòng chứa size2 số nguyên cost[i][j] ($0 \le$ cost[i][j] $\le 100$) — ma trận chi phí kết nối.

Output: một số nguyên duy nhất là chi phí tối thiểu tìm được.

Subtask

  • 20% số test đầu tiên có $1 \le$ size2 $\le$ size1 $\le 5$.
  • 80% số test còn lại không có giới hạn gì thêm.
InputOutputGiải thích
2 2
15 96
36 2
17Cách kết nối tối ưu: điểm 1 (nhóm 1) nối với điểm A (nhóm 2) – chi phí 15; điểm 2 (nhóm 1) nối với điểm B (nhóm 2) – chi phí 2. Tổng chi phí = 15 + 2 = 17.
3 3
1 3 5
4 1 1
1 5 3
4Cách kết nối tối ưu: 1–A (1), 2–B (1), 2–C (1), 3–A (1). Tổng = 1 + 1 + 1 + 1 = 4. Lưu ý: một điểm có thể kết nối với nhiều điểm ở nhóm kia.
5 3
2 5 1
3 4 7
8 1 2
6 2 4
3 8 8
10

Lời mở đầu

Bài này mình được một em học viên hỏi và mình đã hướng dẫn chi tiết cách thức giải quyết cho em ấy. Tin rằng sẽ có giá trị với nhiều bạn nên mình chia sẻ đề bài để các bạn thử sức làm và kèm lời giải để bạn nào không giải quyết được thì có thể tham khảo nhen. Hi vọng mang đến được giá trị cho các bạn 😇.

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
Dành cho tất cả mọi ngườiNộp bài trên Codeforces

Bạn nào muốn thử nộp bài để hệ thống chấm xem có đúng hay không thì dùng link này nhé. Chưa có tài khoản thì tạo tài khoản rồi đăng nhập nộp bài thử hen.

Nộp bài trên Codeforces

Tóm tắt đề bài: bạn được cho size1 điểm thuộc nhóm 1, size2 điểm thuộc nhóm 2. Chi phí để nối điểm $i$ của nhóm 1 với điểm $j$ của nhóm 2 là cost[i][j]. Hãy nối các điểm sao cho mọi điểm của nhóm 1 đều được nối với ít nhất 1 điểm nhóm 2, mọi điểm của nhóm 2 đều được nối với ít nhất 1 điểm nhóm 1, và tổng chi phí ít nhất có thể.

100%Quy hoạch động + bitmask

Để biết những điểm nào của nhóm 2 đã được nối, ta có thể dựa vào tập hợp, và để biểu diễn tập hợp ta dùng dãy bit (bitmask): bit thứ $j$ bằng 1 nghĩa là điểm $j$ của nhóm 2 đã được nối. Vì size2 $\le 12$ nên chỉ có tối đa $2^{12} = 4096$ tập.

Gọi dp[i][mask] là chi phí nhỏ nhất khi ta đã xét xong $i$ điểm đầu tiên của nhóm 1 (mỗi điểm đều đã được nối ít nhất 1 lần), và tập các điểm của nhóm 2 đã được nối là mask. Khởi tạo tất cả dp[i][mask] bằng dương vô cực, ngoại trừ dp[0][0] = 0.

Với dp[i][mask] khác dương vô cực, nếu ta quyết định nối điểm tiếp theo (điểm $i$ của nhóm 1, đánh số từ 0) với một tập các điểm nextmask của nhóm 2, thì chi phí cộng thêm là sumCost[i][nextmask] – tổng chi phí nối điểm $i$ với tất cả các điểm trong nextmask. Ta chuyển trạng thái:

dp[i + 1][mask | nextmask] = min(dp[i + 1][mask | nextmask], dp[i][mask] + sumCost[i][nextmask])

Tuy nhiên, nếu làm như này sẽ bị time out vì phải duyệt $i$, duyệt mask và duyệt nextmask: $12 \cdot 4096 \cdot 4096 \approx 2 \cdot 10^8$ phép tính. Ta có nhận xét sau:

Nhận xét giúp tối ưu
Điểm đang xét chỉ cần nối với nextmask là tập con của mask (nối vào những điểm đã được nối rồi), hoặc là tập không giao với mask (chỉ nối vào những điểm chưa được nối). Vì vậy với mỗi mask, ta chỉ duyệt những nextmask như vậy, độ phức tạp giảm xuống rất nhiều.

Ví dụ với size2 = 3 (các điểm A, B, C) và mask hiện tại là {A, C}:

mask hiện tạinextmask là tập con của masknextmask không giao với maskBị bỏ qua
101 = {A, C}{A}, {C}, {A, C}{B}{A, B}, {B, C}, {A, B, C}
Vì sao bỏ qua các nextmask "lai" vẫn đúng? (mình giải thích thêm)

Một nextmask "lai" là tập vừa chứa điểm đã nối, vừa chứa điểm chưa nối. Phần "đã nối" trong đó không giúp phủ thêm điểm nào mới của nhóm 2 mà chỉ tốn thêm chi phí (chi phí $\ge 0$), nên bỏ phần đó đi thì vẫn được kết quả không tệ hơn: chỉ giữ lại phần "chưa nối" (là tập không giao với mask). Phần "đã nối" chỉ cần thiết khi điểm đang xét không có điểm chưa nối nào để nối, mà khi đó nó là tập con của mask – trường hợp này vẫn được xét. Thực ra chỉ cần tập con gồm đúng 1 điểm là đủ, nhưng xét hết các tập con cũng không làm sai.

Em nhập ma trận chi phí (mỗi hàng là 1 điểm của nhóm 1, các hàng cách nhau bởi dấu ;) rồi bấm Tiếp để xem từng bước: mỗi bước lấy 1 trạng thái dp[i][mask] đã đạt được, thử nối điểm thứ $i + 1$ với các nextmask hợp lệ (tập con của mask hoặc không giao với mask) và cập nhật hàng tiếp theo. Cột của bảng ghi tập điểm nhóm 2 đã được nối:

Ví dụ nhanh:

Đáp án là dp[size1][(1 << size2) - 1]: đã xét hết các điểm nhóm 1 và mọi điểm nhóm 2 đều đã được nối.

Đánh giá độ phức tạp: với mỗi mask, số tập con của mask cộng số tập con của phần bù là $2^{|mask|} + 2^{m - |mask|}$; cộng trên mọi mask thì khoảng $2 \cdot 3^{m}$, nhân với số điểm nhóm 1 ra $O(3^{\text{size2}} \cdot \text{size1})$. Với size1 = size2 = 12 là khoảng 12 triệu phép tính.

#include <bits/stdc++.h>
using namespace std;

const int inf = 1000005;
int dp[13][(1 << 12) + 5];
int sumCost[13][(1 << 12) + 5];
vector<int> listMask[(1 << 12) + 5];

int connectTwoGroups(vector<vector<int>> &cost){
    int n = (int)cost.size();
    int m = (int)cost[0].size();

    // Khởi tạo mảng dp và sumCost
    for(int i = 0; i <= n; i++){
        for(int mask = 0; mask < (1 << m); mask++){
            dp[i][mask] = inf;
            sumCost[i][mask] = 0;
        }
    }

    // Tính tổng chi phí cho mỗi điểm i với mỗi mask
    for(int i = 0; i < n; i++){
        for(int mask = 0; mask < (1 << m); mask++){
            for(int j = 0; j < m; j++){
                if((mask & (1 << j)) > 0){
                    sumCost[i][mask] += cost[i][j];
                }
            }
        }
    }

    // Xây dựng danh sách các mask có thể chọn từ mask hiện tại
    for(int mask = 0; mask < (1 << m); mask++){
        listMask[mask].clear();
        for(int nextmask = 1; nextmask < (1 << m); nextmask++){
            if((mask & nextmask) == 0){
                listMask[mask].push_back(nextmask);
                continue;
            }
            if((mask & nextmask) == nextmask){
                listMask[mask].push_back(nextmask);
                continue;
            }
        }
    }

    // Quy hoạch động
    dp[0][0] = 0;
    for(int i = 0; i < n; i++){
        for(int mask = 0; mask < (1 << m); mask++){
            if(dp[i][mask] == inf){
                continue;
            }
            for(int j = 0; j < (int)listMask[mask].size(); j++){
                int nextmask = listMask[mask][j];
                dp[i + 1][mask | nextmask] = min(dp[i + 1][mask | nextmask],
                                                 dp[i][mask] + sumCost[i][nextmask]);
            }
        }
    }

    return dp[n][(1 << m) - 1];
}

int main(){
    ios::sync_with_stdio(0);
    cin.tie(0);

    int size1, size2;
    cin >> size1 >> size2;

    vector<vector<int>> cost(size1, vector<int>(size2));
    for(int i = 0; i < size1; i++){
        for(int j = 0; j < size2; j++){
            cin >> cost[i][j];
        }
    }

    int result = connectTwoGroups(cost);
    cout << result << endl;

    return 0;
}
import sys

def connectTwoGroups(cost):
    n = len(cost)
    m = len(cost[0])
    full = 1 << m
    inf = 10 ** 9

    # sumCost[i][mask]: tổng chi phí nối điểm i (nhóm 1) với các điểm trong mask (nhóm 2)
    sumCost = []
    for i in range(n):
        row = [0] * full
        for mask in range(1, full):
            low = mask & -mask
            row[mask] = row[mask ^ low] + cost[i][low.bit_length() - 1]
        sumCost.append(row)

    # listMask[mask]: các nextmask là tập con của mask hoặc không giao với mask
    listMask = []
    for mask in range(full):
        lst = []
        sub = mask                      # các tập con khác rỗng của mask
        while sub:
            lst.append(sub)
            sub = (sub - 1) & mask
        rest = (full - 1) ^ mask        # các tập con khác rỗng của phần bù
        sub = rest
        while sub:
            lst.append(sub)
            sub = (sub - 1) & rest
        listMask.append(lst)

    dp = [inf] * full
    dp[0] = 0
    for i in range(n):
        ndp = [inf] * full
        sc = sumCost[i]
        for mask in range(full):
            cur = dp[mask]
            if cur == inf:
                continue
            for nextmask in listMask[mask]:
                val = cur + sc[nextmask]
                if val < ndp[mask | nextmask]:
                    ndp[mask | nextmask] = val
        dp = ndp
    return dp[full - 1]

data = sys.stdin.read().split()
size1, size2 = int(data[0]), int(data[1])
cost = [[int(data[2 + i * size2 + j]) for j in range(size2)] for i in range(size1)]
print(connectTwoGroups(cost))
C++▶ Chạy tay từng dòng codeQuy hoạch động bitmask: dp[i][mask], chỉ chuyển sang nextmask là tập con của mask hoặc không giao với mask.
Dữ liệu mẫu:
1#include <bits/stdc++.h>
2using namespace std;
3
4const int inf = 1000005;
5int dp[13][(1 << 12) + 5];
6int sumCost[13][(1 << 12) + 5];
7vector<int> listMask[(1 << 12) + 5];
8
9int connectTwoGroups(vector<vector<int>> &cost){
10 int n = (int)cost.size();
11 int m = (int)cost[0].size();
12
13 // Khởi tạo mảng dp và sumCost
14 for(int i = 0; i <= n; i++){
15 for(int mask = 0; mask < (1 << m); mask++){
16 dp[i][mask] = inf;
17 sumCost[i][mask] = 0;
18 }
19 }
20
21 // Tính tổng chi phí cho mỗi điểm i với mỗi mask
22 for(int i = 0; i < n; i++){
23 for(int mask = 0; mask < (1 << m); mask++){
24 for(int j = 0; j < m; j++){
25 if((mask & (1 << j)) > 0){
26 sumCost[i][mask] += cost[i][j];
27 }
28 }
29 }
30 }
31
32 // Xây dựng danh sách các mask có thể chọn từ mask hiện tại
33 for(int mask = 0; mask < (1 << m); mask++){
34 listMask[mask].clear();
35 for(int nextmask = 1; nextmask < (1 << m); nextmask++){
36 if((mask & nextmask) == 0){
37 listMask[mask].push_back(nextmask);
38 continue;
39 }
40 if((mask & nextmask) == nextmask){
41 listMask[mask].push_back(nextmask);
42 continue;
43 }
44 }
45 }
46
47 // Quy hoạch động
48 dp[0][0] = 0;
49 for(int i = 0; i < n; i++){
50 for(int mask = 0; mask < (1 << m); mask++){
51 if(dp[i][mask] == inf){
52 continue;
53 }
54 for(int j = 0; j < (int)listMask[mask].size(); j++){
55 int nextmask = listMask[mask][j];
56 dp[i + 1][mask | nextmask] = min(dp[i + 1][mask | nextmask],
57 dp[i][mask] + sumCost[i][nextmask]);
58 }
59 }
60 }
61
62 return dp[n][(1 << m) - 1];
63}
64
65int main(){
66 ios::sync_with_stdio(0);
67 cin.tie(0);
68
69 int size1, size2;
70 cin >> size1 >> size2;
71
72 vector<vector<int>> cost(size1, vector<int>(size2));
73 for(int i = 0; i < size1; i++){
74 for(int j = 0; j < size2; j++){
75 cin >> cost[i][j];
76 }
77 }
78
79 int result = connectTwoGroups(cost);
80 cout << result << endl;
81
82 return 0;
83}

Trong code, sumCost[i][mask] được tính sẵn bằng cách duyệt mọi bit của mask; listMask[mask] lưu sẵn danh sách các nextmask hợp lệ của từng mask. Bản Python mình viết thêm dùng mẹo duyệt tập con sub = (sub - 1) & mask để liệt kê nhanh các tập con thay vì thử cả 4096 tập, và chỉ giữ 2 tầng dp để tiết kiệm bộ nhớ.

Mình đã kiểm tra bản C++ và Python với 600 ma trận ngẫu nhiên (so với cách thử mọi tập cạnh khi ma trận nhỏ và với một cách quy hoạch động khác khi ma trận lớn hơn), đều cho cùng kết quả. Với ma trận $12 \times 12$, bản C++ chạy khoảng 0.05 giây, bản Python khoảng 0.9 giây.

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