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 ReviewDengan Review Optimal
150%90%
330%85%
720%80%
1410%75%
305%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). Reset days_since_last ke 0.
  • advanceDay(): increment days_since_last semua soal. Dipanggil sekali per hari untuk simulasi waktu.
  • recommendReview(top_k): hitung skor prioritas = (5 - mastery) * 3 + days_since_last. Sort descending, ambil top_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 #

OperasiTimeSpace
addProblemO(1)O(1)
recordAttemptO(1)O(1)
recommendReview(k)O(n log n) untuk sortO(n)
recommendWeakness(k)O(n + t log t), t = topicO(n)

Kode: Practice Tracker #

cpp
#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:

text
=== 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 editorial

Bedah Kode Baris-per-Baris #

  1. struct Problem { ... int mastery; int days_since_last; }: setiap soal menyimpan metadata untuk tracking. mastery 0–5 menyatakan tingkat penguasaan; days_since_last melacak waktu sejak terakhir dilihat untuk spaced repetition.
  2. p.mastery = min(5, p.mastery + 1);: naikkan mastery setelah AC, capped di 5. min mencegah overflow nilai — mastery tidak bisa melebihi maksimum.
  3. p.mastery = max(0, p.mastery - 1);: turunkan mastery setelah WA/TLE, floored di 0. max mencegah nilai negatif.
  4. 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.
  5. 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.
  6. scored.push_back({-score, p.id});: negatifkan skor untuk sort ascending. Sort default ascending memberi skor tertinggi di paling depan.
  7. unordered_map<string, pair<int,int>> topic_stat;: agregasi per topic. pair<int,int> menyimpan (sum_mastery, count_problem) untuk hitung rata-rata.
  8. sort(weak_topics.begin(), weak_topics.end());: sort ascending by average mastery. Topic dengan rata-rata terendah (paling lemah) muncul pertama.
  9. 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:

HariAktivitasTujuan
Senin2 soal baru di area kelemahanDeliberate practice
Selasa2 soal review (spaced repetition)Konsolidasi memori
Rabu1 hard problemFokus mendalam
Kamis2 soal di area kuatPertahankan mastery
JumatUpsolve 1 soal WA/TLETutup gap
SabtuMock contest 4 soal, 90 menitSimulasi tekanan
MingguRefleksi + baca editorialBelajar 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 IniTarget BerikutnyaIndikator Siap
Easy 5 soal AC berturutCoba MediumMastery easy ≥ 4
Medium 10 soal ACCoba HardMastery medium ≥ 3 pada multiple topic
Hard stuck 3xKembali Medium, perkuat fondasiTidak ada progress signifikan

Tanda Overtraining #

SinyalTindakan
Solusi yang sebelumnya bisa, sekarang tidakIstirahat 1–2 hari, review konsep
Frustrasi berlebihan setelah 1 soalPindah ke soal lain, jangan force
Rata-rata waktu per soal naik 2×Kurangi target harian
Mimpi tentang soal DSAIstirahat total 1 hari

Metrics yang Harus Ditrack #

MetricMengapa PentingCara Mengukur
AC rate per mingguIndikator pemahamanAC / total attempts
Rata-rata waktu per ACEfisiensiTimer per soal
Mastery per topicDistribusi skillSelf-assessment
Soal stuck > 30 menitIndikator kesulitanLog waktu
Topik yang dihindariBias psikologisAudit 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_last menyeimbangkan kelemahan dan waktu.
  • Pola mingguan: 8 soal dengan distribusi belajar baru + review + upsolve.
  • Refleksi mingguan tertulis lebih efektif daripada refleksi mental.