Chiếu hoa (Đề tự luyện HSG Tin quốc gia – Bài 2) – Chu kỳ xâu khi được sửa tối đa E ký tự: hàm Z, hash + chặt nhị phân nhảy qua các vị trí lệch, đếm theo đoạn cùng màu
Đề bài
Chiếu hoa
Tấm chiếu gồm $N$ sợi lác, sợi thứ $i$ có màu $s_i$ (một chữ cái thường). Tấm chiếu có chu kỳ $p$ ($1 \le p \le N$) nếu $s_i = s_{i+p}$ với mọi $1 \le i \le N - p$. Số $p$ là đạt nếu có thể nhuộm lại (đổi màu tùy ý) không quá $E$ sợi để tấm chiếu có chu kỳ $p$ (mỗi $p$ xét riêng).
Tìm số $p$ đạt nhỏ nhất và đếm số lượng số $p$ đạt.
Dữ liệu vào: dòng đầu $N, E$ ($1 \le N \le 2 \cdot 10^5$; $0 \le E \le 3$); dòng hai là xâu $s$ gồm $N$ chữ cái thường.
Kết quả: hai số – $p$ đạt nhỏ nhất và số lượng $p$ đạt.
- Subtask 1 (20%): $N \le 2000$.
- Subtask 2 (30%): $E = 0$.
- Subtask 3 (50%): không có ràng buộc gì thêm.
13 2
bcabcabcabbab3 7Các $p$ đạt: $3, 6, 9, 10, 11, 12, 13$. Với $p = 3, 6, 9$: nhuộm sợi $11$ từ b sang c (được bcabcabcabcab); $p = 10, 11$: nhuộm hai sợi; $p = 12, 13$: không cần nhuộm. Các $p$ khác cần ít nhất $3$ sợi (chẳng hạn $p = 1$ cần $7$ sợi).
8 0
abaababa5 3Không được nhuộm sợi nào; tấm chiếu có đúng ba chu kỳ $5, 7, 8$.
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 Chiếu hoaLời mở đầu
Trước hết hãy trả lời câu hỏi: với một $p$ cố định, phải nhuộm lại ít nhất bao nhiêu sợi? Điều kiện $s_i = s_{i+p}$ nối sợi $i$ với sợi $i + p$, sợi $i + p$ với sợi $i + 2p$, … Vì vậy các sợi chia thành $p$ dãy độc lập: dãy thứ $r$ gồm các sợi $r, r + p, r + 2p, \ldots$ Tấm chiếu có chu kỳ $p$ khi và chỉ khi mỗi dãy chỉ có một màu. Trong mỗi dãy, cách tốt nhất là giữ nguyên màu xuất hiện nhiều nhất và nhuộm lại các sợi còn lại: $$cost(p) = \sum_{\text{dãy}} \big(\text{độ dài dãy} - \text{số sợi của màu nhiều nhất trong dãy}\big).$$ Số $p$ đạt khi $cost(p) \le E$.
Ví dụ 1 với $p = 3$: ba dãy, chỉ dãy 2 có một sợi khác màu nên $cost(3) = 1 \le 2$.
Tính $cost(p)$ trực tiếp tốn $O(N)$ cho mỗi $p$, tức $O(N^2)$ cho mọi $p$ – quá chậm với $N = 2 \cdot 10^5$. Nút thắt của bài là: $E$ rất nhỏ ($\le 3$), và ta chỉ cần biết $cost(p) \le E$ hay không, chứ không cần giá trị chính xác khi nó lớn. Lộ trình:
Cách 1Tính cost(p) trực tiếp – O(N²) · 20%
Làm đúng công thức ở trên: với mỗi $p$, duyệt từng dãy $r = 0, 1, \ldots, p - 1$ (đánh số từ $0$ trong code), đếm số sợi của từng màu bằng mảng $26$ phần tử rồi lấy màu nhiều nhất. Mỗi $p$ duyệt đúng $N$ sợi, tổng $O(N^2) = 4 \cdot 10^6$ phép tính khi $N \le 2000$. Vì duyệt $p$ tăng dần, $p$ đạt đầu tiên chính là $p$ nhỏ nhất.
#include <bits/stdc++.h>
using namespace std;
int main() {
int n, E;
string s;
cin >> n >> E >> s;
int best = -1, cnt = 0;
for (int p = 1; p <= n; p++) {
int cost = 0; // so soi phai nhuom lai de co chu ky p
for (int r = 0; r < p && r < n; r++) { // day r, r + p, r + 2p, ...
int c[26] = {0}, len = 0, mx = 0;
for (int i = r; i < n; i += p) { c[s[i] - 'a']++; len++; }
for (int x = 0; x < 26; x++) mx = max(mx, c[x]);
cost += len - mx; // giu mau nhieu nhat, nhuom phan con lai
}
if (cost <= E) {
cnt++;
if (best < 0) best = p;
}
}
cout << best << " " << cnt << "\n";
return 0;
}import sys
def main():
data = sys.stdin.buffer.read().split()
n = int(data[0]); E = int(data[1]); s = data[2]
best = -1; cnt = 0
for p in range(1, n + 1):
cost = 0 # so soi phai nhuom lai de co chu ky p
for r in range(min(p, n)): # day r, r + p, r + 2p, ...
c = [0] * 26
ln = 0
for i in range(r, n, p):
c[s[i] - 97] += 1
ln += 1
cost += ln - max(c) # giu mau nhieu nhat, nhuom phan con lai
if cost <= E:
cnt += 1
if best < 0:
best = p
print(best, cnt)
main()Cách 2E = 0: chu kỳ đúng bằng hàm Z – O(N) · 50%
Khi không được nhuộm, $p$ đạt khi và chỉ khi $s_i = s_{i+p}$ với mọi $i$, tức là đoạn đầu $s[0..N-p)$ trùng đoạn cuối $s[p..N)$ (đánh số từ $0$). Gọi $z[p]$ là độ dài tiền tố chung dài nhất của $s$ và $s[p..]$ (hàm Z). Khi đó $$p \text{ là chu kỳ} \iff z[p] \ge N - p \quad (\text{và } p = N \text{ luôn là chu kỳ}).$$ Hàm Z tính mọi $z[i]$ trong $O(N)$: giữ "hộp" $[l, r)$ là đoạn $s[l..r)$ trùng với tiền tố $s[0..r-l)$ có $r$ lớn nhất. Khi xét $i < r$, đoạn $s[i..r)$ trùng $s[i-l..r-l)$ nên $z[i] \ge \min(r - i, z[i - l])$; sau đó chỉ cần so tiếp từng ký tự. Mỗi lần so thành công đều đẩy $r$ sang phải, nên tổng số lần so là $O(N)$.
Với ví dụ 2 ($s = $ abaababa, $N = 8$): $z = [8, 0, 1, 3, 0, 3, 0, 1]$.
| $p$ | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
|---|---|---|---|---|---|---|---|---|
| $z[p]$ | 0 | 1 | 3 | 0 | 3 | 0 | 1 | – |
| $N - p$ | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
| Chu kỳ? | không | không | không | không | có | không | có | có |
Đáp án 5 3. Các em bấm "Tiếp" để xem hàm Z tính từng $z[i]$ và chiếc hộp $[l, r)$ dịch chuyển thế nào:
Code dưới đây dùng hàm Z khi $E = 0$ và quay về Cách 1 khi $E > 0$.
#include <bits/stdc++.h>
using namespace std;
vector<int> zfunc(const string& t) { // z[i] = do dai tien to chung cua t va t[i..]
int m = t.size();
vector<int> z(m, 0);
z[0] = m;
for (int i = 1, l = 0, r = 0; i < m; i++) {
if (i < r) z[i] = min(r - i, z[i - l]);
while (i + z[i] < m && t[z[i]] == t[i + z[i]]) z[i]++;
if (i + z[i] > r) { l = i; r = i + z[i]; }
}
return z;
}
int main() {
int n, E;
string s;
cin >> n >> E >> s;
int best = -1, cnt = 0;
if (E == 0) { // chu ky dung: s[i] = s[i + p] <=> z[p] >= n - p
vector<int> z = zfunc(s);
for (int p = 1; p <= n; p++)
if (p == n || z[p] >= n - p) {
cnt++;
if (best < 0) best = p;
}
} else { // Cach 1
for (int p = 1; p <= n; p++) {
int cost = 0;
for (int r = 0; r < p && r < n; r++) {
int c[26] = {0}, len = 0, mx = 0;
for (int i = r; i < n; i += p) { c[s[i] - 'a']++; len++; }
for (int x = 0; x < 26; x++) mx = max(mx, c[x]);
cost += len - mx;
}
if (cost <= E) { cnt++; if (best < 0) best = p; }
}
}
cout << best << " " << cnt << "\n";
return 0;
}import sys
def zfunc(t): # z[i] = do dai tien to chung cua t va t[i..]
m = len(t)
z = [0] * m
z[0] = m
l = r = 0
for i in range(1, m):
k = 0
if i < r:
k = z[i - l]
if r - i < k:
k = r - i
while i + k < m and t[k] == t[i + k]:
k += 1
z[i] = k
if i + k > r:
l = i; r = i + k
return z
def main():
data = sys.stdin.buffer.read().split()
n = int(data[0]); E = int(data[1]); s = data[2]
best = -1; cnt = 0
if E == 0: # chu ky dung: s[i] = s[i + p] <=> z[p] >= n - p
z = zfunc(s)
for p in range(1, n + 1):
if p == n or z[p] >= n - p:
cnt += 1
if best < 0:
best = p
else: # Cach 1
for p in range(1, n + 1):
cost = 0
for r in range(min(p, n)):
c = [0] * 26
ln = 0
for i in range(r, n, p):
c[s[i] - 97] += 1
ln += 1
cost += ln - max(c)
if cost <= E:
cnt += 1
if best < 0:
best = p
print(best, cnt)
main()Cách 3Hàm Z + hash nhảy qua các chỗ lệch – O(N · E · log N) · 100%
Với $p$ cố định, gọi $i$ ($0 \le i < N - p$) là một chỗ lệch nếu $s_i \ne s_{i+p}$, và $H(p)$ là số chỗ lệch. Ba nhận xét then chốt:
Nhận xét 1 – chỉ cần tìm tối đa $2E + 1$ chỗ lệch. Nhuộm lại sợi $x$ chỉ ảnh hưởng tới hai cặp $(x - p, x)$ và $(x, x + p)$. Muốn hết mọi chỗ lệch thì mỗi chỗ lệch phải bị "chạm" bởi ít nhất một sợi được nhuộm, nên $$cost(p) \ge \left\lceil H(p) / 2 \right\rceil, \qquad cost(p) \le E \implies H(p) \le 2E \le 6.$$ Vậy với mỗi $p$, ngay khi đã thấy $2E + 1$ chỗ lệch là loại được $p$.
Nhận xét 2 – biết các chỗ lệch là tính được $cost(p)$. Chỗ lệch $i$ nằm trong dãy $r = i \bmod p$, giữa phần tử thứ $t = \lfloor i / p \rfloor$ và $t + 1$ của dãy. Các chỗ lệch cắt mỗi dãy thành những đoạn cùng màu; dãy không có chỗ lệch thì đã một màu (đóng góp $0$). Với mỗi dãy có chỗ lệch, cộng độ dài các đoạn theo từng màu rồi giữ màu có tổng lớn nhất. Chú ý: hai đoạn cùng màu không liền nhau vẫn được giữ chung!
Độ dài dãy $r$ là $\lfloor (N - 1 - r) / p \rfloor + 1$ nên độ dài đoạn cuối tính được mà không cần duyệt dãy.
aaaabbbb chỉ có một chỗ lệch nhưng phải nhuộm $4$ sợi. (2) Không được chỉ giữ đoạn cùng màu dài nhất – như hình trên. Cả hai cách hiểu sai này đều vẫn cho đúng ví dụ 1 của đề.Nhận xét 3 – tìm chỗ lệch tiếp theo thật nhanh. Từ vị trí $i$, chỗ lệch tiếp theo là $i + \text{lcp}(i, i + p)$ với $\text{lcp}(a, b)$ là độ dài đoạn chung dài nhất bắt đầu tại $a$ và $b$. Ta tìm các chỗ lệch bằng cách "nhảy": tìm một chỗ lệch, bước qua nó, rồi nhảy tiếp – tối đa $2E + 1$ lần nhảy cho mỗi $p$.
- Chỗ lệch đầu tiên chính là $z[p]$ (hàm Z của $s$). Nếu $z[p] \ge N - p$ thì $p$ là chu kỳ đúng.
- Chỗ lệch cuối cùng cũng có sẵn: gọi $zr$ là hàm Z của xâu đảo ngược, $zr[p]$ là độ dài hậu tố chung của $s[0..N-p)$ và $s[p..N)$, nên chỗ lệch cuối là $N - p - 1 - zr[p]$.
- Các chỗ lệch ở giữa: tính $\text{lcp}(i, i + p)$ bằng hash đa thức + chặt nhị phân theo độ dài: hai đoạn dài $L$ bằng nhau thì hash bằng nhau, nên "đoạn chung dài $L$" là tính chất đơn điệu theo $L$. Mỗi lần $O(\log N)$.
Mỗi $p$ tốn $O(E \log N)$, tổng $O(N \cdot E \cdot \log N) \approx 2 \cdot 10^5 \cdot 5 \cdot 18 \approx 2 \cdot 10^7$ phép so hash. Code duyệt $p$ từ $N - 1$ giảm dần nên $p$ đạt cuối cùng ghi được là $p$ nhỏ nhất.
Mô phỏng dưới đây xét lần lượt từng $p$ cho ví dụ 1: các chỗ lệch được tô đỏ, các "bước nhảy" lcp được liệt kê, rồi tính $cost(p)$ theo đoạn cùng màu.
Chọn hash thế nào cho an toàn? Lời giải dùng modulo nguyên tố $2^{61} - 1$ với cơ số ngẫu nhiên chọn lúc chạy, nhân hai số $61$ bit bằng __uint128_t. Đừng dùng kiểu "để tràn số tự nhiên" (modulo $2^{64}$): với xâu Thue–Morse, khối $2^{11}$ ký tự và khối "đảo màu" của nó có cùng hash modulo $2^{64}$ với mọi cơ số lẻ – bộ test có hai test được dựng đúng như vậy. Bản Python dùng hai modulo cỡ $2^{31}$ để mọi phép nhân vẫn nằm trong số nguyên $64$ bit (PyPy chạy nhanh).
#include <bits/stdc++.h>
using namespace std;
typedef unsigned long long ull;
const ull MOD = (1ULL << 61) - 1;
int n, E;
string s;
vector<ull> h, pw;
ull mulmod(ull a, ull b) {
__uint128_t c = (__uint128_t)a * b;
ull r = (ull)(c & MOD) + (ull)(c >> 61);
return r >= MOD ? r - MOD : r;
}
ull getHash(int i, int len) { // hash cua s[i .. i + len - 1]
ull x = h[i + len] + MOD - mulmod(h[i], pw[len]);
return x >= MOD ? x - MOD : x;
}
int lcp(int i, int j, int lim) { // so ky tu dau tien bang nhau cua s[i..] va s[j..] (toi da lim)
int lo = 0, hi = lim;
while (lo < hi) {
int mid = (lo + hi + 1) / 2;
if (getHash(i, mid) == getHash(j, mid)) lo = mid;
else hi = mid - 1;
}
return lo;
}
vector<int> zfunc(const string& t) { // z[i] = do dai tien to chung cua t va t[i..]
int m = t.size();
vector<int> z(m, 0);
z[0] = m;
for (int i = 1, l = 0, r = 0; i < m; i++) {
if (i < r) z[i] = min(r - i, z[i - l]);
while (i + z[i] < m && t[z[i]] == t[i + z[i]]) z[i]++;
if (i + z[i] > r) { l = i; r = i + z[i]; }
}
return z;
}
int main() {
cin >> n >> E >> s;
mt19937_64 rng(chrono::steady_clock::now().time_since_epoch().count());
ull base = rng() % (MOD - 1000) + 500;
h.assign(n + 1, 0); pw.assign(n + 1, 1);
for (int i = 0; i < n; i++) {
h[i + 1] = mulmod(h[i], base) + (ull)s[i];
if (h[i + 1] >= MOD) h[i + 1] -= MOD;
pw[i + 1] = mulmod(pw[i], base);
}
vector<int> z = zfunc(s);
vector<int> zr = zfunc(string(s.rbegin(), s.rend()));
int best = n, cnt = 1; // p = n luon dat
vector<int> mis;
for (int p = n - 1; p >= 1; p--) {
int m = n - p; // so cap (i, i + p) can so sanh
if (z[p] >= m) { cnt++; best = p; continue; } // khop hoan toan: chu ky dung
int first = z[p], last = m - 1 - zr[p]; // vi tri lech dau tien va cuoi cung
mis.clear();
mis.push_back(first);
int i = first + 1;
while (i < last && (int)mis.size() < 2 * E) { // nhay qua cac doan khop bang lcp
i += lcp(i, i + p, last - i);
if (i >= last) break;
mis.push_back(i);
i++;
}
if (i < last) continue; // da co >= 2E + 1 vi tri lech
if (last != first) mis.push_back(last);
if ((int)mis.size() > 2 * E) continue;
// tinh so soi phai nhuom lai: moi day r, r+p, r+2p,... bi cat thanh cac doan cung mau
sort(mis.begin(), mis.end(), [&](int a, int b) {
return make_pair(a % p, a) < make_pair(b % p, b);
});
int cost = 0;
for (int a = 0; a < (int)mis.size(); ) {
int r = mis[a] % p, b = a;
while (b < (int)mis.size() && mis[b] % p == r) b++;
int len = (n - 1 - r) / p + 1; // so soi trong day r
int cntc[26] = {0}, st = 0, mx = 0;
for (int q = a; q <= b; q++) {
int en = (q < b) ? mis[q] / p : len - 1; // doan cung mau [st, en]
int c = s[r + (long long)st * p] - 'a';
cntc[c] += en - st + 1;
mx = max(mx, cntc[c]);
st = en + 1;
}
cost += len - mx;
a = b;
}
if (cost <= E) { cnt++; best = p; }
}
cout << best << " " << cnt << "\n";
return 0;
}import sys, random
def zfunc(t): # z[i] = do dai tien to chung cua t va t[i..]
m = len(t)
z = [0] * m
z[0] = m
l = r = 0
for i in range(1, m):
k = 0
if i < r:
k = z[i - l]
if r - i < k:
k = r - i
while i + k < m and t[k] == t[i + k]:
k += 1
z[i] = k
if i + k > r:
l = i; r = i + k
return z
def main():
data = sys.stdin.buffer.read().split()
n = int(data[0]); E = int(data[1]); s = data[2]
M1 = 2147483647; M2 = 2147483629 # hai modulo nguyen to (tich < 2^62)
b1 = random.randrange(1000, M1 - 1000); b2 = random.randrange(1000, M2 - 1000)
h1 = [0] * (n + 1); h2 = [0] * (n + 1); p1 = [1] * (n + 1); p2 = [1] * (n + 1)
for i in range(n):
c = s[i]
h1[i + 1] = (h1[i] * b1 + c) % M1; h2[i + 1] = (h2[i] * b2 + c) % M2
p1[i + 1] = p1[i] * b1 % M1; p2[i + 1] = p2[i] * b2 % M2
z = zfunc(s)
zr = zfunc(s[::-1])
best = n; cnt = 1 # p = n luon dat
lim2 = 2 * E
for p in range(n - 1, 0, -1):
m = n - p # so cap (i, i + p) can so sanh
if z[p] >= m: # khop hoan toan: chu ky dung
cnt += 1; best = p
continue
first = z[p]; last = m - 1 - zr[p] # vi tri lech dau tien va cuoi cung
mis = [first]
i = first + 1
while i < last and len(mis) < lim2: # nhay qua cac doan khop bang lcp (hash + chat nhi phan)
lo = 0; hi = last - i; j = i + p
while lo < hi:
mid = (lo + hi + 1) >> 1
if (h1[i + mid] - h1[i] * p1[mid] - h1[j + mid] + h1[j] * p1[mid]) % M1 == 0 and \
(h2[i + mid] - h2[i] * p2[mid] - h2[j + mid] + h2[j] * p2[mid]) % M2 == 0:
lo = mid
else:
hi = mid - 1
i += lo
if i >= last:
break
mis.append(i)
i += 1
if i < last: # da co >= 2E + 1 vi tri lech
continue
if last != first:
mis.append(last)
if len(mis) > lim2:
continue
# tinh so soi phai nhuom lai tren tung day r, r + p, r + 2p, ...
mis.sort(key=lambda x: (x % p, x))
cost = 0
a = 0; k = len(mis)
while a < k:
r = mis[a] % p; b = a
while b < k and mis[b] % p == r:
b += 1
ln = (n - 1 - r) // p + 1 # so soi trong day r
cntc = {}; st = 0; mx = 0
for q in range(a, b + 1):
en = mis[q] // p if q < b else ln - 1 # doan cung mau [st, en]
c = s[r + st * p]
v = cntc.get(c, 0) + en - st + 1
cntc[c] = v
if v > mx:
mx = v
st = en + 1
cost += ln - mx
a = b
if cost <= E:
cnt += 1; best = p
print(best, cnt)
main()Chạy tay từng dòng lời giải chuẩn (dữ liệu nhỏ, $N \le 14$; hash dùng cơ số cố định $131$ cho dễ theo dõi – kết quả không phụ thuộc cơ số):
Bộ test chấm "bắt" những lỗi nào?
| Cách làm | Sub 1 (10 test) | Sub 2 (8 test) | Sub 3 (12 test) | Điểm |
|---|---|---|---|---|
| Cách 1 – tính cost(p) trực tiếp O(N²) | Đúng | TLE | TLE | 20 |
| Cách 2 – E = 0: hàm Z | Đúng | Đúng | TLE | 50 |
| Cách 3 – Z + hash nhảy qua chỗ lệch | Đúng | Đúng | Đúng | 100 |
| Sai: chỉ kiểm tra H(p) ≤ 2E | 4/10 | 8/8 | 0/12 | 38,0 |
| Sai: mỗi dãy chỉ giữ đoạn cùng màu dài nhất | 9/10 | 8/8 | 7/12 | 77,2 |
| Sai: hash tràn số mod 2^64 | 10/10 | 8/8 | 10/12 | 91,7 |
| Gần đúng: tìm chỗ lệch bằng cách so từng ký tự | 10/10 | 4/8 (TLE) | 4/12 (TLE) | 51,7 |
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ó 30 test (10 + 8 + 12), mỗi cách đạt đúng số điểm công bố, còn các lời giải sai bị bắt như bảng trên. Lời giải chuẩn sol.cpp chạy test lâu nhất khoảng 0,1 giây; bản Python sol.py cho cùng đáp án trên mọi test (trên CPython mất tới khoảng 7 giây ở test nặng nhất, nên các em nhớ chọn PyPy 3 khi nộp – giới hạn cho Python là 5 giây). Cách 1 và Cách 2 (khi $E > 0$) phải làm $N^2 = 4 \cdot 10^{10}$ phép tính nên chạy quá 4 giây mỗi test lớn. "TLE" nghĩa là chạy quá 3 lần giới hạn thời gian. Lời giải "gần đúng" so từng ký tự vẫn nhanh với xâu ngẫu nhiên (chỗ lệch xuất hiện rất sớm), nhưng bị TLE với những tấm chiếu gần như một màu – khi đó với mọi $p$ các chỗ lệch nằm rải rác khắp chiều dài. Còn hash tràn số chỉ "chết" ở hai test Thue–Morse, đủ để mất gần 10 điểm oan đó!
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 😇
- 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.
- 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
Đăng nhận xét