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 →

Song ca (Đề tự luyện HSG Tin quốc gia – Bài 1)

Nguồn: Đề tự luyện HSG Tin học quốc gia – Bài 1 (đề tự soạn)Độ khó: KháBitmaskĐổi góc nhìn: đếm theo tậpTổng trên tập chaSOS DP

Đề bài

Song ca

Đội văn nghệ của thầy Sơn có $N$ học sinh, thầy có danh sách $K$ bản vọng cổ. Bạn thứ $i$ cho biết một xâu nhị phân độ dài $K$: ký tự thứ $j$ là 1 nếu bạn đó thuộc bản thứ $j$. Độ ăn ý của một cặp hai bạn khác nhau là số bản mà cả hai cùng thuộc.

Tính $D$ = độ ăn ý lớn nhất của một cặp, và $C$ = số cặp (không tính thứ tự) có độ ăn ý đúng bằng $D$.

Dữ liệu vào: dòng đầu $N, K$ $(2 \le N \le 2\cdot 10^5;\ 1 \le K \le 22)$; $N$ dòng sau, mỗi dòng một xâu 0/1 độ dài $K$.

Kết quả: hai số $D$ và $C$.

  • Subtask 1 (20%): $N \le 2000$.
  • Subtask 2 (20%): $K \le 10$.
  • Subtask 3 (25%): $K \le 17$.
  • Subtask 4 (35%): không có ràng buộc gì thêm.

Giới hạn thời gian: C++ 1 giây, Python/PyPy 3 giây.

Input
6 5
10111
01101
11110
10110
01011
00011
Output
3 3
Giải thích

Ba cặp (1, 3), (1, 4), (3, 4) cùng thuộc ba bản $1, 3, 4$ nên có độ ăn ý $3$; không cặp nào ăn ý từ $4$ trở lên. Ví dụ cặp (2, 5) chỉ cùng thuộc bản $2$ và $5$.

Dành cho học viênNộp bài trên ClueOJ

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é. Bộ test có chia subtask và có cả những test "bẫy", nên code đúng ý tưởng nhưng sai chi tiết nhỏ cũng sẽ bị trừ điểm đó.

Nộp bài Song ca

Lời mở đầu

Mỗi bạn là một xâu nhị phân độ dài $K$, tức là một số nguyên $a_i$ trong đoạn $[0, 2^K)$ (ta đọc xâu như số nhị phân, ký tự đầu là bit cao nhất). Khi đó các bản mà hai bạn cùng thuộc chính là các bit $1$ của a[i] & a[j], và độ ăn ý là số bit 1 của số đó (hàm __builtin_popcount).

Đề hỏi một bài toán "trên mọi cặp" với $N = 2\cdot 10^5$, tức là khoảng $2\cdot 10^{10}$ cặp – không thể duyệt. Nhưng $K \le 22$ lại rất nhỏ: chỉ có $2^{22} \approx 4$ triệu tập bản khác nhau. Nút thắt của bài là chuyển từ "đếm cặp học sinh" sang "đếm theo tập bản vọng cổ":

Nhận xét chính
Gọi $g[S]$ là số bạn thuộc mọi bản trong tập $S$ (xâu của bạn đó "chứa" $S$). Một cặp có độ ăn ý $\ge d$ khi và chỉ khi có một tập $S$ gồm $d$ bản mà cả hai bạn đều chứa. Vậy $D$ là $|S|$ lớn nhất mà $g[S] \ge 2$.
Thay vì hỏi "cặp này chung bao nhiêu bản?", ta hỏi "tập bản S này có bao nhiêu bạn cùng thuộc?" Bạn 1Bạn 3Bạn 4 1 0 1 1 11 1 1 1 01 0 1 1 0 bản: 1 2 3 4 5 S = {1, 3, 4} (xâu 10110), |S| = 3 g[S] = 3 bạn chứa S: bạn 1, 3, 4 → có C(3, 2) = 3 cặp ăn ý ≥ 3 D = |S| lớn nhất có g[S] ≥ 2; C = tổng C(g[S], 2) trên các S có |S| = D (giải thích ở Cách 3).

Đổi góc nhìn: từ "cặp bạn" sang "tập bản vọng cổ mà cả nhóm cùng thuộc".

Lộ trình:

Cách 1Duyệt mọi cặp – O(N²) · 20%

Đổi mỗi xâu thành số nguyên a[i] (đọc từ trái sang: a = a*2 + bit). Với mỗi cặp $i < j$, độ ăn ý là popcount(a[i] & a[j]). Giữ giá trị lớn nhất best và số cặp đạt nó: gặp giá trị lớn hơn thì đặt lại bộ đếm, gặp bằng thì tăng bộ đếm.

Với $N \le 2000$ có khoảng $2\cdot 10^6$ cặp – rất nhanh. Nhưng với $N = 2\cdot 10^5$ là $2\cdot 10^{10}$ cặp, quá chậm.

#include <bits/stdc++.h>
using namespace std;
int main() {
    ios::sync_with_stdio(false); cin.tie(nullptr);
    int n, k;
    cin >> n >> k;
    vector<int> a(n);
    for (int i = 0; i < n; i++) {
        string s; cin >> s;
        a[i] = 0;
        for (char c : s) a[i] = a[i] * 2 + (c - '0');
    }
    int best = -1; long long cnt = 0;
    for (int i = 0; i < n; i++)
        for (int j = i + 1; j < n; j++) {
            int d = __builtin_popcount(a[i] & a[j]);   // so ban cung thuoc
            if (d > best) { best = d; cnt = 0; }
            if (d == best) cnt++;
        }
    cout << best << " " << cnt << "\n";
    return 0;
}
import sys


def main():
    data = sys.stdin.buffer.read().split()
    n = int(data[0])
    a = [int(data[2 + i], 2) for i in range(n)]
    best = -1; cnt = 0
    for i in range(n):
        x = a[i]
        for j in range(i + 1, n):
            d = bin(x & a[j]).count("1")   # so ban cung thuoc
            if d > best:
                best = d; cnt = 1
            elif d == best:
                cnt += 1
    print(best, cnt)


main()
Không gian$O(N)$
Thời gian$O(N^2)$

Cách 2Gộp các bạn cùng "kiểu" – O(4^K) · 40%

Khi $K \le 10$ chỉ có tối đa $2^{10} = 1024$ xâu khác nhau, trong khi $N$ có thể tới $2\cdot 10^5$: rất nhiều bạn thuộc giống hệt nhau. Gọi $f[x]$ là số bạn có xâu đúng bằng $x$. Ta duyệt cặp kiểu thay vì cặp bạn:

  • hai kiểu khác nhau $x < y$: có $f[x]\cdot f[y]$ cặp bạn, mỗi cặp ăn ý popcount(x & y);
  • hai bạn cùng kiểu $x$: có $\binom{f[x]}{2} = \frac{f[x](f[x]-1)}{2}$ cặp, mỗi cặp ăn ý popcount(x).

Số cặp kiểu tối đa là $\frac{(2^K)^2}{2}$, với $K = 10$ chỉ khoảng nửa triệu. Code ghép thêm Cách 1 cho trường hợp $N \le 2000$.

Coi chừng tràn số
Khi mọi bạn thuộc giống nhau, $C = \binom{N}{2} \approx 2\cdot 10^{10}$, vượt xa int. Biến đếm và cả tích $f[x]\cdot f[y]$ phải là long long.
#include <bits/stdc++.h>
using namespace std;
int main() {
    ios::sync_with_stdio(false); cin.tie(nullptr);
    int n, k;
    cin >> n >> k;
    vector<int> a(n);
    for (int i = 0; i < n; i++) {
        string s; cin >> s;
        a[i] = 0;
        for (char c : s) a[i] = a[i] * 2 + (c - '0');
    }
    int best = -1; long long cnt = 0;
    auto upd = [&](int d, long long w) {
        if (w == 0) return;
        if (d > best) { best = d; cnt = 0; }
        if (d == best) cnt += w;
    };
    if (n <= 2000) {                       // Cach 1: duyet moi cap ban
        for (int i = 0; i < n; i++)
            for (int j = i + 1; j < n; j++) upd(__builtin_popcount(a[i] & a[j]), 1);
    } else {                               // K nho: duyet moi cap "kieu" (xau)
        vector<long long> f(1 << k, 0);
        for (int x : a) f[x]++;
        vector<int> ms;
        for (int m = 0; m < (1 << k); m++) if (f[m]) ms.push_back(m);
        for (size_t i = 0; i < ms.size(); i++) {
            upd(__builtin_popcount(ms[i]), f[ms[i]] * (f[ms[i]] - 1) / 2);  // hai ban cung kieu
            for (size_t j = i + 1; j < ms.size(); j++)
                upd(__builtin_popcount(ms[i] & ms[j]), f[ms[i]] * f[ms[j]]);
        }
    }
    cout << best << " " << cnt << "\n";
    return 0;
}
import sys


def main():
    data = sys.stdin.buffer.read().split()
    n = int(data[0]); k = int(data[1])
    a = [int(data[2 + i], 2) for i in range(n)]
    best = -1; cnt = 0
    if n <= 2000:                          # Cach 1: duyet moi cap ban
        for i in range(n):
            x = a[i]
            for j in range(i + 1, n):
                d = bin(x & a[j]).count("1")
                if d > best:
                    best = d; cnt = 1
                elif d == best:
                    cnt += 1
    else:                                  # K nho: duyet moi cap "kieu" (xau)
        f = [0] * (1 << k)
        for x in a:
            f[x] += 1
        pc = [bin(m).count("1") for m in range(1 << k)]
        ms = [m for m in range(1 << k) if f[m]]
        for i in range(len(ms)):
            x = ms[i]; fx = f[x]
            if fx >= 2:
                d = pc[x]; w = fx * (fx - 1) // 2
                if d > best:
                    best = d; cnt = w
                elif d == best:
                    cnt += w
            for j in range(i + 1, len(ms)):
                y = ms[j]
                d = pc[x & y]
                if d > best:
                    best = d; cnt = fx * f[y]
                elif d == best:
                    cnt += fx * f[y]
    print(best, cnt)


main()
Không gian$O(2^K + N)$
Thời gian$O(N^2)$ hoặc $O(N + 4^K)$

Cách 3Đếm theo tập bản – O(3^K) · 65%

Với $K = 17$ có hơn $10^5$ kiểu khác nhau, Cách 2 phải duyệt $5\cdot 10^9$ cặp kiểu. Ta cần thoát hẳn khỏi việc duyệt cặp.

Bước 1 – tính $g[S]$. Ta nói xâu $T$ chứa tập $S$ nếu mọi bit $1$ của $S$ cũng là bit $1$ của $T$, tức là (T & S) == S ($T$ là tập cha của $S$). Khi đó

$$g[S] = \sum_{T \supseteq S} f[T].$$

Duyệt mọi tập cha $T$ của $S$ bằng mẹo T = (T + 1) | S: bắt đầu từ $T = S$, mỗi bước cộng thêm 1 rồi "ép" lại các bit của $S$, ta đi qua các tập cha của $S$ theo thứ tự tăng dần cho tới $2^K - 1$.

Bước 2 – tìm $D$. Theo nhận xét chính, $D = \max\{\,|S| : g[S] \ge 2\,\}$. Lưu ý phải là $\ge 2$: một bạn thuộc nhiều bản nhất chưa chắc có "bạn diễn" thuộc nhiều như thế (ở ví dụ, bạn 1 và bạn 3 thuộc 4 bản nhưng $D = 3$).

Bước 3 – đếm $C$. Xét một cặp có độ ăn ý đúng $D$, tập bản chung của nó là $A$ với $|A| = D$. Cặp này được đếm trong $\binom{g[S]}{2}$ với mọi $S \subseteq A$. Nhưng nếu chỉ lấy các $S$ có đúng $|S| = D$ thì chỉ còn $S = A$: cặp được đếm đúng một lần. Ngược lại, hai bạn cùng chứa một $S$ với $|S| = D$ thì có độ ăn ý $\ge D$, mà không có cặp nào vượt $D$, nên độ ăn ý đúng bằng $D$. Vậy

$$C = \sum_{|S| = D} \binom{g[S]}{2}.$$
Vì sao không cần bao hàm – loại trừ?
Muốn đếm số cặp có tập chung đúng bằng $S$ thường phải dùng bao hàm – loại trừ trên các tập cha. Ở đây ta chỉ cần tầng lớn nhất $|S| = D$: trên tầng đó "chứa $S$" và "chung đúng $S$" là một.

Mô phỏng dưới đây tính $g[S]$ cho từng tập $S$ (theo thứ tự số bản giảm dần) rồi chọn $D$ và cộng ra $C$:

Ví dụ nhanh:

Mỗi tập $S$ có $2^{K - |S|}$ tập cha, nên tổng số bước là $\sum_S 2^{K-|S|} = 3^K$ (mỗi bản: thuộc $S$, không thuộc $S$ nhưng thuộc $T$, hoặc không thuộc cả hai). Với $K = 17$, $3^{17} \approx 1{,}3\cdot 10^8$ – vừa đủ nhanh; nhưng $3^{22} \approx 3\cdot 10^{10}$ thì quá chậm. Code ghép thêm Cách 1 khi $N \le 2000$ (vì khi đó $K$ có thể tới 22).

#include <bits/stdc++.h>
using namespace std;
int main() {
    ios::sync_with_stdio(false); cin.tie(nullptr);
    int n, k;
    cin >> n >> k;
    int full = (1 << k) - 1;
    vector<int> a(n);
    for (int i = 0; i < n; i++) {
        string s; cin >> s;
        a[i] = 0;
        for (char c : s) a[i] = a[i] * 2 + (c - '0');
    }
    int best = -1; long long cnt = 0;
    if (n <= 2000) {                         // Cach 1: duyet moi cap ban
        for (int i = 0; i < n; i++)
            for (int j = i + 1; j < n; j++) {
                int d = __builtin_popcount(a[i] & a[j]);
                if (d > best) { best = d; cnt = 0; }
                if (d == best) cnt++;
            }
    } else {
        vector<long long> f(full + 1, 0), g(full + 1, 0);
        for (int x : a) f[x]++;
        // g[S] = so ban thuoc moi ban trong S = tong f[T] voi T chua S
        for (int S = 0; S <= full; S++)
            for (int T = S; ; T = (T + 1) | S) {
                g[S] += f[T];
                if (T == full) break;
            }
        for (int S = 0; S <= full; S++)
            if (g[S] >= 2) best = max(best, __builtin_popcount(S));
        for (int S = 0; S <= full; S++)
            if (__builtin_popcount(S) == best) cnt += g[S] * (g[S] - 1) / 2;
    }
    cout << best << " " << cnt << "\n";
    return 0;
}
import sys


def main():
    data = sys.stdin.buffer.read().split()
    n = int(data[0]); k = int(data[1])
    a = [int(data[2 + i], 2) for i in range(n)]
    best = -1; cnt = 0
    if n <= 2000:                            # Cach 1: duyet moi cap ban
        for i in range(n):
            x = a[i]
            for j in range(i + 1, n):
                d = bin(x & a[j]).count("1")
                if d > best:
                    best = d; cnt = 1
                elif d == best:
                    cnt += 1
    else:
        full = (1 << k) - 1
        f = [0] * (full + 1)
        for x in a:
            f[x] += 1
        g = [0] * (full + 1)
        # g[S] = tong f[T] voi T chua S (duyet moi tap cha T cua S)
        for S in range(full + 1):
            T = S; tot = 0
            while True:
                tot += f[T]
                if T == full:
                    break
                T = (T + 1) | S
            g[S] = tot
        pc = [bin(m).count("1") for m in range(full + 1)]
        for S in range(full + 1):
            if g[S] >= 2 and pc[S] > best:
                best = pc[S]
        for S in range(full + 1):
            if pc[S] == best:
                cnt += g[S] * (g[S] - 1) // 2
    print(best, cnt)


main()
Không gian$O(2^K)$
Thời gian$O(N + 3^K)$

Cách 4Tổng trên tập cha bằng SOS DP – O(K · 2^K) · 100%

Cách 3 lãng phí ở chỗ mỗi $g[S]$ được tính lại từ đầu. Ý tưởng "Sum over Subsets" (ở đây là tổng trên tập cha): cộng dồn lần lượt theo từng bit, giống như tính tổng tiền tố trên một bảng $K$ chiều, mỗi chiều chỉ có 2 giá trị $0, 1$.

Bắt đầu với f[m] = số bạn có xâu đúng bằng $m$. Lần lượt với từng bit $b = 0, 1, \ldots, K-1$:

for (int m = 0; m < (1 << k); m++)
    if (!(m >> b & 1)) f[m] += f[m | (1 << b)];

Bất biến: sau khi xử lý xong các bit $0..b$, f[m] bằng số bạn có xâu $T$ thỏa mãn: $T$ trùng $m$ ở các bit $> b$, và $T$ chứa $m$ ở các bit $\le b$. Khi xử lý bit $b$: nếu bit $b$ của $m$ là $0$ thì $T$ được phép có bit $b$ bằng $0$ (phần đã có trong f[m]) hoặc $1$ (phần nằm trong f[m | 1<<b]); nếu bit $b$ của $m$ là $1$ thì $T$ bắt buộc có bit $b$ bằng $1$ – giữ nguyên. Sau bit cuối cùng, f[m] chính là $g[m]$.

Thứ tự vòng lặp
Vòng ngoài phải là bit, vòng trong là tập. Đổi ngược hai vòng thì một bạn có thể bị cộng nhiều lần theo nhiều đường khác nhau. Còn trong một lượt bit $b$, ta chỉ đọc f[m | 1<<b] (là ô có bit $b$ bằng 1, không bị sửa trong lượt này), nên thứ tự duyệt $m$ không quan trọng.

Xem các lượt cộng dồn chạy trên dữ liệu nhỏ (mỗi lượt một bit; lượt $b$ ứng với bản thứ $K - b$ vì ký tự đầu xâu là bit cao nhất):

Ví dụ nhanh:

Ghép lại với bước 2 và bước 3 của Cách 3, ta có lời giải hoàn chỉnh. Tổng cộng $K \cdot 2^{K-1} \approx 4{,}6\cdot 10^7$ phép cộng với $K = 22$ – chưa tới 0,1 giây.

#include <bits/stdc++.h>
using namespace std;
int main() {
    ios::sync_with_stdio(false); cin.tie(nullptr);
    int n, k;
    cin >> n >> k;
    vector<long long> f(1 << k, 0);
    for (int i = 0; i < n; i++) {
        string s; cin >> s;
        int m = 0;
        for (char c : s) m = m * 2 + (c - '0');
        f[m]++;
    }
    for (int b = 0; b < k; b++)
        for (int m = 0; m < (1 << k); m++)
            if (!(m >> b & 1)) f[m] += f[m | (1 << b)];
    int best = -1;
    for (int m = 0; m < (1 << k); m++)
        if (f[m] >= 2) best = max(best, __builtin_popcount(m));
    long long cnt = 0;
    for (int m = 0; m < (1 << k); m++)
        if (__builtin_popcount(m) == best) cnt += f[m] * (f[m] - 1) / 2;
    cout << best << " " << cnt << "\n";
    return 0;
}
import sys


def main():
    data = sys.stdin.buffer.read().split()
    n = int(data[0]); k = int(data[1])
    size = 1 << k
    f = [0] * size
    for i in range(2, 2 + n):
        f[int(data[i], 2)] += 1
    # cong don theo tap cha: sau buoc nay f[m] = so ban thuoc moi ban trong m
    for b in range(k):
        step = 1 << b
        for m in range(size):
            if not m & step:
                f[m] += f[m | step]
    pc = [0] * size                          # pc[m] = so bit 1 cua m
    for m in range(1, size):
        pc[m] = pc[m >> 1] + (m & 1)
    best = -1
    for m in range(size):
        if f[m] >= 2 and pc[m] > best:
            best = pc[m]
    cnt = 0
    for m in range(size):
        if pc[m] == best:
            x = f[m]
            cnt += x * (x - 1) // 2
    sys.stdout.write("%d %d\n" % (best, cnt))


main()
Không gian$O(2^K)$
Thời gian$O(N \cdot K + K \cdot 2^K)$
Mẹo cho bản Python
Bản Python ở trên viết vòng lặp đôi $K \cdot 2^K$ thật đơn giản (chỉ dùng một danh sách phẳng, không tạo danh sách phụ) để PyPy 3 dịch ra mã máy chạy nhanh: khoảng $9 \cdot 10^7$ phép cộng chỉ mất chưa tới 1 giây. Nhớ chọn PyPy 3 khi nộp, Python 3 thường sẽ chậm hơn nhiều.
C++▶ Chạy tay từng dòng codeTheo dõi mảng f trước và sau từng lượt cộng dồn, rồi D (best) và C (cnt).
Dữ liệu mẫu:
1#include <bits/stdc++.h>
2using namespace std;
3int main() {
4 ios::sync_with_stdio(false); cin.tie(nullptr);
5 int n, k;
6 cin >> n >> k;
7 vector<long long> f(1 << k, 0);
8 for (int i = 0; i < n; i++) {
9 string s; cin >> s;
10 int m = 0;
11 for (char c : s) m = m * 2 + (c - '0');
12 f[m]++;
13 }
14 for (int b = 0; b < k; b++)
15 for (int m = 0; m < (1 << k); m++)
16 if (!(m >> b & 1)) f[m] += f[m | (1 << b)];
17 int best = -1;
18 for (int m = 0; m < (1 << k); m++)
19 if (f[m] >= 2) best = max(best, __builtin_popcount(m));
20 long long cnt = 0;
21 for (int m = 0; m < (1 << k); m++)
22 if (__builtin_popcount(m) == best) cnt += f[m] * (f[m] - 1) / 2;
23 cout << best << " " << cnt << "\n";
24 return 0;
25}

Bộ test chấm "bắt" những lỗi nào?

Cách làmSub 1 (20%)Sub 2 (20%)Sub 3 (25%)Sub 4 (35%)Điểm
Cách 1 – Duyệt mọi cặp O(N²)ĐúngTLETLETLE20
Cách 2 – Gộp theo kiểu xâu O(4^K)ĐúngĐúngTLETLE40
Cách 3 – Đếm theo tập, duyệt tập cha O(3^K)ĐúngĐúngĐúngTLE65
Cách 4 – SOS DP (sol.cpp)ĐúngĐúngĐúngĐúng100
Sai: dùng int cho f và biến đếm (tràn số)11/116/96/87/1076,6
Sai: chọn D theo g[S] ≥ 1 (quên cặp cần hai bạn)5/114/92/82/1031,2
Sai: tham lam từng bit tìm AND lớn nhất theo giá trị số5/113/90/80/1015,8
Chậm: duyệt mọi tập con của từng bạn O(Σ 2^|a_i|)10/119/96/81/10≈ 60

Ghi chú của mình: các cách C++ ở trên đã được chấm thử với đúng bộ test của bài: bộ test có 38 test (11 + 9 + 8 + 10), mỗi cách ở trên đạt đúng số điểm công bố, còn ba lời giải sai và một lời giải "đúng nhưng chậm" bị bắt như bảng trên. Lời giải chuẩn sol.cpp chạy test lâu nhất hết khoảng 0,3 giây; bản Python sol.py cho cùng đáp án trên mọi test (trên CPython mất khoảng 3 giây ở test K = 22, các em nhớ chọn PyPy 3 khi nộp). "TLE" nghĩa là chạy quá 3 lần giới hạn thời gian; riêng dòng "Chậm" được chấm với đúng giới hạn 1 giây. Lỗi tràn số chỉ lộ ra ở các test có rất nhiều bạn cùng thuộc một bộ bản tối ưu, nên nó vẫn "ăn" được khá nhiều điểm – đừng để mất điểm oan như vậy nhé!

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