Khăn rằn (Đề tự luyện HSG Tin quốc gia – Bài 3)
Đề bài
Khăn rằn. Dải vải của thầy Sơn là xâu $T = BB\ldots B$ ($K$ lần đoạn hoa văn $B$). Có $M$ mẫu $w_1, \ldots, w_M$ đôi một khác nhau. Đếm số cách cắt toàn bộ $T$ thành các mảnh liên tiếp sao cho mỗi mảnh trùng với một mẫu (mẫu dùng lại thoải mái). Hai cách khác nhau nếu tập vị trí cắt khác nhau. In kết quả modulo $10^9 + 7$.
Dữ liệu vào: dòng 1 gồm $K, M$ $(1 \le K \le 10^{18},\ 1 \le M \le 10^4)$; dòng 2 là xâu $B$ $(1 \le |B| \le 2000)$; $M$ dòng sau, mỗi dòng một mẫu $w_i$ $(1 \le |w_i| \le 50)$. Mọi xâu chỉ gồm chữ cái thường.
Kết quả: số cách cắt modulo $10^9 + 7$.
- Subtask 1 (20%): $K \cdot |B| \le 10^5$.
- Subtask 2 (30%): $|B| = 1$.
- Subtask 3 (50%): 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.
3 4
aba
a
ba
aab
aba8$T$ = abaabaaba. 8 cách: aba|aba|aba, a|ba|aba|aba, aba|a|ba|aba, aba|aba|a|ba, a|ba|a|ba|aba, a|ba|aba|a|ba, aba|a|ba|a|ba, a|ba|a|ba|a|ba. Mẫu aab có xuất hiện (ô 3..5, 6..8) nhưng không dùng được.
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 Khăn rằnLời mở đầu
Nếu dải vải ngắn thì đây là bài "cắt xâu thành các từ trong từ điển" quen thuộc. Cái khó nằm ở chỗ dải vải dài tới $K \cdot |B| \approx 2 \cdot 10^{21}$ ô: không thể đi qua từng ô. Bù lại, dải vải lặp lại và mẫu thì ngắn ($\le 50$). Hai chi tiết nhỏ này là toàn bộ chìa khoá của bài.
Nhận xét 1: QHĐ theo mảnh cuối cùng
Gọi $dp[i]$ là số cách cắt $i$ ô đầu tiên của dải vải ($dp[0] = 1$). Xét mảnh cuối của một cách cắt $i$ ô đầu: nó là $T[i-\ell+1..i]$ với một độ dài $\ell$ nào đó, và phải trùng một mẫu. Phần còn lại là một cách cắt $i - \ell$ ô đầu. Vậy $$dp[i] = \sum_{\ell \,:\, T[i-\ell+1..i] \text{ là một mẫu}} dp[i - \ell].$$ Vì các mẫu đôi một khác nhau nên mỗi cách cắt được đếm đúng một lần.
Hình 1. Với ví dụ đề, tại $i = 6$ có ba mẫu kết thúc đúng ở ô 6 nên $dp[6] = dp[5] + dp[4] + dp[3]$. Đường xanh là ranh giới các bản $B$.
Nhận xét 2: chỉ cần nhớ W giá trị, và "luật cộng" lặp theo chu kỳ
Gọi $W$ là độ dài mẫu dài nhất ($W \le 50$). Công thức trên chỉ dùng $dp[i-1], \ldots, dp[i-W]$. Còn những $\ell$ nào được cộng thì chỉ phụ thuộc vào vài ký tự ngay trước ô $i$ — mà dải vải lặp lại với chu kỳ $|B|$, nên "luật cộng" tại ô $i$ và tại ô $i + |B|$ là giống hệt nhau (ít nhất là khi đã đi xa khỏi đầu dải). Đây chính là tín hiệu của nhân ma trận: một phép biến đổi tuyến tính lặp lại $K$ lần.
Cách 1QHĐ trực tiếp trên dải vải – O(K·|B|·W) · 20%
Dựng hẳn xâu $T$ (được khi $K \cdot |B| \le 10^5$) rồi tính $dp$ theo Nhận xét 1. Để tìm nhanh các $\ell$ mà $T[i-\ell+1..i]$ là mẫu, ta đưa mọi mẫu viết ngược vào một cây trie. Từ ô $i$ đi lùi $T[i], T[i-1], \ldots$ đồng thời đi xuống trie: mỗi khi tới một nút đánh dấu "kết thúc mẫu" ở độ sâu $\ell$ thì cộng $dp[i-\ell]$; rơi khỏi trie thì dừng. Mỗi ô đi lùi không quá $W$ bước.
Em thử mô phỏng dưới đây: hàng trên là các ô của dải vải, hàng dưới là $dp$. Ô màu xanh là các $dp[i-\ell]$ được cộng vào $dp[i]$.
#include <bits/stdc++.h>
using namespace std;
const long long MOD = 1000000007;
int main() {
long long K; int M; string B;
cin >> K >> M >> B;
vector<string> w(M);
for (auto& s : w) cin >> s;
string T;
for (long long i = 0; i < K; i++) T += B; // dai vai day du, K*|B| <= 1e5
int n = T.size();
// trie cac mau viet nguoc, de duyet lui tu vi tri cat
vector<array<int, 26>> nxt(1); nxt[0].fill(0);
vector<char> fin(1, 0);
for (auto& s : w) {
int u = 0;
for (int i = (int)s.size() - 1; i >= 0; i--) {
int c = s[i] - 'a';
if (!nxt[u][c]) { nxt[u][c] = nxt.size(); nxt.push_back({}); nxt.back().fill(0); fin.push_back(0); }
u = nxt[u][c];
}
fin[u] = 1;
}
vector<long long> dp(n + 1, 0);
dp[0] = 1; // dp[i] = so cach cat i o dau tien
for (int i = 1; i <= n; i++) {
int u = 0;
for (int l = 1; l <= i; l++) { // manh cuoi la T[i-l..i-1]
u = nxt[u][T[i - l] - 'a'];
if (!u) break;
if (fin[u]) dp[i] = (dp[i] + dp[i - l]) % MOD;
}
}
cout << dp[n] << "\n";
return 0;
}import sys
MOD = 10**9 + 7
def main():
data = sys.stdin.buffer.read().split()
K = int(data[0]); M = int(data[1])
B = data[2]
words = data[3:3 + M]
T = B * K # dai vai day du, K*|B| <= 1e5
n = len(T)
# trie cac mau viet nguoc
ch = [{}]; fin = [False]
for w in words:
u = 0
for c in reversed(w):
nx = ch[u].get(c)
if nx is None:
nx = len(ch); ch[u][c] = nx; ch.append({}); fin.append(False)
u = nx
fin[u] = True
dp = [0] * (n + 1)
dp[0] = 1 # dp[i] = so cach cat i o dau tien
for i in range(1, n + 1):
u = 0; s = 0
j = i - 1
while j >= 0: # manh cuoi la T[j..i-1]
u = ch[u].get(T[j])
if u is None:
break
if fin[u]:
s += dp[j]
j -= 1
dp[i] = s % MOD
print(dp[n])
main()Cách 2Khi |B| = 1: ma trận đồng hành – O(W³ log K) · 50%
Khi $B$ chỉ có một chữ, chẳng hạn c, dải vải là ccc...c. Mảnh nào cũng toàn chữ c, nên chỉ những mẫu dạng $c^\ell$ là dùng được. Gọi $L$ là tập các độ dài đó và $D = \max L$. Khi đó với mọi $i$:
$$dp[i] = \sum_{\ell \in L} dp[i - \ell] \qquad (dp[\text{âm}] = 0).$$
Đây là truy hồi tuyến tính hệ số hằng (giống Fibonacci khi $L = \{1, 2\}$). Đặt vectơ trạng thái $V_i = (dp[i], dp[i-1], \ldots, dp[i-D+1])$ thì $V_{i+1} = A \cdot V_i$ với ma trận đồng hành $A$: hàng đầu có số 1 ở cột $\ell - 1$ với mỗi $\ell \in L$, các hàng sau dời xuống một ô (hàng $j$ có số 1 ở cột $j - 1$). Do đó $V_K = A^K V_0$ với $V_0 = (1, 0, \ldots, 0)$, tính bằng lũy thừa nhanh trong $O(D^3 \log K)$.
Ví dụ $L = \{1, 2\}$: $A = \begin{pmatrix} 1 & 1 \\ 1 & 0 \end{pmatrix}$, và $dp[K]$ chính là số Fibonacci thứ $K + 1$.
Các trường hợp $|B| > 1$ trong code vẫn làm như Cách 1, nên Cách 2 ăn được subtask 1 + 2.
#include <bits/stdc++.h>
using namespace std;
typedef unsigned long long ull;
const ull MOD = 1000000007ULL;
int D;
typedef vector<vector<ull>> Mat;
Mat mul(const Mat& X, const Mat& Y) {
Mat Z(D, vector<ull>(D, 0));
for (int i = 0; i < D; i++)
for (int k = 0; k < D; k++)
for (int j = 0; j < D; j++) Z[i][j] = (Z[i][j] + X[i][k] * Y[k][j]) % MOD;
return Z;
}
int main() {
ull K; int M; string B;
cin >> K >> M >> B;
vector<string> w(M);
for (auto& s : w) cin >> s;
if (B.size() == 1) {
// T = cccc...c (K ky tu): manh nao cung phai la mau gom toan chu c
vector<int> ok(51, 0);
D = 0;
for (auto& s : w)
if (s == string(s.size(), B[0])) { ok[s.size()] = 1; D = max(D, (int)s.size()); }
if (D == 0) { cout << 0 << "\n"; return 0; }
// dp[i] = tong dp[i - l] voi l thuoc tap do dai hop le -> ma tran dong hanh D x D
Mat A(D, vector<ull>(D, 0)), R(D, vector<ull>(D, 0));
for (int l = 1; l <= D; l++) if (ok[l]) A[0][l - 1] = 1;
for (int i = 1; i < D; i++) A[i][i - 1] = 1;
for (int i = 0; i < D; i++) R[i][i] = 1;
while (K) { if (K & 1) R = mul(R, A); A = mul(A, A); K >>= 1; }
cout << R[0][0] << "\n"; // trang thai dau (dp[0], dp[-1], ...) = (1, 0, ...)
return 0;
}
// Truong hop con lai: lam nhu Cach 1 (chi chay kip khi K*|B| nho)
string T;
for (ull i = 0; i < K; i++) T += B;
int n = T.size();
vector<array<int, 26>> nxt(1); nxt[0].fill(0);
vector<char> fin(1, 0);
for (auto& s : w) {
int u = 0;
for (int i = (int)s.size() - 1; i >= 0; i--) {
int c = s[i] - 'a';
if (!nxt[u][c]) { nxt[u][c] = nxt.size(); nxt.push_back({}); nxt.back().fill(0); fin.push_back(0); }
u = nxt[u][c];
}
fin[u] = 1;
}
vector<ull> dp(n + 1, 0); dp[0] = 1;
for (int i = 1; i <= n; i++) {
int u = 0;
for (int l = 1; l <= i; l++) {
u = nxt[u][T[i - l] - 'a'];
if (!u) break;
if (fin[u]) dp[i] = (dp[i] + dp[i - l]) % MOD;
}
}
cout << dp[n] << "\n";
return 0;
}import sys
MOD = 10**9 + 7
def mat_mul(X, Y):
YT = list(zip(*Y))
return [[sum(a * b for a, b in zip(row, col)) % MOD for col in YT] for row in X]
def main():
data = sys.stdin.buffer.read().split()
K = int(data[0]); M = int(data[1])
B = data[2]
words = data[3:3 + M]
if len(B) == 1:
# T = cccc...c: manh nao cung phai la mau gom toan chu c
ok = [False] * 51
D = 0
for w in words:
if w == B * len(w):
ok[len(w)] = True
D = max(D, len(w))
if D == 0:
print(0); return
# dp[i] = tong dp[i - l] voi l hop le -> ma tran dong hanh D x D
A = [[0] * D for _ in range(D)]
for l in range(1, D + 1):
if ok[l]:
A[0][l - 1] = 1
for i in range(1, D):
A[i][i - 1] = 1
R = [[int(i == j) for j in range(D)] for i in range(D)]
while K:
if K & 1:
R = mat_mul(R, A)
A = mat_mul(A, A)
K >>= 1
print(R[0][0]); return
# truong hop con lai: nhu Cach 1
T = B * K
n = len(T)
ch = [{}]; fin = [False]
for w in words:
u = 0
for c in reversed(w):
nx = ch[u].get(c)
if nx is None:
nx = len(ch); ch[u][c] = nx; ch.append({}); fin.append(False)
u = nx
fin[u] = True
dp = [0] * (n + 1); dp[0] = 1
for i in range(1, n + 1):
u = 0; s = 0; j = i - 1
while j >= 0:
u = ch[u].get(T[j])
if u is None:
break
if fin[u]:
s += dp[j]
j -= 1
dp[i] = s % MOD
print(dp[n])
main()Cách 3Ma trận chuyển qua một chu kỳ – O(|B|·W² + W³ log K) · 100%
Khi $|B| > 1$, "luật cộng" ở mỗi ô khác nhau, nên không còn một ma trận cố định cho từng bước. Nhưng nếu ta bước theo từng bản $B$ (mỗi lần $|B|$ ô) thì mọi bước lại giống hệt nhau. Có hai chi tiết cần làm cho thật sạch:
Chi tiết 1: kéo dài dải vải về bên trái, đặt dp[âm] = 0
Ở đầu dải vải, một số mẫu "không vừa" (dài hơn số ô đã có). Thay vì xử lý riêng, ta tưởng tượng dải vải kéo dài tuần hoàn vô hạn về bên trái và đặt $dp[j] = 0$ với mọi $j < 0$. Một mảnh thò ra trước ô 1 sẽ được cộng $dp[\text{âm}] = 0$, tức là tự biến mất. Nhờ vậy công thức $dp[i] = \sum dp[i-\ell]$ đúng cho mọi $i \ge 1$, với tập $\ell$ chỉ phụ thuộc $i \bmod |B|$: ký tự ở ô $j$ (đánh số từ 0) là $B[j \bmod |B|]$, kể cả khi $j$ âm.
Hình 2. Ví dụ đề ($B$ = aba, $W = 3$). Trạng thái tại ranh giới $p$ là $(dp[p], dp[p-1], dp[p-2])$; đi qua một bản $B$ là nhân với cùng một ma trận $A$.
Chi tiết 2: dựng ma trận A bằng "QHĐ hình thức"
Đặt trạng thái tại ranh giới $p$ (bội của $|B|$) là $S_p = (dp[p], dp[p-1], \ldots, dp[p-W+1])$. Ta coi $W$ giá trị này là ẩn số và biểu diễn mỗi $dp[p+i]$ ($i = 1..|B|$) dưới dạng một vectơ hệ số $coef[i]$ có $W$ thành phần:
- $coef[-j] = e_j$ (vectơ đơn vị) với $j = 0..W-1$ — vì $dp[p-j]$ chính là thành phần thứ $j$ của $S_p$.
- $coef[i] = \sum coef[i - \ell]$ theo đúng các $\ell$ mà mảnh dài $\ell$ kết thúc tại ô $p+i$ là một mẫu (tìm bằng trie ngược như Cách 1, chỉ là ký tự lấy theo $\bmod |B|$, nên mảnh có thể vắt qua nhiều bản $B$).
Khi đó $S_{p+|B|} = A \cdot S_p$ với hàng $j$ của $A$ là $coef[|B| - j]$. Vì sao $A$ không phụ thuộc $p$? Vì các phép cộng ở trên chỉ phụ thuộc vào ký tự, mà ký tự thì lặp theo chu kỳ $|B|$. Cuối cùng $S_{K|B|} = A^K \cdot S_0$ với $S_0 = (1, 0, \ldots, 0)$, đáp án là thành phần đầu tiên.
Chi phí dựng $A$: $|B|$ ô, mỗi ô đi lùi tối đa $W$ bước trên trie và mỗi lần khớp cộng một vectơ $W$ thành phần: $O(|B| \cdot W^2) = 2000 \cdot 2500 = 5 \cdot 10^6$. Lũy thừa: $O(W^3 \log K) \approx 125000 \cdot 60 = 7{,}5 \cdot 10^6$. Rất nhẹ.
Mô phỏng dưới đây dựng từng hàng $coef$ qua một chu kỳ, lập ma trận $A$, rồi nhân $K$ lần để ra $dp$ tại các ranh giới (số đã lấy modulo $10^9+7$), cuối cùng đối chiếu với QHĐ trực tiếp.
Code đầy đủ (trie ngược + QHĐ hình thức + lũy thừa nhanh áp lên vectơ):
#include <bits/stdc++.h>
using namespace std;
typedef unsigned long long ull;
const ull MOD = 1000000007ULL;
int W; // do dai mau dai nhat = so chieu trang thai
typedef vector<vector<ull>> Mat;
Mat mul(const Mat& X, const Mat& Y) { // nhan hai ma tran W x W
Mat Z(W, vector<ull>(W, 0));
for (int i = 0; i < W; i++)
for (int k = 0; k < W; k++) {
ull x = X[i][k];
if (!x) continue;
for (int j = 0; j < W; j++) Z[i][j] = (Z[i][j] + x * Y[k][j]) % MOD;
}
return Z;
}
vector<ull> mulv(const Mat& X, const vector<ull>& v) { // tinh X * v
vector<ull> r(W, 0);
for (int i = 0; i < W; i++)
for (int k = 0; k < W; k++) r[i] = (r[i] + X[i][k] * v[k]) % MOD;
return r;
}
int main() {
ull K; int M;
cin >> K >> M;
string B; cin >> B;
int m = B.size();
// trie cua cac mau viet nguoc
vector<array<int, 26>> nxt(1); nxt[0].fill(0);
vector<char> fin(1, 0);
W = 0;
for (int t = 0; t < M; t++) {
string w; cin >> w;
W = max(W, (int)w.size());
int u = 0;
for (int i = (int)w.size() - 1; i >= 0; i--) {
int c = w[i] - 'a';
if (!nxt[u][c]) { nxt[u][c] = nxt.size(); nxt.push_back({}); nxt.back().fill(0); fin.push_back(0); }
u = nxt[u][c];
}
fin[u] = 1;
}
// coef[i + W - 1] = dp[p + i] bieu dien theo trang thai (dp[p], dp[p-1], ..., dp[p-W+1])
vector<vector<ull>> coef(m + W, vector<ull>(W, 0));
for (int j = 0; j < W; j++) coef[W - 1 - j][j] = 1;
for (int i = 1; i <= m; i++) {
vector<ull>& cur = coef[i + W - 1];
int u = 0;
for (int l = 1; l <= W; l++) { // manh cuoi dai l: ky tu T[i-l..i-1]
int pos = ((i - l) % m + m) % m;
u = nxt[u][B[pos] - 'a'];
if (!u) break;
if (fin[u]) {
const vector<ull>& src = coef[i - l + W - 1];
for (int k = 0; k < W; k++) cur[k] = (cur[k] + src[k]) % MOD;
}
}
}
Mat A(W);
for (int j = 0; j < W; j++) A[j] = coef[m - j + W - 1]; // trang thai moi: dp[p+m-j]
vector<ull> v(W, 0); v[0] = 1; // dp[0] = 1, dp[am] = 0
while (K) {
if (K & 1) v = mulv(A, v);
K >>= 1;
if (K) A = mul(A, A);
}
cout << v[0] << "\n";
return 0;
}import sys
from operator import mul
MOD = 10**9 + 7
def mat_mul(X, Y, W):
# tinh X * Y (W x W), dung cot cua Y de tinh nhanh
YT = list(zip(*Y))
return [[sum(map(mul, row, col)) % MOD for col in YT] for row in X]
def main():
data = sys.stdin.buffer.read().split()
K = int(data[0]); M = int(data[1])
B = data[2]
m = len(B)
words = data[3:3 + M]
W = max(len(w) for w in words)
# trie cua cac mau viet nguoc: nut -> dict (chi dung khi dung trie, khong nam trong vong nong)
ch = [{}]
fin = [False]
for w in words:
u = 0
for c in reversed(w):
nx = ch[u].get(c)
if nx is None:
nx = len(ch); ch[u][c] = nx; ch.append({}); fin.append(False)
u = nx
fin[u] = True
# coef[i + W - 1] = dp[p + i] bieu dien theo (dp[p], dp[p-1], ..., dp[p-W+1])
coef = [None] * (m + W)
for j in range(W):
e = [0] * W; e[j] = 1
coef[W - 1 - j] = e
for i in range(1, m + 1):
src = []
u = 0
pos = (i - 1) % m
for l in range(1, W + 1): # manh cuoi dai l
u = ch[u].get(B[pos])
if u is None:
break
if fin[u]:
src.append(coef[i - l + W - 1])
pos -= 1
if pos < 0:
pos = m - 1
if not src:
coef[i + W - 1] = [0] * W
elif len(src) == 1:
coef[i + W - 1] = src[0]
else:
coef[i + W - 1] = [s % MOD for s in map(sum, zip(*src))]
A = [coef[m - j + W - 1] for j in range(W)]
v = [0] * W; v[0] = 1
while K:
if K & 1:
v = [sum(map(mul, row, v)) % MOD for row in A]
K >>= 1
if K:
A = mat_mul(A, A, W)
sys.stdout.write(str(v[0]) + "\n")
main()- Mảnh vắt qua nhiều bản $B$: khi $|B|$ nhỏ (ví dụ $|B| = 2$, $W = 50$), một mẫu có thể trải qua 25 bản $B$. Lấy ký tự theo $(i - \ell) \bmod |B|$ là xử lý gọn mọi trường hợp; nếu chỉ ghép hai bản $BB$ để so khớp sẽ sai.
- Tràn số: phần tử ma trận tới $10^9$, tích hai phần tử tới $10^{18}$: phải nhân trong
unsigned long long/long longvà lấy modulo ngay sau mỗi tích. - $K$ tới $10^{18}$: đọc bằng
long long, không dùngint.
Em có thể chạy tay từng dòng của code C++ trên với dữ liệu nhỏ (để dễ theo dõi: $K \le 10^{15}$, $|B| \le 6$, tối đa 6 mẫu dài $\le 5$):
Bộ test chấm "bắt" những lỗi nào?
Bộ test có 27 test: subtask 1 có 9 test, subtask 2 có 8 test, subtask 3 có 10 test (đều có $K \ge 10^{17}$, có test $|B| = 2, 3$ với mẫu dài 50, có test 8068 mẫu, có test toàn chữ a).
| Cách làm | Sub 1 | Sub 2 | Sub 3 | Điểm |
|---|---|---|---|---|
| Cách 1 – QHĐ trên cả dải vải | 9/9 | Lỗi bộ nhớ | Lỗi bộ nhớ | 20 |
| Cách 2 – |B| = 1: ma trận đồng hành | 9/9 | 8/8 | Lỗi bộ nhớ | 50 |
| Cách 3 – ma trận một chu kỳ (lời giải đầy đủ) | 9/9 | 8/8 | 10/10 | 100 |
| Sai: đáp án = (số cách cắt B)^K | 2/9 | 0/8 | 0/10 | 4,44 |
| Sai: chỉ so khớp trong hai bản B liên tiếp | 5/9 | 1/8 | 6/10 | 44,86 |
| Sai: nhân ma trận bằng int (tràn số) | 2/9 | 1/8 | 0/10 | 8,19 |
Để ý: ví dụ trong đề có đúng 8 = 2³ cách, mọi cách đều cắt ngay tại ranh giới các bản $B$ — cả ba lời giải sai ở trên đều ra đúng 8! Đây là lý do không nên tin vào việc "qua ví dụ là đúng".
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: lời giải chuẩn chạy tối đa 0,14 giây (giới hạn 1 giây); Cách 1, 2 trên test có $K \ge 10^9$ bị lỗi vì cố dựng cả dải vải. Lời giải chuẩn được đối chiếu với chương trình vét cạn mọi tập vị trí cắt trên 600 test nhỏ và với Cách 1 trên 300 test vừa (mẫu dài hơn $|B|$ nhiều lần), không lệch test nào; bản Python cho kết quả trùng khớp trên mọi test, chạy tối đa khoảng 1,2 giây ngay cả trên CPython.
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