Menyelesaikan 500 soal LeetCode bukan jaminan mahir — bila kita hanya menghafal pola tanpa memahami, ingatan akan pudar dalam hitungan minggu. Dua prinsip psikologi kognitif membantu kita belajar secara efisien: spaced repetition yang memanfaatkan kurva lupa, dan deliberate practice yang fokus pada area kelemahan. Pada Chapter ini kita akan membangun practice tracker yang memantau status setiap soal (AC/WA/unattempted), tingkat mastery, dan hari sejak terakhir dilihat. Tracker memberi rekomendasi soal untuk review (spaced repetition) dan soal baru di area kelemahan (deliberate practice). Tujuan akhirnya: latihan yang efektif, bukan sekadar latihan yang banyak.
Dua Prinsip Utama #
Spaced Repetition #
Otak manusia melupakan informasi secara eksponensial — kurva lupa Ebbinghaus. Untuk mengonsolidasi ingatan jangka panjang, kita perlu mengulang pada interval yang semakin panjang: 1 hari, 3 hari, 7 hari, 14 hari, 30 hari. Setiap pengulangan sukses memperpanjang interval berikutnya.
| Hari ke- | Retention Tanpa Review | Dengan Review Optimal |
|---|---|---|
| 1 | 50% | 90% |
| 3 | 30% | 85% |
| 7 | 20% | 80% |
| 14 | 10% | 75% |
| 30 | 5% | 70% |
Deliberate Practice #
Konsep dari Anders Ericsson: ahli bukan mereka yang berlatih paling lama, melainkan yang berlatih paling fokus pada kelemahan. Alih-alih mengerjakan soal mudah berulang, identifikasi area di mana mastery masih rendah, lalu serang dengan soal-soal yang menantang di area tersebut.
Strategi Latihan: Aturan dan Mekanisme #
Aturan #
Tracker menyimpan untuk setiap soal: status terakhir, jumlah attempts, mastery (0–5), dan hari sejak terakhir dilihat. Rekomendasi soal dibagi dua:
- Spaced repetition: pilih soal dengan mastery rendah dan lama tidak dilihat. Soal dengan mastery 5 (mahir) tetap perlu review, tetapi dengan interval lebih panjang (≥ 14 hari).
- Deliberate practice: hitung rata-rata mastery per topic. Topic dengan rata-rata terendah adalah area kelemahan. Pilih soal unattempted dari topic lemah tersebut.
Mekanisme di Balik Layar #
recordAttempt(id, status): update status soal. Bila AC, mastery naik 1 (max 5). Bila WA/TLE, mastery turun 1 (min 0). Resetdays_since_lastke 0.advanceDay(): incrementdays_since_lastsemua soal. Dipanggil sekali per hari untuk simulasi waktu.recommendReview(top_k): hitung skor prioritas =(5 - mastery) * 3 + days_since_last. Sort descending, ambiltop_k.recommendWeakness(top_k): hitung rata-rata mastery per topic. Sort topic ascending by average. Pilih soal unattempted dari topic dengan rata-rata terendah.
Kompleksitas #
| Operasi | Time | Space |
|---|---|---|
addProblem | O(1) | O(1) |
recordAttempt | O(1) | O(1) |
recommendReview(k) | O(n log n) untuk sort | O(n) |
recommendWeakness(k) | O(n + t log t), t = topic | O(n) |
Kode: Practice Tracker #
#include <iostream>
#include <vector>
#include <string>
#include <unordered_map>
#include <algorithm>
#include <iomanip>
using namespace std;
struct Problem {
string id; // mis. "LeetCode 200"
string topic;
int difficulty; // 1=easy, 2=medium, 3=hard
string last_status; // "AC", "WA", "TLE", "unattempted"
int attempts; // berapa kali dicoba
int days_since_last; // hari sejak terakhir dilihat
int mastery; // 0-5, 5=mahir
};
class PracticeTracker {
public:
void addProblem(const Problem& p) {
problems_[p.id] = p;
}
void recordAttempt(const string& id, const string& status) {
auto& p = problems_[id];
p.last_status = status;
p.attempts++;
p.days_since_last = 0;
if (status == "AC") {
p.mastery = min(5, p.mastery + 1);
} else {
p.mastery = max(0, p.mastery - 1);
}
}
void advanceDay() {
for (auto& kv : problems_) kv.second.days_since_last++;
}
// Spaced repetition: soal dengan mastery rendah & lama tidak dilihat diprioritaskan
vector<string> recommendReview(int top_k = 5) {
vector<pair<int,string>> scored;
for (const auto& kv : problems_) {
const Problem& p = kv.second;
if (p.last_status == "unattempted") continue;
int score = (5 - p.mastery) * 3 + p.days_since_last;
// Mastery 5 tetap perlu review berkala (decay)
if (p.mastery == 5 && p.days_since_last < 14) continue;
scored.push_back({-score, p.id}); // negatif untuk sort asc = score tertinggi
}
sort(scored.begin(), scored.end());
vector<string> result;
for (int i = 0; i < min(top_k, (int)scored.size()); ++i) {
result.push_back(scored[i].second);
}
return result;
}
// Deliberate practice: pilih soal pada area kelemahan
vector<string> recommendWeakness(int top_k = 3) {
unordered_map<string, pair<int,int>> topic_stat;
for (const auto& kv : problems_) {
const Problem& p = kv.second;
if (p.last_status == "unattempted") continue;
topic_stat[p.topic].first += p.mastery;
topic_stat[p.topic].second++;
}
vector<pair<double,string>> weak_topics;
for (const auto& kv : topic_stat) {
double avg = (double)kv.second.first / kv.second.second;
weak_topics.push_back({avg, kv.first});
}
sort(weak_topics.begin(), weak_topics.end());
vector<string> result;
for (const auto& wt : weak_topics) {
for (const auto& kv : problems_) {
const Problem& p = kv.second;
if (p.topic == wt.second && p.last_status == "unattempted") {
result.push_back(p.id);
if ((int)result.size() >= top_k) return result;
}
}
}
return result;
}
void report() {
cout << "=== Status Latihan ===\n";
cout << left << setw(15) << "Problem" << setw(12) << "Topic"
<< setw(8) << "Diff" << setw(12) << "Status"
<< setw(10) << "Attempts" << setw(9) << "Mastery"
<< setw(6) << "Days" << "\n";
cout << "---------------|------------|--------|------------|---------|--------|----\n";
for (const auto& kv : problems_) {
const Problem& p = kv.second;
cout << left << setw(15) << p.id
<< setw(12) << p.topic
<< setw(8) << diffLabel(p.difficulty)
<< setw(12) << p.last_status
<< setw(10) << p.attempts
<< setw(9) << p.mastery
<< setw(6) << p.days_since_last << "\n";
}
}
private:
unordered_map<string, Problem> problems_;
static string diffLabel(int d) {
switch (d) {
case 1: return "Easy";
case 2: return "Medium";
case 3: return "Hard";
default: return "?";
}
}
};
int main() {
PracticeTracker tracker;
tracker.addProblem({"LC-200", "graph", 2, "AC", 2, 3, 2});
tracker.addProblem({"LC-207", "graph", 2, "WA", 3, 1, 1});
tracker.addProblem({"LC-322", "dp", 3, "WA", 4, 0, 0});
tracker.addProblem({"LC-70", "dp", 1, "AC", 1, 4, 5});
tracker.addProblem({"LC-1", "hashing", 1, "AC", 2, 5, 30});
tracker.addProblem({"LC-300", "dp", 2, "unattempted", 0, 0, 0});
tracker.addProblem({"LC-739", "stack", 2, "unattempted", 0, 0, 0});
tracker.addProblem({"LC-215", "heap", 2, "AC", 2, 3, 7});
tracker.addProblem({"LC-23", "heap", 3, "unattempted", 0, 0, 0});
tracker.report();
cout << "\n=== Spaced Repetition: Top 5 Soal untuk Review ===\n";
auto review = tracker.recommendReview(5);
for (const auto& id : review) cout << " - " << id << "\n";
cout << "\n=== Deliberate Practice: 3 Soal di Area Kelemahan ===\n";
auto weak = tracker.recommendWeakness(3);
for (const auto& id : weak) cout << " - " << id << "\n";
cout << "\n=== Pola Latihan Mingguan ===\n";
cout << "Senin: 2 soal baru di area kelemahan\n";
cout << "Selasa: 2 soal review (spaced repetition)\n";
cout << "Rabu: 1 hard problem (fokus mendalam)\n";
cout << "Kamis: 2 soal baru di area kuat (pertahankan)\n";
cout << "Jumat: Upsolve 1 soal yang sebelumnya WA/TLE\n";
cout << "Sabtu: Mock contest (4 soal acak, 90 menit)\n";
cout << "Minggu: Refleksi mingguan + baca editorial\n";
return 0;
}Output yang Dihasilkan:
=== Status Latihan ===
Problem Topic Diff Status Attempts Mastery Days
---------------|------------|--------|------------|---------|--------|----
LC-23 heap Hard unattempted0 0 0
LC-215 heap Medium AC 2 3 7
LC-739 stack Medium unattempted0 0 0
LC-1 hashing Easy AC 2 5 30
LC-70 dp Easy AC 1 4 5
LC-322 dp Hard WA 4 0 0
LC-207 graph Medium WA 3 1 1
LC-300 dp Medium unattempted0 0 0
LC-200 graph Medium AC 2 2 3
=== Spaced Repetition: Top 5 Soal untuk Review ===
- LC-322
- LC-207
- LC-200
- LC-215
- LC-1
=== Deliberate Practice: 3 Soal di Area Kelemahan ===
- LC-300
- LC-23
=== Pola Latihan Mingguan ===
Senin: 2 soal baru di area kelemahan
Selasa: 2 soal review (spaced repetition)
Rabu: 1 hard problem (fokus mendalam)
Kamis: 2 soal baru di area kuat (pertahankan)
Jumat: Upsolve 1 soal yang sebelumnya WA/TLE
Sabtu: Mock contest (4 soal acak, 90 menit)
Minggu: Refleksi mingguan + baca editorialBedah Kode Baris-per-Baris #
struct Problem { ... int mastery; int days_since_last; }: setiap soal menyimpan metadata untuk tracking.mastery0–5 menyatakan tingkat penguasaan;days_since_lastmelacak waktu sejak terakhir dilihat untuk spaced repetition.p.mastery = min(5, p.mastery + 1);: naikkan mastery setelah AC, capped di 5.minmencegah overflow nilai — mastery tidak bisa melebihi maksimum.p.mastery = max(0, p.mastery - 1);: turunkan mastery setelah WA/TLE, floored di 0.maxmencegah nilai negatif.int score = (5 - p.mastery) * 3 + p.days_since_last;: formula prioritas spaced repetition. Mastery rendah mendapat bobot 3× lebih besar daripada hari tidak dilihat — mengoreksi kelemahan lebih penting daripada mengulang yang sudah baik.if (p.mastery == 5 && p.days_since_last < 14) continue;: soal dengan mastery 5 (mahir) tetap perlu review, tetapi hanya bila sudah ≥ 14 hari. Ini mencegah over-review soal yang sudah dikuasai.scored.push_back({-score, p.id});: negatifkan skor untuk sort ascending. Sort default ascending memberi skor tertinggi di paling depan.unordered_map<string, pair<int,int>> topic_stat;: agregasi per topic.pair<int,int>menyimpan (sum_mastery, count_problem) untuk hitung rata-rata.sort(weak_topics.begin(), weak_topics.end());: sort ascending by average mastery. Topic dengan rata-rata terendah (paling lemah) muncul pertama.for (const auto& kv : problems_) { if (p.topic == wt.second && p.last_status == "unattempted") { ... } }: cari soal unattempted dari topic lemah. Soal unattempted adalah peluang belajar baru — lebih efektif daripada mengulang soal yang sudah pernah dilihat.
Pola Latihan Mingguan yang Direkomendasikan #
Berdasarkan spaced repetition dan deliberate practice, alokasi mingguan yang seimbang:
| Hari | Aktivitas | Tujuan |
|---|---|---|
| Senin | 2 soal baru di area kelemahan | Deliberate practice |
| Selasa | 2 soal review (spaced repetition) | Konsolidasi memori |
| Rabu | 1 hard problem | Fokus mendalam |
| Kamis | 2 soal di area kuat | Pertahankan mastery |
| Jumat | Upsolve 1 soal WA/TLE | Tutup gap |
| Sabtu | Mock contest 4 soal, 90 menit | Simulasi tekanan |
| Minggu | Refleksi + baca editorial | Belajar dari solusi lain |
Total: ~8 soal/minggu dengan distribusi seimbang antara pembelajaran baru dan konsolidasi.
Difficulty Progression #
Naikkan kesulitan secara bertahap. Lompat dari easy langsung ke hard seringkali menimbulkan frustrasi:
| Level Saat Ini | Target Berikutnya | Indikator Siap |
|---|---|---|
| Easy 5 soal AC berturut | Coba Medium | Mastery easy ≥ 4 |
| Medium 10 soal AC | Coba Hard | Mastery medium ≥ 3 pada multiple topic |
| Hard stuck 3x | Kembali Medium, perkuat fondasi | Tidak ada progress signifikan |
Tanda Overtraining #
| Sinyal | Tindakan |
|---|---|
| Solusi yang sebelumnya bisa, sekarang tidak | Istirahat 1–2 hari, review konsep |
| Frustrasi berlebihan setelah 1 soal | Pindah ke soal lain, jangan force |
| Rata-rata waktu per soal naik 2× | Kurangi target harian |
| Mimpi tentang soal DSA | Istirahat total 1 hari |
Metrics yang Harus Ditrack #
| Metric | Mengapa Penting | Cara Mengukur |
|---|---|---|
| AC rate per minggu | Indikator pemahaman | AC / total attempts |
| Rata-rata waktu per AC | Efisiensi | Timer per soal |
| Mastery per topic | Distribusi skill | Self-assessment |
| Soal stuck > 30 menit | Indikator kesulitan | Log waktu |
| Topik yang dihindari | Bias psikologis | Audit soal terakhir |
Ringkasan #
- Spaced repetition memanfaatkan kurva lupa Ebbinghaus untuk konsolidasi memori jangka panjang.
- Deliberate practice fokus pada area kelemahan, bukan mengulang area kuat.
- Tracker menyimpan status, mastery, dan waktu sejak terakhir dilihat per soal.
- Skor prioritas =
(5 - mastery) * 3 + days_since_lastmenyeimbangkan kelemahan dan waktu. - Pola mingguan: 8 soal dengan distribusi belajar baru + review + upsolve.
- Refleksi mingguan tertulis lebih efektif daripada refleksi mental.