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

Chia Ruộng

Chia Ruộng
Nguồn: Codeforces Group Độ khó: Khá Sinh dãy tam phân Meet in the middle Hash table

Đề bài

Trong làng Omega có 1 phú ông rất giàu có. Tài sản của ông rất nhiều trâu, bò, ruộng vườn. Một hôm ông gọi 3 người con đã đến tuổi trưởng thành đến ông chia cho một số ruộng để làm ăn. Ông có hàng trăm mẫu ruộng nhưng ông chỉ chọn $n$ thửa ruộng để chia, thửa thứ $i$ có diện tích $a_i$ m². Hỏi rằng phú ông có bao nhiêu cách chia đều $n$ thửa ruộng cho 3 con sao cho các thửa ruộng được chia phải giữ nguyên diện tích.

Input:

  • Dòng 1: ghi số $n$ là số thửa ruộng của phú ông đem chia $(1 \le n \le 20)$.
  • Dòng 2: ghi các số $a_i$ là diện tích của thửa ruộng thứ $i$ $(100 \le a_i \le 5000)$.

Output:

  • Dòng thứ nhất ghi số cách chia, nếu không thể chia được thì ghi $-1$.
  • Các dòng sau trong trường hợp chia được, mỗi dòng là 01 cách chia với quy ước người thứ nhất có tên là "A", người thứ hai có tên là "B", người thứ ba có tên là "C" và phải tương ứng với thứ tự của các thửa ruộng được chia.

Lưu ý: Nếu có cách chia thì in ra các cách chia tăng dần theo thứ tự từ điển. Như trong ví dụ đề cho các chuỗi trên từng dòng theo thứ tự từ điển: AABBC < AACCB < BBAAC < BBCCA < CCAAB < CCBBA.

Subtask:

  • 40% số test đầu tiên có $n \le 15$.
  • 60% số test còn lại không có giới hạn gì thêm.
Ví dụ – input
5
100 200 250 50 300
Ví dụ – output
6
AABBC
AACCB
BBAAC
BBCCA
CCAAB
CCBBA

Giải thích ví dụ: tổng diện tích là $100 + 200 + 250 + 50 + 300$ $= 900$, mỗi người con phải nhận đúng $300$. Ví dụ cách AABBC: A nhận $100 + 200$, B nhận $250 + 50$, C nhận $300$.

Lời mở đầu

Bài này mình được một bạn học viên hỏi nên sẵn tiện mình viết bài giải chi tiết để nhiều bạn được tham khảo luôn. Hy 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

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

Vét cạnSinh hết dãy tam phân – O(N · 3^N) là TLE

Với bài chia đất này nếu làm theo kiểu sinh ra các dãy tam phân (do có 3 trường hợp: mỗi thửa ruộng giao cho A, B hoặc C), thì liệt kê ra hết các dãy tam phân rồi cứ 0 là ứng với thằng A, 1 là ứng với thằng B, 2 là ứng với thằng C, như thế tính tổng từng thằng xem trường hợp nào thỏa mãn thì đưa vào đáp án.

Nếu làm như thế độ phức tạp sẽ là $O(N \cdot 3^N)$. Với $N$ tối đa $20$ thì $20 \cdot 3^{20} = 69\,735\,688\,020$, xem như gần 70 tỷ, như thế chắc chắn sẽ TLE rồi vì đã vượt quá ngưỡng $10^8$ (tức là 100 triệu) phép tính của 1 giây.

Thì bài này cũng vẫn làm theo tư tưởng xét dãy tam phân nhưng phải kết hợp thêm với kỹ thuật meet in the middle hay còn gọi là phân đôi tập để tối ưu hóa thêm.

Kiến thứcKỹ thuật Meet in the middle (phân đôi tập)

Định nghĩa sơ lược
Meet in the middle là một kĩ thuật tìm kiếm được sử dụng để giải quyết những vấn đề có đầu vào nhỏ, nhưng không nhỏ đến mức mà ta có thể sử dụng vét cạn để giải quyết. Kĩ thuật này sẽ có điểm khá giống với chia để trị: ta sẽ chia vấn đề thành 2, giải quyết chúng riêng rẽ rồi hợp nhất lại để ra được kết quả. Nhưng mà nó sẽ khác nhau ở chỗ là ở kĩ thuật chia để trị ta sẽ chia bài toán lớn thành các bài toán con, còn ở kĩ thuật meet in the middle thì ta sẽ không làm được như vậy vì sau khi chia vấn đề ra làm 2 thì nó sẽ không còn có cấu trúc giống như bài toán ban đầu.

Em có thể tham khảo thêm bài viết của GeeksforGeeks hoặc bài viết mới của VNOI Wiki về kỹ thuật này.

Em có thể hiểu đại khái như sau: giả sử bài toán cho $N = 20$ thì hãy tách ra làm 2 phần với mỗi phần có $N = 10$, vậy nếu dùng dãy tam phân ta sẽ có $3^{10}$ dãy bên này kết nối với $3^{10}$ dãy bên kia. Em sẽ hiểu rằng cứ mỗi dãy trong $3^{10}$ dãy bên này sẽ xét nó với toàn bộ $3^{10}$ dãy bên kia.

Nhìn lại bài "tiền tệ" quen thuộc

Ví dụ trước đó anh nhớ em đã từng giải bài toán tiền tệ: Bà mẹ có 2 người con là A và B và cần chia $N$ tờ tiền $(N \le 20)$ cho 2 người con sao cho tổng tiền mỗi người con nhận được đều bằng nhau, yêu cầu in ra chi tiết tất cả các cách có thể chia. Test ví dụ đề cho:

Input bài tiền tệ
6
1 2 2 5 10 10
Output bài tiền tệ
4
A A A B A B
A A A B B A
B B B A A B
B B B A B A

Thì có phải là em giải quyết bằng cách sinh ra tất cả các dãy nhị phân có chiều dài $N$, bài cho $N$ tối đa $20$ nên tổng cộng sẽ có $2^{20} = 1\,048\,576$ (hơn 1 triệu) dãy nhị phân, và như thế em sẽ xét vào từng dãy, nếu tổng tiền tại các vị trí số 0 bằng tổng tiền tại các vị trí số 1 thì đó là 1 đáp án thỏa mãn. Tính ra độ phức tạp sẽ là $O(N \cdot 2^N)$, khi $N = 20$ thì sẽ là $20 \cdot 2^{20}$, hơn 20 triệu thôi và vẫn nhỏ hơn ngưỡng $10^8$ của 1 giây nên không sợ bị TLE.

Thì nếu bài tiền tệ này mà em ứng dụng kỹ thuật phân đôi tập vào cũng sẽ giúp chạy nhanh hơn nha. Nhìn lại test ví dụ $N = 6$, chia đôi ra tức là $6 / 2 = 3$, tức là mỗi nửa có $2^3 = 8$ dãy nhị phân: 000, 001, 010, 011, 100, 101, 110, 111. Cứ với 1 dãy bên này sẽ xét với toàn bộ 8 dãy bên kia:

000 ghép với: 000|000 000|001 000|010 000|011 000|100 000|101 000|110 000|111
001 ghép với: 001|000 001|001 001|010 001|011 001|100 001|101 001|110 001|111
010 ghép với: 010|000 010|001 010|010 010|011 010|100 010|101 010|110 010|111

…và cứ thế tới dãy 111 bên này.

Thì khi phân ra làm 2 như vậy, với 1 nửa bên kia em lưu các kết quả vào map và em chỉ việc xét 1 nửa bên này, rồi ứng với từng phần tử của 1 nửa bên này em check vào map để xét với bên kia. Như thế tính ra độ phức tạp thay vì $O(2^N)$ thì chỉ còn $O(2^{N/2} \cdot \log(2^{N/2}))$, tối ưu hơn rất nhiều:

$N = 20$Vét cạnPhân đôi tập
Bài tiền tệ (nhị phân)$2^{20} = 1\,048\,576$$2^{10} \cdot \log_2(2^{10}) = 10\,240$
Bài chia ruộng (tam phân)$20 \cdot 3^{20} \approx 70$ tỷ$10 \cdot 3^{10} \cdot \log_2(3^{10})$ $\approx 9\,359\,045$

Em thấy nhanh hơn cả 100 lần không? Với bài chia ruộng chỉ cần tầm 9 triệu mấy phép tính thì yên tâm AC trong 1 giây. Cái kỹ thuật phân đôi tập này rất hay, em tìm hiểu rồi ứng dụng vào bài chia ruộng nhé.

Ý tưởngÁp dụng vào bài chia ruộng

Để anh nói cụ thể hơn ở bài chia đất này như sau, đầu tiên em chia đôi $n$ ra: $n1 = n / 2$, $n2 = n - n1$. Em tạo ra các dãy tam phân có chiều dài $n1$ và $n2$, quy ước theo ABC cho dễ đi ha. Ví dụ $n = 5$ thì $n1 = 2$, $n2 = 3$: có $3^2 = 9$ dãy cho nửa đầu và $3^3 = 27$ dãy cho nửa sau:

n1 = 2 (9 dãy): AA AB AC BA BB BC CA CB CC
n2 = 3 (27 dãy): AAA AAB AAC ABA ABB ABC ACA ACB ACC BAA BAB BAC BBA BBB BBC BCA BCB BCC CAA CAB CAC CBA CBB CBC CCA CCB CCC

Em tạo ra cấu trúc Key có 3 thành phần là sumA, sumB, sumC. Khi xét các dãy ứng với $n2$ thì em phải hiểu là nó ứng với $n2$ phần tử cuối của mảng. Ví dụ test đề có $n = 5$ thì mảng $a$ có các vị trí $a[0],$ $a[1],$ $a[2],$ $a[3],$ $a[4]$, khi xét dãy $n2 = 3$ phần tử cuối mà giả sử dãy là CAB thì có nghĩa là: chọn $a[2]$ cho thằng C, chọn $a[3]$ cho thằng A, chọn $a[4]$ cho thằng B. Khi đó $sumA = 50$, $sumB = 300$, $sumC = 250$.

Cứ thế em tạo ra cái hash table lưu hết trạng thái Key{sumA, sumB, sumC} của các dãy xét ra, và value của nó chính là (danh sách) chuỗi trạng thái của dãy, ví dụ khi xét "CAB" thì: key = {50, 300, 250} và value chứa "CAB".

Gọi $sum$ là tổng các phần tử của mảng, nếu thấy $sum$ không chia hết cho 3 thì ta kết luận $-1$ ngay, còn nếu chia hết cho 3 thì ta tính $sum$ /= 3 (phần mỗi người con phải nhận). Sau khi xét hết toàn bộ $3^{n2}$ dãy tam phân, em quay lại sinh ra $3^{n1}$ dãy tam phân và cũng tính ra được $\{sumA, sumB, sumC\}$ của từng dãy. Nếu $sumA \le sum$, $sumB \le sum$ và $sumC \le sum$ thì mới thỏa, khi đó ta tìm kiếm trong hash table xem có tồn tại các dãy mà tại đó

Khóa cần tìm
$Key = \{sum - sumA,\ sum - sumB,\ sum - sumC\}$

và lấy value của nó ra ghép với chuỗi dãy tam phân của $n1$ thì đó là 1 đáp án, cứ thế đưa hết các đáp án vào vector, sau cùng in ra nó.

Vì sao đáp án tự động tăng dần theo thứ tự từ điển? Mình sinh các dãy tam phân theo kiểu "đếm" từ AA…A tới CC…C nên chúng xuất hiện theo đúng thứ tự từ điển. Nửa đầu $s1$ được duyệt theo thứ tự từ điển, và với mỗi $s1$ thì các chuỗi nửa sau trong cùng 1 value cũng được thêm vào theo thứ tự từ điển, nên chuỗi ghép $s1 + s2$ đã tăng dần sẵn, không cần sắp xếp lại.

Em nhập diện tích các thửa ruộng rồi bấm Tiếp để xem 2 giai đoạn: lưu nửa sau vào hash table, rồi duyệt nửa đầu và tra khóa còn thiếu:

Ví dụ nhanh:

100%Code Meet in the middle + unordered_map với Key tự định nghĩa

Nó chỉ khó ở chỗ khi em dùng hash table thì dữ liệu hiện tại có Key là struct có 3 thành phần số nguyên, lúc này em phải tự cài đặt cơ chế hash cho nó. Nếu chỉ có 1 số nguyên thì nó còn biết hash, nhưng ở đây nó đang là 1 cấu trúc có 3 thành phần số nguyên thì nó sẽ không biết hash thế nào. Giải pháp ở đây hoặc là em tự tạo hash table, hoặc em dùng hash table có trong thư viện thì phải chỉ định rõ phép so sánh bằng (operator==) và hàm hash (KeyHasher) cho nó, em có thể tham khảo tại câu hỏi này trên StackOverflow.

Đó thì toàn bộ ý tưởng là như vậy, anh gửi luôn source code của anh đã làm đúng theo ý tưởng anh nói ở trên để nếu em không làm được thì tham khảo, đã nộp thành công trên web. Và cũng nói rõ là code này chưa có clean đâu nha em, vì em xem sẽ thấy có rất nhiều phần code bị trùng nhau, nếu chia hàm rồi gọi lại nhìn sẽ gọn gàng hơn nhưng anh tranh thủ code nhanh ra nên để hết code trong hàm main. Bản Python thì dùng luôn dict với key là tuple (sumA, sumB, sumC), Python tự hash được tuple nên đỡ phải cài đặt.

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

struct Key
{
  int first, second, third;

  bool operator==(const Key &other) const{
      return first == other.first && second == other.second && third == other.third;
  }
};

// https://stackoverflow.com/questions/17016175/c-unordered-map-using-a-custom-class-type-as-the-key
struct KeyHasher
{
  size_t operator()(const Key& k) const
  {
    return ((hash<int>()(k.first) ^ (hash<int>()(k.second) << 1)) >> 1) ^ (hash<int>()(k.third) << 1);
  }
};

int main(){
    ios::sync_with_stdio(0);
    cin.tie(0);
    int n;
    cin >> n;
    int a[n], sum = 0;
    for(int i = 0; i < n; ++i){
        cin >> a[i];
        sum += a[i];
    }
    if(sum % 3 != 0){
        cout << -1;
        return 0;
    }
    sum /= 3;
    vector<string> res;
    int n1 = n / 2;
    int n2 = n - n1;
    unordered_map<Key, vector<string>, KeyHasher> mp;
    string s(n2, 'A');
    bool flag = true;
    while(flag){
        flag = false;
        int sumA = 0, sumB = 0, sumC = 0;
        for(int i = 0; i < n2; ++i){
            if(s[i] == 'A'){
                sumA += a[n1 + i];
            }
            else if(s[i] == 'B'){
                sumB += a[n1 + i];
            }
            else{
                sumC += a[n1 + i];
            }
        }
        if(sumA <= sum && sumB <= sum && sumC <= sum){
            mp[{sumA, sumB, sumC}].push_back(s);
        }
        for(int i = n2 - 1; i >= 0; --i){
            if(s[i] != 'C'){
                s[i]++;
                flag = true;
                for(int j = i + 1; j < n2; ++j){
                    s[j] = 'A';
                }
                break;
            }
        }
    }

    string s1(n1, 'A');
    flag = true;
    while(flag){
        flag = false;
        int sumA = 0, sumB = 0, sumC = 0;
        for(int i = 0; i < n1; ++i){
            if(s1[i] == 'A'){
                sumA += a[i];
            }
            else if(s1[i] == 'B'){
                sumB += a[i];
            }
            else{
                sumC += a[i];
            }
        }
        if(sumA <= sum && sumB <= sum && sumC <= sum){
            auto it = mp.find({sum - sumA, sum - sumB, sum - sumC});
            if(it != mp.end()){
                for(int i = 0; i < it->second.size(); ++i){
                    res.push_back(s1 + it->second[i]);
                }
            }
        }
        for(int i = n1 - 1; i >= 0; --i){
            if(s1[i] != 'C'){
                s1[i]++;
                flag = true;
                for(int j = i + 1; j < n1; ++j){
                    s1[j] = 'A';
                }
                break;
            }
        }
    }
    if(res.size() == 0){
        cout << -1;
    }
    else{
        cout << res.size();
        for(int i = 0; i < res.size(); ++i){
            cout << '\n' << res[i];
        }
    }
    return 0;
}
import sys
input = sys.stdin.readline

n = int(input())
a = list(map(int, input().split()))
s = sum(a)

if s % 3 != 0:
    print(-1)
    sys.exit(0)

target = s // 3
res = []

n1 = n // 2
n2 = n - n1

# sinh n2 phần tử
mp = {}      # key: (sumA, sumB, sumC) -> list các chuỗi

s2 = ['A'] * n2
flag = True
while flag:
    flag = False
    sumA = sumB = sumC = 0
    for i in range(n2):
        if s2[i] == 'A':
            sumA += a[n1 + i]
        elif s2[i] == 'B':
            sumB += a[n1 + i]
        else:
            sumC += a[n1 + i]
    if sumA <= target and sumB <= target and sumC <= target:
        key = (sumA, sumB, sumC)
        mp.setdefault(key, []).append(''.join(s2))

    # tăng chuỗi base-3 A/B/C
    for i in range(n2 - 1, -1, -1):
        if s2[i] != 'C':
            s2[i] = chr(ord(s2[i]) + 1)  # A→B, B→C
            flag = True
            for j in range(i + 1, n2):
                s2[j] = 'A'
            break

# sinh n1 phần tử
s1 = ['A'] * n1
flag = True
while flag:
    flag = False
    sumA = sumB = sumC = 0
    for i in range(n1):
        if s1[i] == 'A':
            sumA += a[i]
        elif s1[i] == 'B':
            sumB += a[i]
        else:
            sumC += a[i]

    if sumA <= target and sumB <= target and sumC <= target:
        need = (target - sumA, target - sumB, target - sumC)
        if need in mp:
            pre = ''.join(s1)
            for tail in mp[need]:
                res.append(pre + tail)

    # tăng chuỗi base-3 A/B/C
    for i in range(n1 - 1, -1, -1):
        if s1[i] != 'C':
            s1[i] = chr(ord(s1[i]) + 1)
            flag = True
            for j in range(i + 1, n1):
                s1[j] = 'A'
            break

if not res:
    print(-1)
else:
    print(len(res))
    for r in res:
        print(r)
C++▶ Chạy tay từng dòng codeMeet in the middle: lưu 3^n2 dãy nửa sau vào hash table, duyệt 3^n1 dãy nửa đầu và tra khóa còn thiếu.
Dữ liệu mẫu:
1#include <bits/stdc++.h>
2using namespace std;
3
4struct Key
5{
6 int first, second, third;
7
8 bool operator==(const Key &other) const{
9 return first == other.first && second == other.second && third == other.third;
10 }
11};
12
13// https://stackoverflow.com/questions/17016175/c-unordered-map-using-a-custom-class-type-as-the-key
14struct KeyHasher
15{
16 size_t operator()(const Key& k) const
17 {
18 return ((hash<int>()(k.first) ^ (hash<int>()(k.second) << 1)) >> 1) ^ (hash<int>()(k.third) << 1);
19 }
20};
21
22int main(){
23 ios::sync_with_stdio(0);
24 cin.tie(0);
25 int n;
26 cin >> n;
27 int a[n], sum = 0;
28 for(int i = 0; i < n; ++i){
29 cin >> a[i];
30 sum += a[i];
31 }
32 if(sum % 3 != 0){
33 cout << -1;
34 return 0;
35 }
36 sum /= 3;
37 vector<string> res;
38 int n1 = n / 2;
39 int n2 = n - n1;
40 unordered_map<Key, vector<string>, KeyHasher> mp;
41 string s(n2, 'A');
42 bool flag = true;
43 while(flag){
44 flag = false;
45 int sumA = 0, sumB = 0, sumC = 0;
46 for(int i = 0; i < n2; ++i){
47 if(s[i] == 'A'){
48 sumA += a[n1 + i];
49 }
50 else if(s[i] == 'B'){
51 sumB += a[n1 + i];
52 }
53 else{
54 sumC += a[n1 + i];
55 }
56 }
57 if(sumA <= sum && sumB <= sum && sumC <= sum){
58 mp[{sumA, sumB, sumC}].push_back(s);
59 }
60 for(int i = n2 - 1; i >= 0; --i){
61 if(s[i] != 'C'){
62 s[i]++;
63 flag = true;
64 for(int j = i + 1; j < n2; ++j){
65 s[j] = 'A';
66 }
67 break;
68 }
69 }
70 }
71
72 string s1(n1, 'A');
73 flag = true;
74 while(flag){
75 flag = false;
76 int sumA = 0, sumB = 0, sumC = 0;
77 for(int i = 0; i < n1; ++i){
78 if(s1[i] == 'A'){
79 sumA += a[i];
80 }
81 else if(s1[i] == 'B'){
82 sumB += a[i];
83 }
84 else{
85 sumC += a[i];
86 }
87 }
88 if(sumA <= sum && sumB <= sum && sumC <= sum){
89 auto it = mp.find({sum - sumA, sum - sumB, sum - sumC});
90 if(it != mp.end()){
91 for(int i = 0; i < it->second.size(); ++i){
92 res.push_back(s1 + it->second[i]);
93 }
94 }
95 }
96 for(int i = n1 - 1; i >= 0; --i){
97 if(s1[i] != 'C'){
98 s1[i]++;
99 flag = true;
100 for(int j = i + 1; j < n1; ++j){
101 s1[j] = 'A';
102 }
103 break;
104 }
105 }
106 }
107 if(res.size() == 0){
108 cout << -1;
109 }
110 else{
111 cout << res.size();
112 for(int i = 0; i < res.size(); ++i){
113 cout << '\n' << res[i];
114 }
115 }
116 return 0;
117}

Giải thích một số chỗ trong code

  1. ios::sync_with_stdio(0); cin.tie(0); 2 dòng đầu tiên của hàm main sẽ giúp đẩy nhanh tốc độ nhập xuất dữ liệu nếu các bạn đang code theo ngôn ngữ C++ (để hiểu vì sao thì các bạn cứ gõ mấy từ này lên Google là sẽ ra rất nhiều bài viết giải thích nhé), bài này có thể phải in ra rất nhiều dòng đáp án nên ta hãy để 2 dòng này vào để chạy nhanh hơn. Tất nhiên nếu các bạn code theo cú pháp của C thì chẳng cần mấy dòng đó làm gì, tuy nhiên thường khi code các bài lập trình thi đấu thế này ta dùng C++ vì cú pháp ngắn gọn cũng như nhiều hỗ trợ của nó, vậy nên bạn nào code C++ thì nhớ thêm 2 dòng này vào đầu hàm main nhé.
  2. Sinh dãy tam phân kế tiếp: duyệt từ cuối chuỗi về đầu, gặp ký tự đầu tiên khác 'C' thì tăng nó lên 1 ('A' → 'B', 'B' → 'C'), các ký tự phía sau đặt lại hết thành 'A', giống hệt phép cộng 1 trong hệ cơ số 3. Nếu cả chuỗi toàn 'C' thì flag vẫn là false, vòng while kết thúc.
  3. mp[{sumA, sumB, sumC}] có value là vector<string> vì nhiều dãy nửa sau có thể cho ra cùng 1 bộ tổng. Chỉ lưu những dãy có cả 3 tổng đều $\le sum$, các dãy khác chắc chắn không ghép được.
  4. Khi $n = 1$ thì $n1 = 0$, chuỗi s1 rỗng vẫn được xét đúng 1 lần (vòng while chạy 1 lần với 3 tổng bằng 0) nên code vẫn đúng.
Không gian$O(3^{N/2})$
Thời gian$O(N \cdot 3^{N/2})$

Sinh $3^{n2}$ dãy, mỗi dãy tốn $O(n2)$ để tính tổng và $O(1)$ trung bình để thêm vào hash table; tương tự $3^{n1}$ dãy nửa đầu, mỗi dãy tra hash table $O(1)$. Tổng cộng cỡ $O(N \cdot 3^{N/2})$ cộng với số dòng đáp án phải in ra. Với $N = 20$ thì $20 \cdot 3^{10} \approx 1{,}2 \cdot 10^6$ phép tính, nhẹ nhàng.

Ghi chú kiểm tra của mình: code C++ và Python là của mình trong bài giải gốc (chỉ sửa định dạng). Cả 2 bản đã được kiểm tra với 300 bộ test ngẫu nhiên ($n \le 9$, diện tích là bội của 50 để có nhiều cách chia) so với chương trình vét cạn sinh đủ $3^n$ dãy: khớp 100% cả số cách lẫn thứ tự từ điển. Với $n = 20$ ngẫu nhiên, bản C++ chạy khoảng 0.04 giây, bản Python khoảng 0.4 giây. Riêng test cực đoan kiểu 18 thửa $200$ và 2 thửa $300$ có tới hơn 31 triệu cách chia, khi đó thời gian chủ yếu nằm ở việc in ra hơn 600 MB đáp án (C++ khoảng 4.5 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