Điệu Nhảy Mùa Xuân (DPRCCOW)
Đề bài
DPRCCOW – Điệu nhảy mùa xuân
Đó là điệu nhảy mùa xuân trong một lần hiếm hoi $N$ con bò đực đã được mời nhảy với $M$ con bò cái (miễn là chúng giữ được hành vi tốt nhất của chúng).
Nông dân John, gần như bị ám ảnh trong tổ chức các điệu nhảy của mình, muốn cảnh tượng đó hấp dẫn nhất có thể. Vì vậy, anh ta muốn ghép cặp $N$ bò đực với các đối tác bò cái sao cho tổng số tất cả các khác biệt về chiều cao của các cặp được giảm thiểu. Bò đực có chiều cao $B_i$ và bò cái có chiều cao $C_i$.
Tất nhiên, một số con bò cái sẽ không thể ghép cặp được vì $N \lt M$. Những con bò cái đó ta không tính vào đáp án.
Dữ liệu vào:
- Dòng 1: Hai số nguyên được phân tách bằng dấu cách: $N$ và $M$.
- Dòng $2..N + 1$: Dòng $i + 1$ chứa một số nguyên duy nhất: $B_i$.
- Dòng $N + 2..M + N + 1$: Dòng $i + N + 1$ chứa một số nguyên duy nhất: $C_i$.
Kết quả: Một số nguyên duy nhất là tổng tối thiểu của giá trị tuyệt đối chênh lệch chiều cao.
Ràng buộc: $1 \le N \le 5000$; $N \lt M \le 5000$; $1 \le C_i \le 1\,000\,000$; $1 \le B_i \le 1\,000\,000$.
5 7
10 16 12 10 13
7 17 12 10 9 6 114Ví dụ của trang chấm ghi các chiều cao trên cùng một dòng, còn đề nói mỗi số một dòng – đọc bằng cin / split() thì kiểu nào cũng được.
Lời mở đầu
Bài này là một bài quy hoạch động rất "kinh điển" về ghép cặp hai dãy. Cái khó không nằm ở công thức, mà nằm ở chỗ vì sao được phép sắp xếp rồi ghép theo thứ tự – mình sẽ chứng minh kỹ phần này cho các bạn. Sau đó là đủ bộ: vét cạn → đệ quy có nhớ → bottom up → rút gọn còn mảng 1 chiều. Hy vọng mang đến được giá trị cho các bạn 😇.
Trước tiên mình tóm tắt lại đề cho đơn giản. Cho $n \lt m$; chiều cao của $n$ con bò đực là $a[1..n]$ (đề gọi là $B_i$), chiều cao của $m$ con bò cái là $b[1..m]$ (đề gọi là $C_i$). Độ chênh lệch của một cặp (bò đực $i$, bò cái $j$) là $|a[i] - b[j]|$. Hãy tạo ra $n$ cặp (mỗi con bò đực một bạn nhảy, mỗi con bò cái nhảy với tối đa một con) sao cho tổng độ chênh lệch nhỏ nhất. Những con bò cái bị thừa thì không tính.
Với ví dụ: $a = \{10, 16, 12, 10, 13\}$, $b = \{7, 17, 12, 10, 9, 6, 11\}$. Cách ghép tối ưu là:
Tổng chênh lệch $= 0 + 1 + 1 + 1 + 1 = 4$. Hai con bò cái cao $6$ và $7$ bị thừa. Vậy output là $4$.
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 OJLộ trình lời giải, bấm vào từng phần để nhảy tới:
Cách 1Sắp xếp rồi vét cạn bằng đệ quy
Vét cạn "thô" thì quá nhiều trường hợp
Cách đơn giản nhất là thử mọi trường hợp: chọn ra $n$ con bò cái trong $m$ con – có $C_m^n$ (tổ hợp chập $n$ của $m$) cách, rồi còn phải sắp xếp $n$ con được chọn để ghép với từng con bò đực – nhân thêm $n!$. Tổng cộng chính là chỉnh hợp chập $n$ của $m$: $A_m^n = \dfrac{m!}{(m - n)!}$. Giả sử $m = 5000$, $n = 2500$ thì có $5000 \cdot 4999 \cdots 2501$ trường hợp – không thể nào chạy kịp, mà code cũng khá phức tạp. Nên ta tới bản cải tiến.
Sắp xếp cả 2 dãy – và chứng minh vì sao được
Ta sắp xếp cả 2 mảng $a$ và $b$ tăng dần. Giả sử ta đã chọn cặp $(a[i], b[j])$. Khi xét tới con bò đực tiếp theo $a[i + 1]$, nó có thể ghép với bất kỳ con bò cái nào chưa được chọn. Nhưng mình sẽ chứng minh: chỉ cần xét các con bò cái từ $j + 1$ tới $m$, không cần xét các con từ $1$ tới $j - 1$. Theo trực giác thì "bé đi với bé, lớn đi với lớn", nhưng nhiều lúc trực giác không đúng nên ta chứng minh cho chắc nha.
Đặt $A = a[i]$, $B = b[j]$, $C = a[i + 1]$, $D = b[j + 1]$, $E = b[j - 1]$. Ta đang chọn cặp $(A, B)$, và lưu ý là trước đó ta đã từng thử trường hợp chọn cặp $(A, E)$ rồi. Lấy $E$ vì nó là con bò cái cao nhất mà vẫn thấp hơn $B$. Câu hỏi là: sau $(A, B)$ có cần thử $(C, E)$ không? Nếu chọn $(A, B)$ rồi $(C, E)$ thì tổng là $AB + CE$; còn trường hợp cũ $(A, E)$ rồi $(C, B)$ thì tổng là $AE + CB$ (độ chênh lệch của cặp $(X, Y)$ = độ dài đoạn $XY$ trên trục số). Ta sẽ thấy luôn có $AB + CE \ge AE + CB$:
$E$ là con bò cái gần $B$ nhất mà còn không cần xét, thì những con thấp hơn nữa (thấp hơn $E$) tất nhiên xét cũng bị thừa. Kết luận:
Hàm đệ quy Try(n, m)
Gọi Try(n, m) trả về độ chênh lệch tối thiểu khi ghép cặp $n$ con bò đực đầu tiên với $m$ con bò cái đầu tiên (ghép hết $n$ con bò đực).
Điều kiện dừng:
- $n = 0$: hết bò đực để ghép, tức đã ghép xong nhiệm vụ → trả về $0$. Code:
if(n == 0) return 0; - $m \lt n$: số bò cái còn lại không đủ cho số bò đực – trường hợp không hợp lệ. Hàm tìm min nên ta trả về một giá trị rất to để nó không bao giờ được chọn. To cỡ nào? Tối đa $5000$ cặp, mỗi cặp chênh lệch chưa tới $10^6$, tổng chưa tới $5 \cdot 10^9$. Vậy cần trả về một số $\gt 5 \cdot 10^9$, mình chọn $10^{15}$:
if(m < n) return (ll)1e15;(anh em thích số nào $\gt 10^{10}$ đều được).
LLONG_MAX (giá trị lớn nhất của long long, $2^{63} - 1$). Làm thế là tràn số ngay, vì ở phần xử lý ta còn cộng thêm: Try(...) + abs(...). Số đã chạm trần mà cộng thêm là tràn. Nên chọn số to vừa đủ như $10^{15}$.Xử lý chạy: ở đây ta đang xét cặp (bò đực thứ $n$, bò cái thứ $m$) – tức là đi ngược từ cuối về đầu. Có ảnh hưởng tới điều vừa chứng minh không? Không nha: ta chỉ cần sắp xếp 2 mảng giảm dần. Sắp giảm dần rồi duyệt từ cuối về đầu thì khác gì sắp tăng dần rồi duyệt từ đầu về cuối đâu. Khi đó ta có 2 lựa chọn:
- Chọn con bò cái thứ $m$ ghép với con bò đực thứ $n$: cặp tiếp theo xét là $(n - 1, m - 1)$, kết quả $=$
Try(n - 1, m - 1) + (ll)abs(a[n] - b[m])(ép kiểulong longcho chắc). - Không chọn con bò cái thứ $m$: sang con bò cái thứ $m - 1$, kết quả $=$
Try(n, m - 1).
return min(Try(n - 1, m - 1) + (ll)abs(a[n] - b[m]), Try(n, m - 1));Source code để các bạn tham khảo nếu làm theo hướng này:
#include <bits/stdc++.h>
#define endl '\n'
using namespace std;
typedef long long ll;
const int nMax = 5005;
ll a[nMax], b[nMax];
ll Try(int n, int m) {
if(n == 0) {
return 0;
}
if(m < n) {
return (ll)1e15;
}
return min(Try(n - 1, m - 1) + (ll)abs(a[n] - b[m]), Try(n, m - 1));
}
int main() {
// freopen("dprccow.inp", "r", stdin);
// freopen("dprccow.out", "w", stdout);
ios_base::sync_with_stdio(0);
cin.tie(0);
int n, m;
cin >> n >> m;
for(int i = 1; i <= n; ++i) {
cin >> a[i];
}
for(int i = 1; i <= m; ++i) {
cin >> b[i];
}
sort(a + 1, a + n + 1, greater<ll>());
sort(b + 1, b + m + 1, greater<ll>());
cout << Try(n, m);
return 0;
}
import sys
sys.setrecursionlimit(20000)
def Try(n, m):
if n == 0:
return 0
if m < n:
return 10 ** 15
return min(Try(n - 1, m - 1) + abs(a[n] - b[m]), Try(n, m - 1))
data = sys.stdin.read().split()
n, m = int(data[0]), int(data[1])
a = [0] + sorted(map(int, data[2:2 + n]), reverse=True)
b = [0] + sorted(map(int, data[2 + n:2 + n + m]), reverse=True)
print(Try(n, m))
Giải thích 1 số chỗ trong code:
const int nMax = 5005;là giá trị lớn nhất của $n$, $m$; mình hay $+5$ khi khai báo mảng cho chắc.sort(a + 1, a + n + 1, greater<ll>());sắp xếp $a[1..n]$ giảm dần. Tham sốgreater<ll>()giúp sort giảm dần; ae nào chưa biết thì có thể tự viết hàmbool compcũng được nha.- Gọi
Try(n, m)vì ta cần tổng chênh lệch tối thiểu khi ghép $n$ con bò đực đầu tiên với $m$ con bò cái đầu tiên. - Code gốc của mình có 2 dòng
freopen("dprccow.inp", ...)/freopen("dprccow.out", ...)để đọc ghi file. Nộp trên CLUE OJ (đọc/ghi chuẩn) thì để chúng ở dạng chú thích như trên; thi offline yêu cầu file thì bỏ dấu//đi nhé.
Các bạn bấm Tiếp để xem cây đệ quy được mở ra thế nào – để ý có những Try(n, m) bị gọi đi gọi lại nhiều lần:
Đánh giá độ phức tạp của cách làm này: mỗi lời gọi tách thành 2 nhánh, số lời gọi tăng theo hàm mũ (cỡ số cách chọn $n$ con bò cái trong $m$ con, $C_m^n$). Mình đo thử: $n = 12$, $m = 24$ chạy rất nhanh, nhưng $n = 20$, $m = 40$ thì chạy hơn 2 phút vẫn chưa xong.
Cách 2Đệ quy có nhớ (quy hoạch động top down) – O(N·M)
Bạn nào code được phần vét cạn kia thì chỉ cần cải tiến thêm vài dòng là có thể AC bài này rồi. Code vét cạn chạy lâu vì gặp những trường hợp đã tính rồi lại đi tính lại (các bạn thấy ở khung chạy tay trên đó). Để giải quyết, ta tạo một bảng nhớ lưu lại kết quả đã tính.
Nhìn vào hàm Try(): đầu vào có 2 biến $n$ và $m$, cả 2 đều thay đổi mỗi lần gọi đệ quy. Vậy ta tạo mảng 2 chiều f với f[n][m] = Try(n, m). $n$, $m$ tối đa $5000$ nên khai báo long long f[nMax][nMax]; (mảng toàn cục để tránh tràn stack).
- Lấp đầy mảng
fbằng $-1$ – giá trị màTrykhông bao giờ tạo ra.f[n][m] != -1nghĩa là đã tính rồi →return f[n][m];(đặt ngay sau điều kiện dừng). - Nếu
f[n][m] == -1thì tính, gán rồi trả về:return f[n][m] = min(Try(n - 1, m - 1) + (ll)abs(a[n] - b[m]), Try(n, m - 1)); - Hàng $n = 0$ thì
Try(0, m)luôn bằng $0$ (và còn bị điều kiện dừng chặn trước), mảng toàn cục sẵn bằng $0$ rồi, nên chỉ cần gán từf[1][0]tớif[n][m]:fill(&f[1][0], &f[n][m + 1], -1);
fill gán trên đoạn $[$đầu, cuối$)$ – ngoặc vuông là có tính phần tử mút, ngoặc tròn là không. Nên mút cuối phải là phần tử ngay sau f[n][m], chính là f[n][m + 1]. (Mảng 2 chiều nằm liên tiếp trong bộ nhớ theo từng hàng, nên đoạn này phủ hết các hàng $1..n$ – hàng $n$ thì tới cột $m$.)Source code để các bạn tham khảo nếu làm theo hướng này:
#include <bits/stdc++.h>
#define endl '\n'
using namespace std;
typedef long long ll;
const int nMax = 5005;
ll a[nMax], b[nMax];
ll f[nMax][nMax];
ll Try(int n, int m) {
if(n == 0) {
return 0;
}
if(m < n) {
return (ll)1e15;
}
if(f[n][m] != -1) {
return f[n][m];
}
return f[n][m] = min(Try(n - 1, m - 1) + (ll)abs(a[n] - b[m]), Try(n, m - 1));
}
int main() {
// freopen("dprccow.inp", "r", stdin);
// freopen("dprccow.out", "w", stdout);
ios_base::sync_with_stdio(0);
cin.tie(0);
int n, m;
cin >> n >> m;
fill(&f[1][0], &f[n][m + 1], -1);
for(int i = 1; i <= n; ++i) {
cin >> a[i];
}
for(int i = 1; i <= m; ++i) {
cin >> b[i];
}
sort(a + 1, a + n + 1, greater<ll>());
sort(b + 1, b + m + 1, greater<ll>());
cout << Try(n, m);
return 0;
}
import sys
sys.setrecursionlimit(20000)
def Try(n, m):
if n == 0:
return 0
if m < n:
return 10 ** 15
if f[n][m] != -1:
return f[n][m]
f[n][m] = min(Try(n - 1, m - 1) + abs(a[n] - b[m]), Try(n, m - 1))
return f[n][m]
data = sys.stdin.read().split()
n, m = int(data[0]), int(data[1])
a = [0] + sorted(map(int, data[2:2 + n]), reverse=True)
b = [0] + sorted(map(int, data[2 + n:2 + n + m]), reverse=True)
f = [[-1] * (m + 1) for _ in range(n + 1)]
print(Try(n, m))
Đánh giá độ phức tạp của cách làm này:
- Không gian: $O(N \cdot M)$. Thực chất là $O(N \cdot M + N + M)$ vì còn 2 mảng $a$, $b$, nhưng so với mảng $f$ thì không đáng kể.
- Thời gian: $O(N \cdot M)$ – trường hợp xấu nhất ta lấp đầy toàn bộ mảng $f$, mỗi ô tính 1 lần với $O(1)$. Tức khoảng $5000 \cdot 5000 = 2.5 \cdot 10^7$ phép tính, dư sức chạy dưới 1 giây. Vậy là AC 🎉.
long long f[5005][5005] chiếm khoảng $5005 \cdot 5005 \cdot 8 \approx 200$ MB. Mình đo với $N = 2500$, $M = 5000$ thì chương trình dùng khoảng 194 MB. Nếu hệ thống chấm cho bộ nhớ ít hơn thì các bạn dùng cách 4 (mảng 1 chiều) nhé.Phương pháp trên được gọi là quy hoạch động top down – đệ quy có nhớ. Để làm được thì mấu chốt là anh em phải xem biến nào thay đổi khi ta gọi đệ quy.
Cách 3Quy hoạch động bottom up – O(N·M)
Chúng ta dp top down được thì cũng dp bottom up được – tức là tạo ra công thức truy hồi. Cho ae nào đã biết cơ bản về quy hoạch động rồi thì ta có 4 bước:
Bước 1 – Bài toán con: gọi $F[i][j]$ là tổng độ chênh lệch tối thiểu khi ghép cặp $i$ con bò đực đầu tiên với $j$ con bò cái đầu tiên.
Bước 2 – Bài toán cơ sở: $i = 0$ (không có con bò đực nào) thì $F[0][j] = 0$ với $0 \le j \le m$ – mảng toàn cục sẵn bằng $0$. Còn các trường hợp $j \lt i$ (thiếu bò cái) là không hợp lệ, gán $10^{15}$. Cho tiện ta gán toàn bộ (trừ hàng $0$) bằng $10^{15}$: fill(&f[1][0], &f[n][m + 1], (ll)1e15);
Bước 3 – Đáp án: hiển nhiên là $F[n][m]$.
Bước 4 – Công thức truy hồi: chuyển thẳng từ hàm Try. Xét con bò đực thứ $i$ và con bò cái thứ $j$ ($i \ne 0$, $j \ge i$):
- Chọn con bò cái thứ $j$ ghép với con bò đực thứ $i$: trước đó chỉ còn $i - 1$ con bò đực và $j - 1$ con bò cái, nên được $F[i - 1][j - 1] + |a[i] - b[j]|$.
- Không chọn con bò cái thứ $j$: kết quả bằng lần trước đó – khi xét $i$ con bò đực và $j - 1$ con bò cái: $F[i][j - 1]$.
Giờ ta duyệt xuôi ($i$ con bò đực đầu, $j$ con bò cái đầu, từ đầu về cuối) nên cho chắc thì sắp xếp 2 mảng tăng dần. Vòng for(i) chạy từ $1$ tới $n$ (tránh $i = 0$), vòng for(j) chạy từ $i$ tới $m$ (tránh $j \lt i$).
Minh hoạ bảng F
Các bạn nhập 2 dãy rồi bấm Tiếp để xem từng ô $F[i][j]$ được tính: ô viền xanh lá là $F[i-1][j-1]$ (lựa chọn ghép), ô viền xanh dương là $F[i][j-1]$ (lựa chọn bỏ qua). Cuối cùng mình lần ngược để chỉ ra các cặp được ghép:
Source code để các bạn tham khảo nếu làm theo hướng này:
#include <bits/stdc++.h>
#define endl '\n'
using namespace std;
typedef long long ll;
const int nMax = 5005;
ll a[nMax], b[nMax];
ll f[nMax][nMax];
int main() {
// freopen("dprccow.inp", "r", stdin);
// freopen("dprccow.out", "w", stdout);
ios_base::sync_with_stdio(0);
cin.tie(0);
int n, m;
cin >> n >> m;
for(int i = 1; i <= n; ++i) {
cin >> a[i];
}
for(int i = 1; i <= m; ++i) {
cin >> b[i];
}
sort(a + 1, a + n + 1);
sort(b + 1, b + m + 1);
fill(&f[1][0], &f[n][m + 1], (ll)1e15);
for(int i = 1; i <= n; ++i) {
for(int j = i; j <= m; ++j) {
f[i][j] = min(f[i - 1][j - 1] + (ll)abs(a[i] - b[j]), f[i][j - 1]);
}
}
cout << f[n][m];
return 0;
}
import sys
data = sys.stdin.read().split()
n, m = int(data[0]), int(data[1])
a = [0] + sorted(map(int, data[2:2 + n]))
b = [0] + sorted(map(int, data[2 + n:2 + n + m]))
INF = 10 ** 15
f = [[0] * (m + 1)] + [[INF] * (m + 1) for _ in range(n)]
for i in range(1, n + 1):
fi, fp, ai = f[i], f[i - 1], a[i]
for j in range(i, m + 1):
fi[j] = min(fp[j - 1] + abs(ai - b[j]), fi[j - 1])
print(f[n][m])
Đánh giá độ phức tạp: cả không gian lẫn thời gian đều như cách top down, $O(N \cdot M)$. Nhưng trớ trêu ở chỗ: dù không phải tốn thời gian và bộ nhớ cho việc gọi đệ quy, cách bottom up này khi mình nộp lại còn chạy lâu hơn cả cách top down 😅 (thời gian đo trên hệ thống chấm có dao động; mình chạy thử trên máy thì cách 3 lại nhanh hơn cách 2 một chút). Tuy vậy vẫn AC, và nhờ nó ta cải tiến được thành một cách ăn ít bộ nhớ và chạy nhanh hơn nữa ở ngay dưới đây.
Cách 4Rút gọn còn mảng 1 chiều – tham khảo
Phần này tham khảo thôi nha ae, sẽ khá khó hiểu đó. Ta giảm mảng 2 chiều $F[i][j]$ về mảng 1 chiều $F[i]$ (tổng chênh lệch tối thiểu khi xét $i$ con bò đực), đáp án là $F[n]$. Công thức
trở thành
Đã bỏ $[j]$ đi thì ta phải đẩy vòng for(j) ra ngoài – ngầm hiểu là với mỗi con bò cái, nó đi ghép thử với mọi con bò đực có thể ghép – còn vòng for(i) bị đẩy vào trong và phải chạy ngược để tránh bị trùng. Vì sao chạy ngược? Khi tính $F[i]$ ở lượt $j$, ta cần $F[i - 1]$ của lượt $j - 1$ (chính là $F[i-1][j-1]$). Chạy $i$ giảm dần thì lúc tính $F[i]$, ô $F[i - 1]$ chưa bị ghi đè ở lượt $j$, nên nó vẫn là giá trị cũ. Còn $F[i]$ lúc chưa gán chính là $F[i][j - 1]$.
Chạy ngược từ đâu? Con bò đực thứ $i$ chỉ ghép được với các con bò cái $j = i, i + 1, \ldots, m$. Nghĩ ngược lại: con bò cái thứ $j$ chỉ ghép được với các con bò đực từ $1$ tới $j$ (ví dụ con bò cái thứ $1$ chỉ đi với con bò đực thứ $1$; con bò cái thứ $5$ đi được với bò đực $1..5$, bò đực thứ $6$ tối thiểu phải đi với con cái thứ $6$). Mà chỉ có $n$ con bò đực, nên vòng for(i) chạy ngược từ $\min(j, n)$ về $1$.
Trường hợp cơ sở chỉ còn $F[0] = 0$, còn lại gán $10^{15}$: fill(&f[1], &f[n + 1], (ll)1e15);. Thứ tự duyệt ngược của for(i) không ảnh hưởng gì tới việc sắp xếp, bản chất vẫn là công thức truy hồi cũ, nên vẫn sắp xếp 2 mảng tăng dần. Khó hình dung thì các bạn bấm chạy tay bên dưới, để ý mảng f sau mỗi vòng $j$ chính là một cột của bảng $F$ ở cách 3.
Source code để các bạn tham khảo nếu làm theo hướng này:
#include <bits/stdc++.h>
#define endl '\n'
using namespace std;
typedef long long ll;
const int nMax = 5005;
ll a[nMax], b[nMax];
ll f[nMax];
int main() {
// freopen("dprccow.inp", "r", stdin);
// freopen("dprccow.out", "w", stdout);
ios_base::sync_with_stdio(0);
cin.tie(0);
int n, m;
cin >> n >> m;
for(int i = 1; i <= n; ++i) {
cin >> a[i];
}
for(int i = 1; i <= m; ++i) {
cin >> b[i];
}
sort(a + 1, a + n + 1);
sort(b + 1, b + m + 1);
fill(&f[1], &f[n + 1], (ll)1e15);
for(int j = 1; j <= m; ++j) {
for(int i = min(j, n); i >= 1; --i) {
f[i] = min(f[i], f[i - 1] + (ll)abs(a[i] - b[j]));
}
}
cout << f[n];
return 0;
}
import sys
data = sys.stdin.read().split()
n, m = int(data[0]), int(data[1])
a = [0] + sorted(map(int, data[2:2 + n]))
b = [0] + sorted(map(int, data[2 + n:2 + n + m]))
INF = 10 ** 15
f = [0] + [INF] * n
for j in range(1, m + 1):
bj = b[j]
for i in range(min(j, n), 0, -1):
f[i] = min(f[i], f[i - 1] + abs(a[i] - bj))
print(f[n])
Đánh giá độ phức tạp của cách làm này:
- Không gian: $O(M)$. Thực chất là $O(N + M + N)$ cho 2 mảng $a$, $b$ và mảng $f$; với $n = 4999$, $m = 5000$ thì xấp xỉ $3 \cdot m$, theo quy tắc big O là $O(M)$. Ăn ít bộ nhớ hơn rất nhiều rồi.
- Thời gian: vẫn $O(N \cdot M)$, nhưng mảng 1 chiều nhỏ nằm gọn trong bộ nhớ đệm nên truy xuất nhanh hơn, code chạy nhanh hơn hẳn.
- 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