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 →

Kth Smallest Product of Two Sorted Arrays (LeetCode 2040)

Kth Smallest Product of Two Sorted Arrays (LeetCode 2040)
Nguồn: LeetCode 2040 Độ khó: Khó Tìm kiếm nhị phân Chặt nhị phân kết quả Đếm Toán học

Đề bài

LeetCode 2040 – Kth Smallest Product of Two Sorted Arrays (Tích nhỏ thứ k của hai mảng đã sắp xếp) – độ khó Hard.

Cho hai mảng số nguyên nums1 và nums2 đã được sắp xếp (tăng dần), đánh số từ 0, cùng một số nguyên $k$. Hãy trả về tích nhỏ thứ $k$ (đếm từ 1) trong các tích nums1[i] * nums2[j] với $0 \le i \lt$ nums1.length và $0 \le j \lt$ nums2.length.

Ràng buộc:

  • $1 \le$ nums1.length, nums2.length $\le 5 \cdot 10^4$
  • $-10^5 \le$ nums1[i], nums2[j] $\le 10^5$
  • $1 \le k \le$ nums1.length $\times$ nums2.length
  • nums1 và nums2 đã được sắp xếp.

Ví dụ 1:

Input: nums1 = [2,5], nums2 = [3,4], k = 2
Output: 8

Giải thích: 2 tích nhỏ nhất là:

  • nums1[0] * nums2[0] = 2 * 3 = 6
  • nums1[0] * nums2[1] = 2 * 4 = 8

Tích nhỏ thứ 2 là 8.

Ví dụ 2:

Input: nums1 = [-4,-2,0,3], nums2 = [2,4], k = 6
Output: 0

Giải thích: 6 tích nhỏ nhất là:

  • nums1[0] * nums2[1] = (-4) * 4 = -16
  • nums1[0] * nums2[0] = (-4) * 2 = -8
  • nums1[1] * nums2[1] = (-2) * 4 = -8
  • nums1[1] * nums2[0] = (-2) * 2 = -4
  • nums1[2] * nums2[0] = 0 * 2 = 0
  • nums1[2] * nums2[1] = 0 * 4 = 0

Tích nhỏ thứ 6 là 0.

Ví dụ 3:

Input: nums1 = [-2,-1,0,1,2], nums2 = [-3,-1,2,4,5], k = 3
Output: -6

Giải thích: 3 tích nhỏ nhất là:

  • nums1[0] * nums2[4] = (-2) * 5 = -10
  • nums1[0] * nums2[3] = (-2) * 4 = -8
  • nums1[4] * nums2[0] = 2 * (-3) = -6

Tích nhỏ thứ 3 là -6.

2040. Kth Smallest Product of Two Sorted Arrays – Hard

Given two sorted 0-indexed integer arrays nums1 and nums2 as well as an integer k, return the kth (1-based) smallest product of nums1[i] * nums2[j] where 0 <= i < nums1.length and 0 <= j < nums2.length.

Constraints:

  • 1 <= nums1.length, nums2.length <= 5 * 104
  • -105 <= nums1[i], nums2[j] <= 105
  • 1 <= k <= nums1.length * nums2.length
  • nums1 and nums2 are sorted.

Example 1:

Input: nums1 = [2,5], nums2 = [3,4], k = 2
Output: 8
Explanation: The 2 smallest products are:
- nums1[0] * nums2[0] = 2 * 3 = 6
- nums1[0] * nums2[1] = 2 * 4 = 8
The 2nd smallest product is 8.

Example 2:

Input: nums1 = [-4,-2,0,3], nums2 = [2,4], k = 6
Output: 0
Explanation: The 6 smallest products are:
- nums1[0] * nums2[1] = (-4) * 4 = -16
- nums1[0] * nums2[0] = (-4) * 2 = -8
- nums1[1] * nums2[1] = (-2) * 4 = -8
- nums1[1] * nums2[0] = (-2) * 2 = -4
- nums1[2] * nums2[0] = 0 * 2 = 0
- nums1[2] * nums2[1] = 0 * 4 = 0
The 6th smallest product is 0.

Example 3:

Input: nums1 = [-2,-1,0,1,2], nums2 = [-3,-1,2,4,5], k = 3
Output: -6
Explanation: The 3 smallest products are:
- nums1[0] * nums2[4] = (-2) * 5 = -10
- nums1[0] * nums2[3] = (-2) * 4 = -8
- nums1[4] * nums2[0] = 2 * (-3) = -6
The 3rd smallest product is -6.

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 😇.

Về dữ liệu vào / ra của code trong bài
Trên LeetCode các bạn chỉ cần nộp phần class Solution. Để các bạn chạy thử được trên máy (hoặc trang Chạy code online), mọi code trong bài đều có thêm hàm main đọc từ bàn phím theo định dạng: dòng 1 gồm 3 số $n$, $m$, $k$ (độ dài nums1, độ dài nums2 và $k$); dòng 2 gồm $n$ số của nums1; dòng 3 gồm $m$ số của nums2. In ra một số nguyên là tích nhỏ thứ $k$. Ví dụ 2 sẽ nhập là:
4 2 6
-4 -2 0 3
2 4

Tóm tắt đề bài: cho 2 dãy số $a$ (độ dài $n$) và $b$ (độ dài $m$), đã sắp xếp tăng dần. Hãy liệt kê tích $a[i] \cdot b[j]$ với mọi cặp $(i, j)$, tức là $n \cdot m$ tích. Ta sắp xếp các số đã liệt kê tăng dần, hãy đưa ra số đứng thứ $K$ trong danh sách sau khi sắp xếp (số nhỏ thứ $K$, tính cả các số bằng nhau).

Ví dụ 2: các tích là $(-4) \cdot 2 = -8$, $(-4) \cdot 4 = -16$, $(-2) \cdot 2 = -4$, $(-2) \cdot 4 = -8$, $0, 0$, $3 \cdot 2 = 6$, $3 \cdot 4 = 12$. Sắp xếp: $-16, -8, -8, -4, 0, 0, 6, 12$ ⇒ số thứ 6 là 0. Cái khó của bài là có số âm: nhân với số âm thì thứ tự bị đảo ngược, nên tích nhỏ nhất không nằm ở "góc" nào cố định cả.

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

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

Cách 1Làm theo đề: liệt kê hết rồi sắp xếp – O(nm log(nm))

Cách tự nhiên nhất là làm đúng như đề nói: tính hết $n \cdot m$ tích (nhớ dùng long long, vì tích có thể tới $10^{10}$), sắp xếp tăng dần rồi lấy phần tử thứ $k$ (chỉ số $k - 1$).

Source code để các bạn tham khảo nếu làm theo hướng này:

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

class Solution {
public:
    long long kthSmallestProduct(vector<int>& a, vector<int>& b, long long k) {
        vector <long long> p;
        for (int i = 0; i < (int) a.size(); i++) {
            for (int j = 0; j < (int) b.size(); j++) {
                p.push_back(1LL * a[i] * b[j]);
            }
        }
        sort(p.begin(), p.end());
        return p[k - 1];
    }
};

int main() {
    int n, m;
    long long k;
    cin >> n >> m >> k;
    vector<int> a(n), b(m);
    for (int i = 0; i < n; i++) cin >> a[i];
    for (int j = 0; j < m; j++) cin >> b[j];
    Solution sol;
    cout << sol.kthSmallestProduct(a, b, k);
    return 0;
}
import sys
from typing import List

class Solution:
    def kthSmallestProduct(self, a: List[int], b: List[int], k: int) -> int:
        p = [x * y for x in a for y in b]
        p.sort()
        return p[k - 1]

def main():
    data = sys.stdin.buffer.read().split()
    n, m, k = int(data[0]), int(data[1]), int(data[2])
    a = [int(x) for x in data[3:3 + n]]
    b = [int(x) for x in data[3 + n:3 + n + m]]
    print(Solution().kthSmallestProduct(a, b, k))

main()
Không gian$O(n \cdot m)$
Thời gian$O(nm \log(nm))$
Không qua được test lớn
Với $n = m = 5 \cdot 10^4$ thì có tới $2{,}5 \cdot 10^9$ tích: chỉ riêng việc lưu đã cần khoảng 20 GB bộ nhớ, chưa nói tới sắp xếp ⇒ chắc chắn MLE/TLE. Cách này chỉ dùng để hiểu đề và làm code vét cạn đối chiếu.

Cách 2Chặt nhị phân đáp án + đếm số cặp – O(n log m log V)

Ý tưởng: đoán đáp án rồi đếm

Phương pháp giải của mình là Binary Search (tìm kiếm nhị phân) theo kết quả. Đầu tiên ta sắp xếp dãy $a$ tăng dần, dãy $b$ tăng dần theo giá trị (đề cho sẵn rồi, nhưng code mình vẫn sort cho chắc). Ta chia dãy $a$ thành 2 dãy: nega là danh sách các số âm của dãy $a$, posa là danh sách các số $\ge 0$ của dãy $a$.

Với mỗi giá trị $v$, ta đếm số lượng cặp $(i, j)$ sao cho $a[i] \cdot b[j] \le v$, gọi là $cnt(v)$. Nếu $cnt(v) \ge K$, ta sẽ chấp nhận $v$ làm đáp án, tuy nhiên vẫn có thể tồn tại số $v$ nhỏ hơn cũng thoả. Khi $v$ tăng thì $cnt(v)$ chỉ có tăng (hoặc giữ nguyên), nên đáp án chính là số $v$ nhỏ nhất mà $cnt(v) \ge K$ ⇒ tìm kiếm nhị phân được.

Vì sao số v nhỏ nhất có cnt(v) ≥ K lại đúng là tích thứ K?
Gọi $x$ là tích nhỏ thứ $K$ trong danh sách đã sắp xếp. Mọi tích đứng ở vị trí $1..K$ đều $\le x$ ⇒ $cnt(x) \ge K$. Còn với $v \lt x$, các tích $\le v$ đều đứng trước vị trí $K$ ⇒ $cnt(v) \le K - 1$. Vậy $x$ đúng là số nhỏ nhất thoả $cnt(v) \ge K$ – và nó chắc chắn là một tích có thật.

Tích $a[i] \cdot b[j]$ nằm trong đoạn $[-10^{10}, 10^{10}]$ (vì $|a[i]|, |b[j]| \le 10^5$), nên ta đặt lo $\approx -10^{10}$, hi $\approx 10^{10}$, lấy mid = (lo + hi) / 2. Ta đếm số lượng cặp $(i, j)$ có tích $\le$ mid, so sánh với $K$: nếu $\ge K$ thì ghi nhận ans = mid và tìm tiếp bên trái (hi = mid - 1), ngược lại tìm bên phải (lo = mid + 1). Đoạn tìm kiếm dài cỡ $2 \cdot 10^{10}$ nên chỉ mất khoảng $\log_2(2 \cdot 10^{10}) \approx 35$ vòng lặp.

Đếm số cặp có tích ≤ v

Vấn đề cuối cùng là làm sao để đếm được số lượng cặp $(i, j)$? Xét bất đẳng thức $a[i] \cdot b[j] \le v$. Nếu ta cố định giá trị $a[i]$, ta chỉ cần đếm số lượng $j$ sao cho $a[i] \cdot b[j] \le v$. Có 3 trường hợp xảy ra:

  1. $a[i] = 0$: bất đẳng thức trở thành $0 \le v$. Do đó nếu $v \ge 0$, tất cả $m$ giá trị $j$ đều thoả. Nếu $v \lt 0$, không có giá trị $j$ nào thoả.
  2. $a[i] \lt 0$: chia hai vế cho số âm thì đổi chiều: $a[i] \cdot b[j] \le v \Leftrightarrow b[j] \ge \dfrac{v}{a[i]}$. Ta cần đếm số vị trí $j$ thoả $b[j] \ge v / a[i]$. Vì $b$ đã được sắp xếp, ta tìm vị trí $p$ nhỏ nhất sao cho $b[p] \ge v / a[i]$, số lượng vị trí $j$ thoả mãn là $m - p$ (đánh số từ 0). Đây là hàm findBigger.
  3. $a[i] \gt 0$: $a[i] \cdot b[j] \le v \Leftrightarrow b[j] \le \dfrac{v}{a[i]}$. Ta tìm vị trí $p$ lớn nhất sao cho $b[p] \le v / a[i]$, số lượng vị trí $j$ thoả mãn là $p + 1$. Đây là hàm findSmaller.
Nhìn theo bảng tích
Xếp các tích thành bảng: hàng là $a[i]$, cột là $b[j]$ (cả hai tăng dần). Với hàng $a[i] \gt 0$, các ô có tích $\le v$ luôn là một đoạn đầu của hàng; với hàng $a[i] \lt 0$ thì là một đoạn cuối; hàng $a[i] = 0$ thì hoặc cả hàng, hoặc không ô nào. Vì vậy mỗi hàng chỉ cần một lần chặt nhị phân trên $b$ là đếm xong. Em xem khung minh hoạ bên dưới sẽ thấy rõ.

Minh hoạ

Em nhập 2 mảng và $k$, rồi bấm Tiếp. Mỗi bước là một lần thử mid: bảng tô xanh các ô có tích $\le$ mid, cột bên phải là số ô xanh của từng hàng (đúng bằng kết quả của findSmaller / findBigger). Để minh hoạ gọn, mình cho lo, hi bắt đầu từ tích nhỏ nhất và lớn nhất (code thật dùng $\pm 10^{10}$, chỉ tốn thêm vài vòng):

Ví dụ nhanh:

Source code để các bạn tham khảo nếu làm theo hướng này (code C++ của mình đã AC trên LeetCode):

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

class Solution {
public:
    int findSmaller(const vector <int> &a, double x) {
        int lo = 0;
        int hi = (int) a.size() - 1;
        int pos = -1;
        while (lo <= hi) {
            int mid = (lo + hi) / 2;
            if (1.0 * a[mid] <= x) {
                pos = mid;
                lo = mid + 1;
            }
            else {
                hi = mid - 1;
            }
        }
        return pos + 1;
    }
    int findBigger(const vector <int> &a, double x) {
        int n = (int) a.size();
        int lo = 0;
        int hi = (int) n - 1;
        int pos = -1;
        while (lo <= hi) {
            int mid = (lo + hi) / 2;
            if (1.0 * a[mid] >= x) {
                pos = mid;
                hi = mid - 1;
            }
            else {
                lo = mid + 1;
            }
        }
        if (pos == -1) {
            return 0;
        }
        else {
            return n - pos;
        }
    }
    long long kthSmallestProduct(vector<int>& a, vector<int>& b, long long k) {
        sort(a.begin(), a.end());
        sort(b.begin(), b.end());
        vector <int> nega, posa;
        int n = (int) a.size();
        int m = (int) b.size();
        for (int i = 0; i < n; i++) {
            if (a[i] >= 0) {
                posa.push_back(a[i]);
            }
            else {
                nega.push_back(a[i]);
            }
        }
        long long ans = 0LL;
        long long lo = -10000000007LL;
        long long hi = 10000000007LL;
        while (lo <= hi) {
            long long mid = (lo + hi) >> 1;
            long long total = 0LL;
            for (int i = 0; i < (int) nega.size(); i++) {
                double x = 1.0 * mid / nega[i];
                total += findBigger(b, x);
            }
            for (int i = 0; i < (int) posa.size(); i++) {
                if (posa[i] == 0) {
                    if (mid >= 0) {
                        total += m;
                    }
                    continue;
                }
                double x = 1.0 * mid / posa[i];
                total += findSmaller(b, x);
            }
            if (total >= k) {
                ans = mid;
                hi = mid - 1;
            }
            else {
                lo = mid + 1;
            }
        }
        return ans;
    }
};

int main() {
    int n, m;
    long long k;
    cin >> n >> m >> k;
    vector<int> a(n), b(m);
    for (int i = 0; i < n; i++) cin >> a[i];
    for (int j = 0; j < m; j++) cin >> b[j];
    Solution sol;
    cout << sol.kthSmallestProduct(a, b, k);
    return 0;
}
import sys
from bisect import bisect_left, bisect_right
from typing import List

class Solution:
    def kthSmallestProduct(self, a: List[int], b: List[int], k: int) -> int:
        a.sort()
        b.sort()
        m = len(b)
        nega = [x for x in a if x < 0]
        posa = [x for x in a if x >= 0]
        ans = 0
        lo, hi = -10000000007, 10000000007
        while lo <= hi:
            mid = (lo + hi) >> 1
            total = 0
            for x in nega:                       # x < 0: cần b[j] >= mid / x
                c = -(-mid // x)                 # c = ceil(mid / x), chia nguyên chính xác
                total += m - bisect_left(b, c)
            for x in posa:
                if x == 0:                       # 0 * b[j] = 0 <= mid khi mid >= 0
                    if mid >= 0:
                        total += m
                    continue
                total += bisect_right(b, mid // x)   # x > 0: cần b[j] <= floor(mid / x)
            if total >= k:
                ans = mid
                hi = mid - 1
            else:
                lo = mid + 1
        return ans

def main():
    data = sys.stdin.buffer.read().split()
    n, m, k = int(data[0]), int(data[1]), int(data[2])
    a = [int(x) for x in data[3:3 + n]]
    b = [int(x) for x in data[3 + n:3 + n + m]]
    print(Solution().kthSmallestProduct(a, b, k))

main()
Về phép chia trong code
Code C++ của mình so sánh $b[j]$ với $x = mid / a[i]$ tính bằng số thực double. Cách này an toàn ở bài này: $|mid| \le 10^{10}$ biểu diễn chính xác được bằng double, và nếu $mid / a[i]$ không nguyên thì nó cách số nguyên gần nhất ít nhất $1 / |a[i]| \ge 10^{-5}$, lớn hơn rất nhiều so với sai số làm tròn (cỡ $10^{-6}$) ⇒ phép so sánh luôn đúng. Bản Python thì mình dùng chia nguyên cho chắc ăn: với $a[i] \gt 0$ đếm $b[j] \le \lfloor mid / a[i] \rfloor$ (bisect_right), với $a[i] \lt 0$ đếm $b[j] \ge \lceil mid / a[i] \rceil$ (bisect_left), trong đó $\lceil x / y \rceil$ viết là -(-x // y).
Biên của lo, hi
Lúc phân tích ban đầu mình hay nghĩ $v$ nằm trong $[-10^9, 10^9]$, nhưng thật ra tích có thể tới $10^5 \cdot 10^5 = 10^{10}$, vượt int. Vì vậy trong code lo, hi, mid, total, ans đều là long long và đoạn tìm kiếm là $[-10^{10} - 7,\ 10^{10} + 7]$.
C++▶ Chạy tay từng dòng codeChặt nhị phân đáp án mid, đếm số cặp có tích ≤ mid bằng findBigger / findSmaller.
Dữ liệu mẫu:
1#include <bits/stdc++.h>
2using namespace std;
3
4class Solution {
5public:
6 int findSmaller(const vector <int> &a, double x) {
7 int lo = 0;
8 int hi = (int) a.size() - 1;
9 int pos = -1;
10 while (lo <= hi) {
11 int mid = (lo + hi) / 2;
12 if (1.0 * a[mid] <= x) {
13 pos = mid;
14 lo = mid + 1;
15 }
16 else {
17 hi = mid - 1;
18 }
19 }
20 return pos + 1;
21 }
22 int findBigger(const vector <int> &a, double x) {
23 int n = (int) a.size();
24 int lo = 0;
25 int hi = (int) n - 1;
26 int pos = -1;
27 while (lo <= hi) {
28 int mid = (lo + hi) / 2;
29 if (1.0 * a[mid] >= x) {
30 pos = mid;
31 hi = mid - 1;
32 }
33 else {
34 lo = mid + 1;
35 }
36 }
37 if (pos == -1) {
38 return 0;
39 }
40 else {
41 return n - pos;
42 }
43 }
44 long long kthSmallestProduct(vector<int>& a, vector<int>& b, long long k) {
45 sort(a.begin(), a.end());
46 sort(b.begin(), b.end());
47 vector <int> nega, posa;
48 int n = (int) a.size();
49 int m = (int) b.size();
50 for (int i = 0; i < n; i++) {
51 if (a[i] >= 0) {
52 posa.push_back(a[i]);
53 }
54 else {
55 nega.push_back(a[i]);
56 }
57 }
58 long long ans = 0LL;
59 long long lo = -10000000007LL;
60 long long hi = 10000000007LL;
61 while (lo <= hi) {
62 long long mid = (lo + hi) >> 1;
63 long long total = 0LL;
64 for (int i = 0; i < (int) nega.size(); i++) {
65 double x = 1.0 * mid / nega[i];
66 total += findBigger(b, x);
67 }
68 for (int i = 0; i < (int) posa.size(); i++) {
69 if (posa[i] == 0) {
70 if (mid >= 0) {
71 total += m;
72 }
73 continue;
74 }
75 double x = 1.0 * mid / posa[i];
76 total += findSmaller(b, x);
77 }
78 if (total >= k) {
79 ans = mid;
80 hi = mid - 1;
81 }
82 else {
83 lo = mid + 1;
84 }
85 }
86 return ans;
87 }
88};
89
90int main() {
91 int n, m;
92 long long k;
93 cin >> n >> m >> k;
94 vector<int> a(n), b(m);
95 for (int i = 0; i < n; i++) cin >> a[i];
96 for (int j = 0; j < m; j++) cin >> b[j];
97 Solution sol;
98 cout << sol.kthSmallestProduct(a, b, k);
99 return 0;
100}

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

  • Không gian: chỉ thêm 2 mảng nega, posa ⇒ $O(n)$ (không tính 2 mảng đầu vào).
  • Thời gian: sắp xếp mất $O(n \log n + m \log m)$. Chặt nhị phân đáp án chạy khoảng $\log_2 V \approx 35$ vòng ($V \approx 2 \cdot 10^{10}$ là độ dài đoạn tìm kiếm), mỗi vòng duyệt $n$ phần tử của $a$, mỗi phần tử chặt nhị phân trên $b$ mất $O(\log m)$ ⇒ tổng cộng $O(n \cdot \log m \cdot \log V)$, cỡ $35 \cdot 5 \cdot 10^4 \cdot 16 \approx 2{,}8 \cdot 10^7$ phép tính.
Không gian$O(n)$
Thời gian$O(n \log m \log V)$
Mẹo nhỏ
Nếu muốn nhanh hơn nữa, có thể đổi vai trò để mảng duyệt ngoài là mảng ngắn hơn (đổi chỗ nums1, nums2 không làm thay đổi tập các tích), hoặc thay chặt nhị phân trong bằng hai con trỏ để đếm trong $O(n + m)$ mỗi vòng. Với giới hạn đề bài thì code ở trên đã đủ AC rồi nha.
Ghi chú của mình: code C++ và Python của cả 2 cách đã được kiểm tra với 600 bộ test ngẫu nhiên nhỏ (có cả số âm, số 0 và các giá trị biên $\pm 10^5$), so khớp với nhau; code C++ cách 2 còn được so với cách 1 trên 300 bộ test có $n, m \le 300$ và một test $n = m = 3000$ – khớp hết. Với $n = m = 5 \cdot 10^4$, code C++ cách 2 chạy khoảng 0.3 giây, bản Python khoảng 1.5 giây. So với code gốc của mình trên Facebook, mình chỉ bỏ 2 mảng negb, posb khai báo mà không dùng tới.
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