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
Thi thử miễn phí
🏆 Thi thử HSG Tin học tuần này trên ClueOJ
Mở công khai cho mọi người · đề do thầy Nguyễn Việt Nam Sơn biên soạn
Lịch thi, đề cũ & lời giải →

Two Sets II

Two Sets II
Nguồn: CSES Problem Set Độ khó: Trung bình Quy hoạch động Đếm Modulo Nghịch đảo modulo

Đề bài

Two Sets II – Hai tập hợp II (CSES 1093)

Giới hạn thời gian: 1.0 giây. Giới hạn bộ nhớ: 512 MB.

Nhiệm vụ của bạn là đếm số cách chia các số $1, 2, \ldots, n$ thành hai tập hợp có tổng bằng nhau.

Ví dụ, nếu $n = 7$ thì có bốn cách chia:

  • $\{1, 3, 4, 6\}$ và $\{2, 5, 7\}$
  • $\{1, 2, 5, 6\}$ và $\{3, 4, 7\}$
  • $\{1, 2, 4, 7\}$ và $\{3, 5, 6\}$
  • $\{1, 6, 7\}$ và $\{2, 3, 4, 5\}$

Dữ liệu vào: Dòng duy nhất chứa một số nguyên $n$.

Kết quả: In ra đáp án sau khi chia lấy dư cho $10^9 + 7$.

Ràng buộc: $1 \le n \le 500$.

Ví dụ – input
7
Ví dụ – output
4

Lời mở đầu

Bài này là một bài kinh điển trong bộ CSES: nhìn thì là bài đếm tập con có tổng cho trước (quy hoạch động quen thuộc), nhưng có thêm một "cái bẫy" ở bước cuối – phải chia đôi một số đã bị chia lấy dư. Để vượt qua nó mình sẽ dẫn các bạn qua nghịch đảo modulo, định lý Fermat nhỏ và luỹ thừa nhanh. Hy vọng mang đến được giá trị cho các bạn 😇.

Bạn nào muốn thử nộp bài để hệ thống chấm xem có đúng hay không thì link bài đây nhé:

Link nộp bài

Bấm vào nút bên dưới để nộp bài và được hệ thống CLUE OJ chấm tự động.

Nộp bài trên CLUE OJ

Tóm tắt đề bài một cách đơn giản nhất: ta cần đếm số cách chia các số $1, 2, \ldots, n$ $(1 \le n \le 500)$ thành 2 tập hợp có tổng bằng nhau, in ra kết quả chia dư cho $10^9 + 7$.

Giải thích ví dụ $n = 7$: tổng các số từ $1$ tới $7$ là $28$, nên mỗi tập phải có tổng đúng $14$. Có đúng 4 cách chia như đề liệt kê. Để ý mỗi cách chia gồm 2 tập con cùng có tổng $14$, nên tổng cộng có $8$ tập con có tổng $14$:

{1, 3, 4, 6} và {2, 5, 7}
{1, 2, 5, 6} và {3, 4, 7}
{1, 2, 4, 7} và {3, 5, 6}
{1, 6, 7} và {2, 3, 4, 5}

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

Nhận xétĐưa về đếm tập con, rồi chia đôi

Đáp án = một nửa số tập con có tổng bằng nửa tổng dãy

Ở ví dụ trên, tổng của các tập hợp đều bằng $14$, bằng một nửa tổng các số từ $1$ tới $7$ (là $28$). Có $8$ tập con có tổng $14$, mà đáp án là $4$. Vì sao vậy?

  • Ta cần chia dãy thành 2 tập có tổng bằng nhau, mà cả dãy có tổng $S(n)$, vậy mỗi tập phải có tổng $S(n) / 2$.
  • Mỗi cách chia ứng với đúng 2 tập con có tổng $S(n)/2$ (tập này và phần bù của nó). Ngược lại, mỗi tập con có tổng $S(n)/2$ thì phần bù cũng có tổng $S(n)/2$.
Nhận xét chính
Số cách chia = một nửa số tập con của $\{1, 2, \ldots, n\}$ có tổng bằng $S(n) / 2$.

Tính tổng 1 + 2 + … + n

Có thể dùng một vòng for, độ phức tạp $O(n)$ với $n \le 500$ thì cũng không sao. Nhưng ta hoàn toàn tính được trong $O(1)$ bằng công thức $S(n) = \dfrac{n(n + 1)}{2}$.

Bạn nào nhớ luôn thì càng tốt vì công thức này dùng trong rất nhiều bài. Còn nếu không nhớ, hoặc cần tính tổng dãy như $0 + 2 + 4 + \ldots + 2n$ hay $1 + 3 + 5 + \ldots + (2n - 1)$, thì chỉ cần nhớ đúng 1 công thức tổng $n$ số hạng đầu của cấp số cộng ($u_n = u_{n-1} + d$):

Tổng n số hạng đầu của cấp số cộng
$S_n = \dfrac{n\,(u_1 + u_n)}{2} = n \cdot u_1 + \dfrac{n(n - 1)}{2} \cdot d$
Công thức thứ 2 dùng khi không biết $u_n$. Với dãy $1, 2, \ldots, n$ thì $u_1 = 1$, $u_n = n$, $d = 1$ nên $S(n) = \dfrac{n(1 + n)}{2}$.

Nếu $S(n)$ là số lẻ thì không thể chia đôi được, đáp án chắc chắn là $0$. Còn nếu $S(n)$ chẵn thì ta đếm số tập con có tổng $sum = S(n)/2 = \dfrac{n(n+1)}{4}$.

Cái bẫy: chia đôi sau khi đã chia dư

Trong quá trình đếm, kết quả rất lớn (cỡ $2^{500}$) nên ta phải liên tục chia dư cho $mod = 10^9 + 7$. Theo đồng dư thức $(a + b) \bmod m = \big((a \bmod m) + (b \bmod m)\big) \bmod m$, chia dư trong lúc tính cho kết quả giống hệt tính xong mới chia dư. Phép $+$, $-$, $\times$ đều có đồng dư thức như vậy, nhưng phép chia thì không:

SAI: (a / b) % m = ((a % m) / (b % m)) % m
Ví dụ $m = 7$, $a = 8$, $b = 2$: $(8 / 2) \bmod 7 = 4$, nhưng $(8 \bmod 7) / (2 \bmod 7) = 1 / 2 = 0$ (chia nguyên). Nên lấy số tập con đã chia dư rồi mới / 2 là sai; mà không chia dư thì tràn số. Kẹt!

Nghịch đảo modulo và định lý Fermat nhỏ

Để giải quyết đoạn này ta cần biết về nghịch đảo modulo (modular inverse). Ta không có đồng dư thức cho phép chia nhưng có cho phép nhân, vậy biến phép chia thành phép nhân:

$\dfrac{a}{b} = a \cdot \dfrac{1}{b} = a \cdot b^{-1}$ ⇒ $\left(\dfrac{a}{b}\right) \bmod m = \big((a \bmod m) \cdot (b^{-1} \bmod m)\big) \bmod m$.

Giờ chỉ cần tính $b^{-1} \bmod m$. Định lý Fermat nhỏ phát biểu: "Nếu $p$ là số nguyên tố và $a$ là số nguyên không chia hết cho $p$ thì $a^{p-1} - 1$ chia hết cho $p$", tức là $a^{p-1} \bmod p = 1$. Đề cho $mod = 10^9 + 7$ chính là một số nguyên tố. Nhân cả 2 vế với $a^{-1}$:

Công thức nghịch đảo modulo (m nguyên tố)
$a^{m-1} \bmod m = 1$ ⇒ $(a^{m-1} \cdot a^{-1}) \bmod m = a^{-1} \bmod m$ ⇒ $a^{-1} \bmod m = a^{m-2} \bmod m$.

Bài này $b = 2$ vì ta cần chia đôi. Lưu ý không phải lúc nào $b^{-1}$ cũng tồn tại, nó chỉ tồn tại khi $\gcd(b, m) = 1$; ở đây $m$ nguyên tố nên đương nhiên $\gcd(2, m) = 1$. Vậy $2^{-1} \bmod m = 2^{m-2} \bmod m$ (tính ra là $500000004$; kiểm tra: $2 \cdot 500000004 = 1000000008 \equiv 1 \pmod{10^9+7}$ ✅).

Luỹ thừa nhanh bằng chia để trị

$m - 2 = 10^9 + 5$. Nếu tính $2^{10^9+5}$ rồi mới chia dư thì chắc chắn tràn số. Còn nếu nhân $a \cdot a \cdot a \ldots$ đủ $b$ lần bằng vòng for (vừa nhân vừa chia dư) thì độ phức tạp $O(b) = O(10^9)$, chắc chắn TLE vì 1 giây chỉ chạy được tầm $10^8$ phép tính. Ta cần luỹ thừa nhị phân hoặc chia để trị, chỉ tốn $O(\log_2 b)$. Tách $a^b$ thành 3 trường hợp:

  • $b = 0$: $a^0 = 1$.
  • $b$ chẵn: $a^b = a^{b/2} \cdot a^{b/2}$.
  • $b$ lẻ: lúc này b / 2 trong C++ làm tròn xuống (ví dụ 11 / 2 = 5), nên $a^b = a^{b/2} \cdot a^{b/2} \cdot a$.

Với $a^b \bmod m$ thì cứ áp đồng dư thức của phép nhân vào. Ta viết hàm đệ quy powMod(a, b, m) trả về $a^b \bmod m$:

  • Điều kiện dừng: b == 0 thì trả về $1$.
  • Xử lý chạy: dù $b$ chẵn hay lẻ đều cần $a^{b/2} \bmod m$, nên gọi half = powMod(a, b / 2, m). Rồi gán lại half = (half * half) % m. Nếu $b$ lẻ thì trả về (half * (a % m)) % m, còn $b$ chẵn thì trả về half luôn.
typedef long long ll;
ll powMod(ll a, ll b, ll m) {
    if(b == 0) {
        return 1;
    }
    ll half = powMod(a, b / 2, m);
    half = (half * half) % m;
    if(b % 2) {
        return (half * (a % m)) % m;
    }
    return half;
}

Ví dụ nhỏ: $3^{11}$ đi qua chuỗi lời gọi $b = 11 \to 5 \to 2 \to 1 \to 0$, chỉ 5 lời gọi thay vì 11 phép nhân:

powMod(3, 0) = 1powMod(3, 1) = 1 · 1 · 3 = 3 (b lẻ)powMod(3, 2) = 3 · 3 = 9 (b chẵn)powMod(3, 5) = 9 · 9 · 3 = 243 (b lẻ)powMod(3, 11) = 243 · 243 · 3 = 177147 (b lẻ)

Với $b = 10^9 + 5$ thì chỉ cần khoảng $30$ lời gọi. Các bạn sẽ thấy rõ trong khung chạy tay ở dưới (phần cuối chương trình).

Cách 1Quy hoạch động bảng 2 chiều – O(n · n(n+1)/4)

Giờ chỉ còn vấn đề đếm số tập con có tổng bằng $sum$. Vì các phần tử của dãy là $1, 2, \ldots, n$ nên ở đây $a[i] = i$ luôn. Ta có 4 bước quy hoạch động:

1Bài toán con: $F[i][j]$ = số tập con của $\{1, 2, \ldots, i\}$ có tổng bằng $j$ (đã chia dư).
2Bài toán cơ sở: $F[0][0] = 1$ (tập rỗng có tổng $0$), các ô khác của hàng $0$ bằng $0$.
3Đáp án: $F[n][sum] \cdot 2^{-1} \bmod (10^9 + 7)$.
4Truy hồi: không lấy số $i$, hoặc lấy số $i$ (khi $i \le j$).

Ở bước 4, xét số $i$: nếu không lấy thì số cách là $F[i-1][j]$; nếu lấy số $i$ (chỉ được khi $i \le j$) thì phần còn lại phải có tổng $j - i$ từ các số $1..i-1$, số cách là $F[i-1][j-i]$. Cộng 2 trường hợp lại:

Công thức truy hồi
  • Nếu $i \gt j$: $F[i][j] = F[i-1][j]$
  • Nếu $i \le j$: $F[i][j] = F[i-1][j] + F[i-1][j-i]$
Trường hợp cơ sở $F[0][0] = 1$; nhớ chia dư cho $10^9 + 7$ sau mỗi lần tính.

Minh hoạ: lấp đầy bảng F

Các bạn nhập $n$ rồi bấm Tiếp. Ô vàng là ô đang tính, các ô tím ở hàng trên là $F[i-1][j]$ và $F[i-1][j-i]$ được cộng lại. Ở bước cuối mình liệt kê luôn các cách chia (với $n$ nhỏ):

Ví dụ nhanh:

Và đây là source code của mình về ý tưởng trên cho anh em nào cần tham khảo nha:

#include <bits/stdc++.h>
#define endl '\n'
using namespace std;
typedef long long ll;

const ll mod = (ll)1e9 + 7;

ll powMod(ll a, ll b, ll m) {
    if(b == 0) {
        return 1;
    }
    ll half = powMod(a, b / 2, m);
    half = (half * half) % m;
    if(b % 2) {
        return (half * (a % m)) % m;
    }
    return half;
}

ll f[505][500 * 501 / 4 + 5];

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

    int n;
    cin >> n;
    ll sum = (n * (n + 1)) / 2;
    if(sum % 2) {
        cout << 0;
        return 0;
    }
    sum /= 2;
    f[0][0] = 1;
    for(int i = 1; i <= n; ++i) {
        for(int j = 0; j <= sum; ++j) {
            if(j < i) {
                f[i][j] = f[i - 1][j];
            }
            else {
                f[i][j] = f[i - 1][j] + f[i - 1][j - i];
            }
            f[i][j] %= mod;
        }
    }
    ll res = (f[n][sum] * powMod(2, mod - 2, mod)) % mod;
    cout << res;
    return 0;
}
import sys
sys.setrecursionlimit(10000)

mod = 10**9 + 7

def powMod(a, b, m):
    if b == 0:
        return 1
    half = powMod(a, b // 2, m)
    half = (half * half) % m
    if b % 2:
        return (half * (a % m)) % m
    return half

n = int(input())
s = (n * (n + 1)) // 2
if s % 2:
    print(0)
    sys.exit(0)
s //= 2
f = [[0] * (s + 1) for _ in range(n + 1)]
f[0][0] = 1
for i in range(1, n + 1):
    for j in range(0, s + 1):
        if j < i:
            f[i][j] = f[i - 1][j]
        else:
            f[i][j] = f[i - 1][j] + f[i - 1][j - i]
        f[i][j] %= mod
res = (f[n][s] * powMod(2, mod - 2, mod)) % mod
print(res)
C++▶ Chạy tay từng dòng codeCách 1: bảng f[i][j] đếm tập con của {1..i} có tổng j, rồi nhân với 2^(-1) = powMod(2, mod - 2, mod).
Dữ liệu mẫu:
1#include <bits/stdc++.h>
2#define endl '\n'
3using namespace std;
4typedef long long ll;
5
6const ll mod = (ll)1e9 + 7;
7
8ll powMod(ll a, ll b, ll m) {
9 if(b == 0) {
10 return 1;
11 }
12 ll half = powMod(a, b / 2, m);
13 half = (half * half) % m;
14 if(b % 2) {
15 return (half * (a % m)) % m;
16 }
17 return half;
18}
19
20ll f[505][500 * 501 / 4 + 5];
21
22int main() {
23 ios_base::sync_with_stdio(0);
24 cin.tie(0);
25
26 int n;
27 cin >> n;
28 ll sum = (n * (n + 1)) / 2;
29 if(sum % 2) {
30 cout << 0;
31 return 0;
32 }
33 sum /= 2;
34 f[0][0] = 1;
35 for(int i = 1; i <= n; ++i) {
36 for(int j = 0; j <= sum; ++j) {
37 if(j < i) {
38 f[i][j] = f[i - 1][j];
39 }
40 else {
41 f[i][j] = f[i - 1][j] + f[i - 1][j - i];
42 }
43 f[i][j] %= mod;
44 }
45 }
46 ll res = (f[n][sum] * powMod(2, mod - 2, mod)) % mod;
47 cout << res;
48 return 0;
49}

Giải thích 1 số chỗ trong code ở trên:

  1. ll f[505][500 * 501 / 4 + 5]; ta cần mảng 2 chiều $F[n + 1][sum + 1]$ với $sum = n(n+1)/4$. Mình khai báo mảng toàn cục để tránh tràn bộ nhớ stack: khi mảng tĩnh có số lượng phần tử $\gt 2 \cdot 10^5$ thì anh em nên dùng vector hoặc để biến toàn cục nha. $505 = 500 + 5$ (mình thường cộng thêm 5 cho chắc), $500 \cdot 501 / 4 + 5$ chính là $n(n+1)/4$ cộng thêm 5. Mảng toàn cục còn được tự khởi tạo toàn số $0$, đúng như bài toán cơ sở cần.
  2. ll res = (f[n][sum] * powMod(2, mod - 2, mod)) % mod; chính là số tập con có tổng $sum$ nhân với $2^{-1} \bmod mod$ = powMod(2, mod - 2, mod), rồi lấy tất cả chia dư cho $mod$.

Đánh giá độ phức tạp của cách làm này:

  • Không gian: $O\big((n + 5) \cdot (n(n+1)/4 + 5)\big)$ vì ta chỉ dùng một mảng $F$. Với $n = 500$ là khoảng $505 \cdot 62630 \approx 3.2 \cdot 10^7$ số long long, tầm 253 MB – vẫn trong giới hạn 512 MB của đề nhưng khá "nặng".
  • Thời gian: $O\big(n \cdot n(n+1)/4\big)$ vì phải lấp đầy mảng $F$, cộng thêm $O(\log mod)$ cho powMod. Với $n = 500$ mất tầm $3.14 \cdot 10^7$ phép tính, trường hợp xấu nhất khoảng 0.3 đến 0.4 giây, vẫn dưới 1 giây.
Không gian$O(n^3)$
Thời gian$O(n^3)$

Cách 2Quy hoạch động mảng 1 chiều – ít bộ nhớ, chạy nhanh hơn

Để ý hàng $i$ của bảng chỉ dùng tới hàng $i - 1$. Vậy ta có cách dùng ít bộ nhớ hơn: chỉ tạo mảng $F[n(n+1)/4 + 1]$. Trường hợp cơ sở $F[0] = 1$, và công thức truy hồi:

Công thức truy hồi 1 chiều
Với mỗi $i$ từ $1$ tới $n$, duyệt $j$ từ $sum$ ngược về $i$: $F[j] = F[j] + F[j - i]$.
Mảng $F$ khai báo toàn cục để lúc đầu toàn bộ phần tử bằng $0$.
Vì sao phải duyệt j ngược?
Khi tính $F[j]$ mới, ta cần $F[j - i]$ của hàng cũ ($i - 1$). Duyệt $j$ giảm dần thì ô $j - i$ (nhỏ hơn $j$) chưa bị cập nhật, vẫn là giá trị cũ ✅. Nếu duyệt xuôi, $F[j - i]$ đã bị cập nhật ở hàng $i$ rồi, tức là số $i$ có thể được lấy nhiều lần – thành bài đếm cách đổi tiền với số lượng không giới hạn, sai ❌. Các bạn thử đổi chiều duyệt trong khung minh hoạ dưới đây để thấy nha.

Cách này không chỉ ít bộ nhớ hơn mà cũng chạy nhanh hơn, dù độ phức tạp thời gian vẫn là $O\big(n \cdot n(n+1)/4\big)$. Vì với mảng 2 chiều toàn cục rất to, ta tốn thêm thời gian khởi tạo và truy cập bộ nhớ; chưa kể vòng $j$ ở cách 1 đi từ $0$ đến $sum$, còn ở đây chỉ đi từ $sum$ về $i$. Nhìn chung từ phân tích và lúc nộp bài thì cách này nhanh hơn nha.

Minh hoạ: cập nhật mảng 1 chiều

Ví dụ nhanh:

Sau đây là source code của mình về ý tưởng trên cho anh em nào cần tham khảo nha:

#include <bits/stdc++.h>
#define endl '\n'
using namespace std;
typedef long long ll;

const ll mod = (ll)1e9 + 7;

ll powMod(ll a, ll b, ll m) {
    if(b == 0) {
        return 1;
    }
    ll half = powMod(a, b / 2, m);
    half = (half * half) % m;
    if(b % 2) {
        return (half * (a % m)) % m;
    }
    return half;
}

ll f[500 * 501 / 4 + 5];

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

    int n;
    cin >> n;
    ll sum = (n * (n + 1)) / 2;
    if(sum % 2) {
        cout << 0;
        return 0;
    }
    sum /= 2;
    f[0] = 1;
    for(int i = 1; i <= n; ++i) {
        for(int j = sum; j >= i; --j) {
            f[j] += f[j - i];
            f[j] %= mod;
        }
    }
    ll res = (f[sum] * powMod(2, mod - 2, mod)) % mod;
    cout << res;
    return 0;
}
import sys
sys.setrecursionlimit(10000)

mod = 10**9 + 7

def powMod(a, b, m):
    if b == 0:
        return 1
    half = powMod(a, b // 2, m)
    half = (half * half) % m
    if b % 2:
        return (half * (a % m)) % m
    return half

n = int(input())
s = (n * (n + 1)) // 2
if s % 2:
    print(0)
    sys.exit(0)
s //= 2
f = [0] * (s + 1)
f[0] = 1
for i in range(1, n + 1):
    for j in range(s, i - 1, -1):
        f[j] += f[j - i]
        f[j] %= mod
res = (f[s] * powMod(2, mod - 2, mod)) % mod
print(res)
C++▶ Chạy tay từng dòng codeCách 2: mảng 1 chiều f[j] += f[j - i] với j duyệt ngược từ sum về i, rồi nhân với 2^(-1).
Dữ liệu mẫu:
1#include <bits/stdc++.h>
2#define endl '\n'
3using namespace std;
4typedef long long ll;
5
6const ll mod = (ll)1e9 + 7;
7
8ll powMod(ll a, ll b, ll m) {
9 if(b == 0) {
10 return 1;
11 }
12 ll half = powMod(a, b / 2, m);
13 half = (half * half) % m;
14 if(b % 2) {
15 return (half * (a % m)) % m;
16 }
17 return half;
18}
19
20ll f[500 * 501 / 4 + 5];
21
22int main() {
23 ios_base::sync_with_stdio(0);
24 cin.tie(0);
25
26 int n;
27 cin >> n;
28 ll sum = (n * (n + 1)) / 2;
29 if(sum % 2) {
30 cout << 0;
31 return 0;
32 }
33 sum /= 2;
34 f[0] = 1;
35 for(int i = 1; i <= n; ++i) {
36 for(int j = sum; j >= i; --j) {
37 f[j] += f[j - i];
38 f[j] %= mod;
39 }
40 }
41 ll res = (f[sum] * powMod(2, mod - 2, mod)) % mod;
42 cout << res;
43 return 0;
44}

Đánh giá độ phức tạp của cách làm này:

  • Không gian: chỉ còn $O\big(n(n+1)/4\big) = O(n^2)$ cho mảng $F$ (khoảng $62630$ số với $n = 500$, chỉ cỡ 0.5 MB).
  • Thời gian: vẫn là $O\big(n \cdot n(n+1)/4\big)$, nhưng hằng số nhỏ hơn cách 1.
Không gian$O(n^2)$
Thời gian$O(n^3)$
Bản Python chỉ để tham khảo
Với $n = 500$ phải chạy khoảng $3 \cdot 10^7$ phép cộng, Python thuần chậm hơn C++ rất nhiều: mình đo bản Python cách 2 mất khoảng 8 giây, cách 1 khoảng 13.5 giây, nên sẽ bị TLE với giới hạn 1 giây. Anh em nộp bài thì nên dùng C++ nha.
Ghi chú của mình: code C++ của cả 2 cách đã được so với một chương trình đếm chính xác bằng số nguyên lớn (không chia dư, chia đôi ở cuối) cho mọi $n$ từ $1$ tới $120$, và với $n = 496, 499, 500$ – khớp hết; code C++ và Python cũng được so với code vét cạn duyệt mọi tập con cho $n \le 20$ – khớp hết, đúng ví dụ của đề. Với $n = 500$, cách 1 (C++) chạy khoảng 0.44 giây, cách 2 (C++) khoảng 0.1 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 😇
Tự học thuật toán: khó ở đâu?
Đọc xong một bài giải là bước đầu. Phần lớn học sinh tự học sẽ gặp những trở ngại sau:
  • Không biết học gì trước, học gì sau, học lan man nên kiến thức bị hổng.
  • Bí bài không ai gỡ, ngồi cả buổi vẫn không ra rồi bỏ cuộc.
  • Code chạy được nhưng không đủ điểm, không biết sai ở đâu, chậm ở đâu.
  • Không có đề thi thử và người chấm, nên không biết mình đang ở mức nào.
  • Dễ nản giữa chừng khi không có ai theo sát.
Học cùng thầy Nam Sơn, các trở ngại đó được gỡ bỏ:
  • Lộ trình 111 buổi video sắp sẵn từ dòng code đầu tiên đến đề HSG thật của các tỉnh.
  • Thầy trực tiếp gỡ bí, chữa bài cho từng em qua nhóm Zalo riêng.
  • Hệ thống chấm bài tự động 24/7 và thi thử HSG hằng tuần để biết chính xác mình đang ở đâu.
  • Đóng học phí một lần, thầy kèm lâu dài: từ thi chuyên Tin đến HSG quốc gia và cả khi lên đại học.

Nhận xét