Tuyển sinh 🎓 Mình nhận đào tạo lập trình giải thuật thi đấu: thi vào lớp 10 chuyên tin · thi học sinh giỏi tin học cấp tỉnh/thành · thi vào đội tuyển quốc gia · Olympic tin học sinh viên / ICPC · phỏng vấn thuật toán. Bấm vào đây để xem thông tin khóa học → Xem khóa học
Thi thử miễn phí
🏆 Thi thử HSG Tin học tuần này trên ClueOJ
Mở công khai cho mọi người · đề do thầy Nguyễn Việt Nam Sơn biên soạn
Lịch thi, đề cũ & lời giải →

Bài 32: Tổng ôn Thuật toán nâng cao: nhận dạng bài và đề thi thử 6 câu có lời giải C++ và Python

Bài 32: Tổng ôn Thuật toán nâng cao: nhận dạng bài và đề thi thử 6 câu có lời giải C++ và Python
Series: Thuật toán nâng cao Độ khó: Khá Bài 32 / Thuật toán nâng cao Tổng ôn Đề thi thử
// Thuật toán nâng cao · Bài 32
Tổng ôn Thuật toán nâng cao: nhận dạng bài và đề thi thử 6 câu
Tám bài học, hơn chục kỹ thuật. Bài cuối series giúp em xếp chúng lên một tấm bản đồ, luyện phản xạ “đọc đề là biết dùng gì”, rồi tự kiểm tra bằng một đề thi thử 180 phút có lời giải chi tiết.

Lời mở đầu

Vậy là em đã đi hết series Thuật toán nâng cao: từ chia để trị ở bài 24 tới Fenwick và segment tree ở bài 31. Học từng bài thì thấy bài nào cũng dễ hiểu, nhưng vào phòng thi, đề không bao giờ ghi “bài này dùng Dijkstra”. Cái khó nhất lúc ấy là nhận ra kỹ thuật cần dùng, và biết ăn điểm từng subtask khi chưa nghĩ ra lời giải đầy đủ.

Giống như bài 23 khép lại series Thuật toán cơ bản, bài này không dạy kỹ thuật mới. Mình sẽ cùng em: vẽ lại bản đồ kiến thức của cả series, luyện cách đọc giới hạn để đoán độ phức tạp, làm một đề thi thử 6 câu từ dễ tới khó, rồi chữa thật kỹ từng câu, đi từ vét cạn lấy điểm subtask tới lời giải tối ưu. Em nhớ tự làm đề trước (bấm giờ 180 phút), sau đó mới đọc lời giải nhé.

Trong bài này bạn sẽ học: bản đồ toàn bộ kỹ thuật của series kèm dấu hiệu nhận biết trong đề; bảng giới hạn n → độ phức tạp để chọn thuật toán; chiến thuật chia thời gian và ăn subtask trong phòng thi; đề thi thử 6 câu (180 phút) phủ nghịch thế, cửa sổ trượt, tổ hợp modulo, Dijkstra, DSU/Kruskal, Fenwick; lời giải chi tiết từng câu đi từ vét cạn tới tối ưu, code C++ và Python. Có mô phỏng nhận dạng bài, khung chạy tay cửa sổ trượt bằng hai deque, mô phỏng Kruskal với đỉnh ảo, mô phỏng đếm bộ ba bằng Fenwick, và lộ trình luyện tập tiếp theo.

Lộ trình bài viết, bấm vào từng phần để nhảy tới:

Phần 1Bản đồ kiến thức của series

Bảng dưới đây là “tờ giấy nhớ” của cả series. Cột quan trọng nhất là dấu hiệu trong đề: đó là những cụm từ, những kiểu giới hạn mà hễ nhìn thấy là em phải nghĩ ngay tới kỹ thuật tương ứng. Bấm vào tên bài để mở lại lý thuyết khi cần.

Kỹ thuậtBàiDấu hiệu trong đềĐộ phức tạp
Merge sort, đếm nghịch thếBài 24đếm cặp $i < j$ mà $a_i > a_j$; “số lần đổi chỗ kề nhau”$O(n \log n)$
Quickselect, nth_elementBài 24số nhỏ thứ $k$, trung vị, $n$ tới $10^7$$O(n)$ TB
Ngăn xếp đơn điệuBài 25“gần nhất bên trái/phải lớn hơn”, hình chữ nhật lớn nhất$O(n)$
Hàng đợi đơn điệu (deque)Bài 25min/max mọi đoạn dài $k$, cửa sổ trượt, QHĐ $f_i = \min f_j + \ldots$ với $j$ trong cửa sổ$O(n)$
Lũy thừa nhanh, nghịch đảo moduloBài 26“in theo modulo $10^9+7$”, số mũ tới $10^{18}$, phép chia dưới modulo$O(\log b)$
Tổ hợp $C(n, k)$ moduloBài 26đếm số cách chọn, đường đi trên lưới, nhiều truy vấn$O(N)$ + $O(1)$/truy vấn
QHĐ có truy vếtBài 27“in ra một phương án tối ưu”như QHĐ gốc
QHĐ đoạnBài 27gộp/cắt đoạn liên tiếp, $n \le 500$, nhân ma trận, xâu đối xứng$O(n^3)$
QHĐ bitmaskBài 27$n \le 20$, “đi qua mỗi đỉnh đúng 1 lần”, phân công$O(2^n \cdot n^2)$
QHĐ chữ sốBài 27đếm số trong $[L, R]$ với $R \le 10^{18}$ theo tính chất chữ số$O(\text{số chữ số} \cdot \text{trạng thái})$
Hash xâuBài 28so sánh nhanh hai xâu con, xâu con lặp lại, kết hợp tìm kiếm nhị phân$O(n)$ + $O(1)$/so sánh
KMP (hàm tiền tố)Bài 28tìm mọi lần xuất hiện của mẫu, chu kỳ nhỏ nhất của xâu$O(n + m)$
DijkstraBài 29đường đi ngắn nhất, trọng số không âm, một nguồn$O(m \log n)$
BFS 0-1Bài 29trọng số chỉ 0 hoặc 1 (“miễn phí / tốn 1 lượt”)$O(n + m)$
FloydBài 29mọi cặp đỉnh, $n \le 400$$O(n^3)$
Bellman-FordBài 29cạnh âm, phát hiện chu trình âm$O(nm)$
DSUBài 30gộp nhóm, “cùng nhóm không?”, đếm thành phần khi thêm cạnh$\approx O(1)$/thao tác
Kruskal, PrimBài 30“nối mọi điểm với chi phí nhỏ nhất”, cây khung nhỏ nhất$O(m \log m)$
Fenwick (BIT)Bài 31sửa điểm + tổng tiền tố, đếm “bao nhiêu số nhỏ hơn đã gặp”$O(\log n)$/thao tác
Segment treeBài 31sửa + hỏi min/max/tổng trên đoạn, cập nhật đoạn (lazy)$O(\log n)$/thao tác

Bảng 1. Bản đồ kiến thức series Thuật toán nâng cao: kỹ thuật, bài học, dấu hiệu nhận biết và độ phức tạp.

Em để ý: rất nhiều bài thi là tổ hợp của hai kỹ thuật. Đếm nghịch thế làm được bằng merge sort (bài 24) hoặc Fenwick (bài 31). Bài đường đi có “phiếu giảm giá” là Dijkstra (bài 29) trên đồ thị trạng thái, giống cách nghĩ trạng thái của QHĐ (bài 27). Kruskal (bài 30) bản chất là sắp xếp + DSU. Vì vậy khi ôn, đừng chỉ học thuộc từng bài, hãy tự hỏi “kỹ thuật này giải quyết được thao tác gì trong $O(?)$”.

Ba câu hỏi để “định vị” một bài
1. Đề hỏi gì? Đếm, tìm min/max, tồn tại hay không, in phương án? Trên dãy, xâu, lưới hay đồ thị?
2. Có thao tác gì lặp đi lặp lại? Hỏi tổng đoạn, so sánh xâu con, gộp nhóm, lấy min cửa sổ… Thao tác lặp lại nhiều nhất quyết định cấu trúc dữ liệu.
3. Giới hạn cho phép bao nhiêu? Đây là gợi ý mạnh nhất, Phần 2 sẽ nói kỹ.

Phần 2Đọc giới hạn để chọn thuật toán

Máy chấm thường chạy khoảng $10^8$ phép tính đơn giản mỗi giây (Python chậm hơn chừng 10 – 50 lần). Nhân giới hạn vào công thức độ phức tạp là em biết ngay cách nào qua, cách nào không. Nhắc lại từ bài 9, nhưng nay mỗi dòng đã có thêm “vũ khí” của series này:

Giới hạnĐộ phức tạp chấp nhậnKỹ thuật của series nên nghĩ tới
$n \le 10$$O(n!)$quay lui (bài 15) thử mọi hoán vị
$n \le 20$$O(2^n \cdot n)$, $O(2^n \cdot n^2)$QHĐ bitmask (bài 27)
$n \le 500$$O(n^3)$QHĐ đoạn (bài 27), Floyd (bài 29)
$n \le 5000$$O(n^2)$QHĐ hai chiều, vét cạn mọi cặp, Bellman-Ford trên đồ thị nhỏ
$n \le 2 \cdot 10^5$$O(n \log n)$, $O(n \log^2 n)$sắp xếp, merge sort (bài 24), Dijkstra (bài 29), Kruskal (bài 30), Fenwick, segment tree (bài 31), hash + chặt nhị phân (bài 28)
$n \le 10^6$ – $10^7$$O(n)$ngăn xếp/hàng đợi đơn điệu (bài 25), KMP (bài 28), giai thừa nghịch đảo (bài 26), BFS 0-1 (bài 29)
$n \le 10^{18}$$O(\log n)$ hoặc theo số chữ sốlũy thừa nhanh (bài 26), QHĐ chữ số (bài 27), tìm kiếm nhị phân (bài 16)

Bảng 2. Từ giới hạn tới độ phức tạp và kỹ thuật. Với Python, nên lùi xuống một bậc khi giới hạn sát ngưỡng.

Ca 1: “Đếm số cặp …” với $n \le 2 \cdot 10^5$

Đề nói: đếm số cặp $(i, j)$, $i < j$, thoả một điều kiện so sánh giữa $a_i$ và $a_j$. Nghĩ tới: đếm mọi cặp là $O(n^2) = 4 \cdot 10^{10}$, chắc chắn quá chậm. Cố định $j$, cần đếm nhanh “có bao nhiêu $i$ đứng trước thoả điều kiện” → hoặc Fenwick trên giá trị (thêm dần từng số, hỏi tiền tố), hoặc merge sort đếm lúc trộn. Nếu điều kiện là tổng/hiệu cố định thì có khi chỉ cần map đếm.

Ca 2: “Sau mỗi thao tác, in ra …”

Đề nói: có $q \le 10^5$ thao tác xen kẽ, sau mỗi thao tác in kết quả. Nghĩ tới: tính lại từ đầu mỗi lần là $O(nq)$. Phải tìm cấu trúc dữ liệu cập nhật được dần. Thao tác “nối hai nhóm, hỏi số nhóm” → DSU. Thao tác “sửa một số, hỏi tổng/min đoạn” → Fenwick / segment tree. Nếu thao tác là xoá cạnh thì mẹo là đảo ngược thứ tự: xử lý từ cuối lên, xoá biến thành thêm, rồi dùng DSU.

Ca 3: “Đáp án rất lớn, in theo modulo $10^9 + 7$”

Đề nói: đếm số cách, đáp án in theo modulo. Nghĩ tới: đây là bài đếm, gần như chắc chắn là QHĐ (bài 27) hoặc công thức tổ hợp (bài 26). Nếu đối tượng là “chọn $k$ trong $n$”, “đường đi trên lưới”, “chia kẹo” thì thử tìm công thức $C(n, k)$ trước; nếu có ràng buộc phức tạp thì quay về QHĐ. Thấy phép chia dưới modulo thì nhớ nghịch đảo modulo bằng lũy thừa nhanh: $a^{-1} \equiv a^{p-2} \pmod p$.

Giờ em tự luyện phản xạ với 10 đề ngắn dưới đây. Mỗi đề, em đọc rồi tự đoán kỹ thuật trước, sau đó bấm Tiếp để mở dần từng dấu hiệu và đáp án. Nhập số đề (1 – 10) để luyện riêng một đề:

MÔ PHỎNGNhận dạng bài: từ dấu hiệu trong đề tới kỹ thuậtMỗi đề mở dần từng dấu hiệu rồi mới lộ kỹ thuật. Hãy tự đoán trước khi bấm Tiếp!
dấu hiệu trong đềkỹ thuật cần dùngchưa mở
Giới hạn nhỏ bất thường là một lời nhắn
Khi phần lớn giới hạn là $10^5$ mà có một tham số chỉ $\le 20$ (số loại, số đỉnh đặc biệt, $k$…), người ra đề đang “thì thầm” rằng em được phép mũ hoá tham số đó: bitmask, hoặc chạy Dijkstra/BFS $k$ lần. Ngược lại, giới hạn $10^{18}$ nghĩa là không được duyệt, phải $O(\log)$ hoặc theo chữ số.

Phần 3Chiến thuật trong phòng thi

Đề thi HSG thường có 4 – 6 câu, mỗi câu chia subtask: subtask nhỏ cho vét cạn, subtask lớn mới cần thuật toán tối ưu. Điểm của em là tổng điểm subtask, nên chiến thuật quan trọng không kém thuật toán.

Thời gianViệc cần làm
10 phút đầuĐọc hết 6 câu, ghi nhanh dấu hiệu và giới hạn từng subtask. Đánh dấu câu chắc làm được.
60 phútLàm trọn các câu dễ (thường câu 1 – 3). Mỗi câu xong phải tự thử biên: $n = 1$, toàn số bằng nhau, số lớn nhất.
60 phútVới câu khó: viết vét cạn lấy subtask 1 trước, nộp ngay. Sau đó mới nghĩ lời giải tối ưu.
40 phútĐối chiếu lời giải tối ưu với vét cạn bằng test ngẫu nhiên (stress test). Còn thời gian thì vét thêm subtask câu cuối.
10 phút cuốiKhông sửa lớn nữa. Kiểm tra tên tệp, đọc/ghi đúng định dạng, đã bỏ dòng in gỡ lỗi chưa.

Bảng 3. Gợi ý chia 180 phút cho đề 6 câu.

Vét cạn không phải là “làm dở”
Code vét cạn có ba công dụng: (1) lấy chắc điểm subtask nhỏ; (2) giúp em hiểu đề đúng trước khi tối ưu; (3) làm “đáp án chuẩn” để đối chiếu với lời giải nhanh. Phần lời giải bên dưới, câu nào mình cũng viết bản vét cạn trước.

Stress test là cách nhanh nhất để bắt lỗi khi chưa có máy chấm: sinh hàng trăm test nhỏ ngẫu nhiên, chạy cả code nhanh và code vét cạn, hễ khác nhau thì in test đó ra. Một đoạn Python ngắn là đủ:

import random, subprocess
for it in range(500):
    n = random.randint(1, 8)
    a = [random.randint(1, 5) for _ in range(n)]
    inp = f"{n}\n" + " ".join(map(str, a)) + "\n"
    fast = subprocess.run(["./fast"], input=inp, capture_output=True, text=True).stdout
    slow = subprocess.run(["./brute"], input=inp, capture_output=True, text=True).stdout
    if fast.split() != slow.split():
        print("SAI:", inp, fast, slow); break
else:
    print("OK 500 test")

Giá trị nhỏ (1 – 5) cố ý tạo nhiều số bằng nhau, nơi các lỗi < / <= hay ẩn nấp nhất.

Bài tậpĐề thi thử 6 câu (180 phút)

Em chuẩn bị giấy nháp, bấm đồng hồ 180 phút và làm như thi thật. Mỗi câu có gợi ý và code tham khảo nhưng đều được giấu, em chỉ mở khi đã hết giờ hoặc bí thật sự. Lời giải đầy đủ (đi từ vét cạn tới tối ưu) nằm ở Phần 4.

CâuTên bàiĐiểmMức độ
1Xếp hàng chào cờ15Dễ
2Đoạn ổn định dài nhất15Trung bình
3Đường về tránh ô sửa15Trung bình
4Phiếu giảm giá taxi20Khá
5Kéo điện về làng15Khá
6Bộ ba tăng dần20Khó

Bảng 4. Cấu trúc đề thi thử, tổng 100 điểm. Giới hạn thời gian mỗi test: 1 giây (C++), Python được nới gấp đôi.

Bài 1Xếp hàng chào cờDễ

Hàng chào cờ có $n$ bạn, bạn thứ $i$ cao $h_i$. Mỗi lần, thầy được đổi chỗ hai bạn đứng cạnh nhau. Hỏi cần ít nhất bao nhiêu lần đổi để hàng có chiều cao không giảm từ đầu tới cuối?

Giới hạn: $1 \le n \le 2 \cdot 10^5$, $1 \le h_i \le 10^9$. Subtask 1 (50% số điểm): $n \le 1000$. Subtask 2 (50%): không có ràng buộc thêm.

Input
5
160 150 170 150 155
Output
5

📝 Nộp bài, chấm tự động trên ClueOJ ↗

💡 Gợi ý
Mỗi lần đổi hai bạn kề nhau làm số cặp nghịch thế (cặp $i < j$ mà $h_i > h_j$) thay đổi đúng 1, và khi hàng còn nghịch thế thì luôn có một cặp kề nhau bị ngược để đổi. Vậy đáp án = số cặp nghịch thế. Ví dụ: (160,150), (160,150), (160,155), (170,150), (170,155) → 5. Hai bạn cùng cao 150 không tính. Đếm bằng merge sort như bài 24, nhớ dùng long long.
Xem code tham khảo (C++ / Python)
#include <bits/stdc++.h>
using namespace std;

int a[200005], tmp[200005];

long long countInv(int l, int r){
    if(l >= r) return 0;
    int m = (l + r) / 2;
    long long res = countInv(l, m) + countInv(m + 1, r);
    int i = l, j = m + 1, k = l;
    while(i <= m && j <= r){
        if(a[i] <= a[j]) tmp[k++] = a[i++];      // bằng nhau: không phải nghịch thế
        else { res += m - i + 1; tmp[k++] = a[j++]; }
    }
    while(i <= m) tmp[k++] = a[i++];
    while(j <= r) tmp[k++] = a[j++];
    for(int t = l; t <= r; t++) a[t] = tmp[t];
    return res;
}

int main(){
    ios::sync_with_stdio(false); cin.tie(nullptr);
    int n;
    cin >> n;
    for(int i = 0; i < n; i++) cin >> a[i];
    cout << countInv(0, n - 1);
    return 0;
}
import sys

def count_inv(a):
    if len(a) <= 1:
        return a, 0
    m = len(a) // 2
    left, x = count_inv(a[:m])
    right, y = count_inv(a[m:])
    res, i, j, cnt = [], 0, 0, x + y
    while i < len(left) and j < len(right):
        if left[i] <= right[j]:
            res.append(left[i]); i += 1
        else:
            cnt += len(left) - i          # left[i:] đều lớn hơn right[j]
            res.append(right[j]); j += 1
    res.extend(left[i:]); res.extend(right[j:])
    return res, cnt

data = sys.stdin.buffer.read().split()
n = int(data[0])
print(count_inv(list(map(int, data[1:n + 1])))[1])
Bài 2Đoạn ổn định dài nhấtTrung bình

Một cảm biến ghi lại $n$ số đo nhiệt độ $a_1, a_2, \ldots, a_n$. Một đoạn liên tiếp gọi là ổn định nếu chênh lệch giữa số lớn nhất và số nhỏ nhất trong đoạn không vượt quá $K$. Tìm độ dài đoạn ổn định dài nhất.

Giới hạn: $1 \le n \le 5 \cdot 10^5$, $0 \le K, a_i \le 10^9$. Subtask 1 (30%): $n \le 300$. Subtask 2 (30%): $n \le 5000$. Subtask 3 (40%): không có ràng buộc thêm.

Input: dòng đầu $n$ và $K$, dòng sau $n$ số $a_i$. Output: một số nguyên.

Input
8 2
5 3 4 6 2 3 1 3
Output
4

📝 Nộp bài, chấm tự động trên ClueOJ ↗

💡 Gợi ý
Nếu đoạn $[l, r]$ ổn định thì mọi đoạn con của nó cũng ổn định → dùng hai con trỏ: tăng $r$, khi đoạn hỏng thì tăng $l$. Cần biết max và min của cửa sổ đang trượt: dùng hai hàng đợi đơn điệu (bài 25), một giữ chỉ số có giá trị giảm dần (đầu là max), một tăng dần (đầu là min). Ví dụ: đoạn 2 3 1 3 có max − min = 2 ≤ 2, dài 4.
Xem code tham khảo (C++ / Python)
#include <bits/stdc++.h>
using namespace std;

int main(){
    ios::sync_with_stdio(false); cin.tie(nullptr);
    int n; long long K;
    cin >> n >> K;
    vector<long long> a(n);
    for(auto &x : a) cin >> x;
    deque<int> mx, mn;               // chỉ số; a[mx] giảm dần, a[mn] tăng dần
    int l = 0, best = 0;
    for(int r = 0; r < n; r++){
        while(!mx.empty() && a[mx.back()] <= a[r]) mx.pop_back();
        mx.push_back(r);
        while(!mn.empty() && a[mn.back()] >= a[r]) mn.pop_back();
        mn.push_back(r);
        while(a[mx.front()] - a[mn.front()] > K){   // cửa sổ hỏng: thu hẹp bên trái
            l++;
            if(mx.front() < l) mx.pop_front();
            if(mn.front() < l) mn.pop_front();
        }
        best = max(best, r - l + 1);
    }
    cout << best;
    return 0;
}
import sys
from collections import deque

def main():
    data = sys.stdin.buffer.read().split()
    n, K = int(data[0]), int(data[1])
    a = list(map(int, data[2:2 + n]))
    mx, mn = deque(), deque()        # chỉ số; a[mx] giảm dần, a[mn] tăng dần
    l = best = 0
    for r in range(n):
        x = a[r]
        while mx and a[mx[-1]] <= x: mx.pop()
        mx.append(r)
        while mn and a[mn[-1]] >= x: mn.pop()
        mn.append(r)
        while a[mx[0]] - a[mn[0]] > K:   # cửa sổ hỏng: thu hẹp bên trái
            l += 1
            if mx[0] < l: mx.popleft()
            if mn[0] < l: mn.popleft()
        if r - l + 1 > best: best = r - l + 1
    print(best)

main()
Bài 3Đường về tránh ô sửaTrung bình

Robot đứng ở ô $(0, 0)$ và muốn tới ô $(n, m)$. Mỗi bước nó chỉ đi sang phải $(x+1, y)$ hoặc lên trên $(x, y+1)$. Ô $(a, b)$ đang sửa nên robot không được đi qua. Đếm số đường đi, in theo modulo $10^9 + 7$. Có $q$ câu hỏi độc lập.

Giới hạn: $1 \le q \le 10^5$, $0 \le n, m \le 5 \cdot 10^5$, $0 \le a \le n$, $0 \le b \le m$. Subtask 1 (30%): $n, m \le 10$. Subtask 2 (30%): $q = 1$, $n, m \le 1000$. Subtask 3 (40%): không có ràng buộc thêm.

Input: dòng đầu $q$, mỗi dòng sau gồm $n, m, a, b$. Output: $q$ dòng, mỗi dòng một đáp án.

Input
3
2 2 1 1
3 3 0 0
2 3 2 0
Output
2
0
9

📝 Nộp bài, chấm tự động trên ClueOJ ↗

💡 Gợi ý
Đường đi từ $(0,0)$ tới $(n,m)$ gồm $n$ bước phải và $m$ bước lên, nên có $C(n+m, n)$ đường. Trừ đi số đường đi qua ô $(a,b)$: $C(a+b, a) \cdot C(n-a+m-b, n-a)$. Tính $C$ trong $O(1)$ nhờ tiền xử lý giai thừa và nghịch đảo giai thừa (bài 26). Câu hỏi thứ hai: ô sửa là chính ô xuất phát nên đáp án 0. Câu thứ ba: $10 - 1 = 9$.
Xem code tham khảo (C++ / Python)
#include <bits/stdc++.h>
using namespace std;

const long long MOD = 1000000007;
const int N = 1000005;
long long f[N], inv[N];

long long power(long long a, long long b){
    long long r = 1; a %= MOD;
    while(b){ if(b & 1) r = r * a % MOD; a = a * a % MOD; b >>= 1; }
    return r;
}
long long C(int n, int k){
    if(k < 0 || k > n) return 0;
    return f[n] * inv[k] % MOD * inv[n - k] % MOD;
}

int main(){
    ios::sync_with_stdio(false); cin.tie(nullptr);
    f[0] = 1;
    for(int i = 1; i < N; i++) f[i] = f[i - 1] * i % MOD;
    inv[N - 1] = power(f[N - 1], MOD - 2);              // Fermat
    for(int i = N - 1; i > 0; i--) inv[i - 1] = inv[i] * i % MOD;
    int q;
    cin >> q;
    while(q--){
        int n, m, a, b;
        cin >> n >> m >> a >> b;
        long long all = C(n + m, n);
        long long bad = C(a + b, a) * C(n - a + m - b, n - a) % MOD;   // đi qua ô (a, b)
        cout << ((all - bad) % MOD + MOD) % MOD << "\n";
    }
    return 0;
}
import sys

def main():
    MOD = 10**9 + 7
    data = sys.stdin.buffer.read().split()
    q = int(data[0])
    Q = [tuple(map(int, data[1 + 4 * i: 5 + 4 * i])) for i in range(q)]
    N = max(n + m for n, m, a, b in Q) + 1
    f = [1] * (N + 1)
    for i in range(1, N + 1):
        f[i] = f[i - 1] * i % MOD
    inv = [1] * (N + 1)
    inv[N] = pow(f[N], MOD - 2, MOD)                # Fermat
    for i in range(N, 0, -1):
        inv[i - 1] = inv[i] * i % MOD

    def C(n, k):
        return f[n] * inv[k] % MOD * inv[n - k] % MOD

    out = []
    for n, m, a, b in Q:
        bad = C(a + b, a) * C(n - a + m - b, n - a)   # đi qua ô (a, b)
        out.append((C(n + m, n) - bad) % MOD)
    print('\n'.join(map(str, out)))

main()
Bài 4Phiếu giảm giá taxiKhá

Thành phố có $n$ địa điểm và $m$ con đường hai chiều, đi taxi trên đường thứ $i$ tốn $w_i$ đồng. Em có một phiếu giảm giá, dùng cho tối đa một con đường: đường đó chỉ tốn $\lfloor w_i / 2 \rfloor$. Tìm chi phí nhỏ nhất để đi từ địa điểm $1$ tới địa điểm $n$, in $-1$ nếu không thể đi tới.

Giới hạn: $2 \le n \le 10^5$, $1 \le m \le 2 \cdot 10^5$, $1 \le w_i \le 10^9$. Subtask 1 (30%): $n \le 100$, $m \le 200$. Subtask 2 (70%): không có ràng buộc thêm.

Input
4 4
1 2 10
2 4 10
1 3 3
3 4 18
Output
12

📝 Nộp bài, chấm tự động trên ClueOJ ↗

💡 Gợi ý
Mở rộng mỗi đỉnh thành hai trạng thái: $(v, 0)$ chưa dùng phiếu, $(v, 1)$ đã dùng. Từ $(u, 0)$ đi cạnh $w$ sang $(v, 0)$ giá $w$ hoặc sang $(v, 1)$ giá $\lfloor w/2 \rfloor$; từ $(u, 1)$ chỉ sang $(v, 1)$ giá $w$. Chạy Dijkstra (bài 29) trên $2n$ trạng thái. Ví dụ: đi 1 → 3 → 4, dùng phiếu cho đường 18: $3 + 9 = 12$.
Xem code tham khảo (C++ / Python)
#include <bits/stdc++.h>
using namespace std;

typedef long long ll;
const ll INF = LLONG_MAX / 4;

int main(){
    ios::sync_with_stdio(false); cin.tie(nullptr);
    int n, m;
    cin >> n >> m;
    vector<vector<pair<int, ll>>> g(n + 1);
    for(int i = 0; i < m; i++){
        int u, v; ll w;
        cin >> u >> v >> w;
        g[u].push_back({v, w});
        g[v].push_back({u, w});
    }
    // d[v][t]: chi phí nhỏ nhất tới v, t = 1 nếu đã dùng phiếu
    vector<array<ll, 2>> d(n + 1, {INF, INF});
    priority_queue<tuple<ll, int, int>, vector<tuple<ll, int, int>>, greater<>> pq;
    d[1][0] = 0; pq.push({0, 1, 0});
    while(!pq.empty()){
        auto [du, u, t] = pq.top(); pq.pop();
        if(du != d[u][t]) continue;                    // bản ghi cũ
        for(auto [v, w] : g[u]){
            if(du + w < d[v][t]){ d[v][t] = du + w; pq.push({d[v][t], v, t}); }
            if(t == 0 && du + w / 2 < d[v][1]){        // dùng phiếu cho cạnh này
                d[v][1] = du + w / 2; pq.push({d[v][1], v, 1});
            }
        }
    }
    ll ans = min(d[n][0], d[n][1]);
    cout << (ans >= INF ? -1 : ans);
    return 0;
}
import sys, heapq

def main():
    data = sys.stdin.buffer.read().split()
    n, m = int(data[0]), int(data[1])
    g = [[] for _ in range(n + 1)]
    for i in range(m):
        u, v, w = int(data[2 + 3 * i]), int(data[3 + 3 * i]), int(data[4 + 3 * i])
        g[u].append((v, w)); g[v].append((u, w))
    INF = float('inf')
    d0 = [INF] * (n + 1)                         # chưa dùng phiếu
    d1 = [INF] * (n + 1)                         # đã dùng phiếu
    d0[1] = 0
    pq = [(0, 1, 0)]
    pop, push = heapq.heappop, heapq.heappush
    while pq:
        du, u, t = pop(pq)
        if t == 0:
            if du != d0[u]:
                continue                         # bản ghi cũ
            for v, w in g[u]:
                nd = du + w
                if nd < d0[v]:
                    d0[v] = nd; push(pq, (nd, v, 0))
                nd = du + w // 2                 # dùng phiếu cho cạnh này
                if nd < d1[v]:
                    d1[v] = nd; push(pq, (nd, v, 1))
        else:
            if du != d1[u]:
                continue
            for v, w in g[u]:
                nd = du + w
                if nd < d1[v]:
                    d1[v] = nd; push(pq, (nd, v, 1))
    ans = min(d0[n], d1[n])
    print(-1 if ans == INF else ans)

main()
Bài 5Kéo điện về làngKhá

Huyện có $n$ làng, trong đó $p$ làng đã có trạm điện. Có $m$ phương án kéo dây: dây thứ $i$ nối hai làng $u_i, v_i$ với chi phí $c_i$. Một làng có điện nếu nó nối (trực tiếp hoặc qua các làng khác) tới ít nhất một trạm. Tìm tổng chi phí nhỏ nhất để mọi làng có điện, in $-1$ nếu không thể.

Giới hạn: $1 \le p \le n \le 10^5$, $0 \le m \le 2 \cdot 10^5$, $0 \le c_i \le 10^9$. Subtask 1 (30%): $n \le 8$, $m \le 12$. Subtask 2 (30%): $p = 1$. Subtask 3 (40%): không có ràng buộc thêm.

Input: dòng đầu $n, m, p$; dòng hai gồm $p$ làng có trạm (khác nhau); $m$ dòng sau mỗi dòng $u_i, v_i, c_i$.

Input
5 5 2
1 4
1 2 3
2 3 1
3 4 4
4 5 2
2 5 6
Output
6

📝 Nộp bài, chấm tự động trên ClueOJ ↗

💡 Gợi ý
Khi $p = 1$ đây đúng là cây khung nhỏ nhất. Khi có nhiều trạm: thêm một đỉnh ảo $0$ (“nhà máy tổng”) nối tới mọi làng có trạm với chi phí 0. Mọi làng có điện ⇔ mọi đỉnh liên thông với đỉnh $0$ ⇔ cây khung của đồ thị $n+1$ đỉnh. Chạy Kruskal + DSU (bài 30). Ví dụ: chọn dây 2–3 (1), 4–5 (2), 1–2 (3), tổng 6.
Xem code tham khảo (C++ / Python)
#include <bits/stdc++.h>
using namespace std;

int par[100005];
int findRoot(int x){ return par[x] == x ? x : par[x] = findRoot(par[x]); }

int main(){
    ios::sync_with_stdio(false); cin.tie(nullptr);
    int n, m, p;
    cin >> n >> m >> p;
    vector<array<long long, 3>> E;             // {chi phí, u, v}
    for(int i = 0; i < p; i++){ int x; cin >> x; E.push_back({0, 0, x}); }   // cạnh ảo 0 – nhà máy
    for(int i = 0; i < m; i++){ long long u, v, c; cin >> u >> v >> c; E.push_back({c, u, v}); }
    sort(E.begin(), E.end());
    for(int i = 0; i <= n; i++) par[i] = i;
    long long cost = 0; int used = 0;
    for(auto &e : E){
        int a = findRoot(e[1]), b = findRoot(e[2]);
        if(a == b) continue;                   // tạo chu trình
        par[a] = b; cost += e[0]; used++;
        if(used == n) break;                   // n + 1 đỉnh cần n cạnh
    }
    cout << (used == n ? cost : -1);
    return 0;
}
import sys

def main():
    data = sys.stdin.buffer.read().split()
    n, m, p = int(data[0]), int(data[1]), int(data[2])
    E = [(0, 0, int(x)) for x in data[3:3 + p]]           # cạnh ảo 0 – nhà máy
    pos = 3 + p
    for i in range(m):
        u, v, c = int(data[pos]), int(data[pos + 1]), int(data[pos + 2]); pos += 3
        E.append((c, u, v))
    E.sort()
    par = list(range(n + 1))

    def find(x):
        while par[x] != x:
            par[x] = par[par[x]]                          # nén đường đi (chia đôi)
            x = par[x]
        return x
    cost = used = 0
    for c, u, v in E:
        a, b = find(u), find(v)
        if a == b:
            continue                                      # tạo chu trình
        par[a] = b; cost += c; used += 1
        if used == n:
            break
    print(cost if used == n else -1)

main()
Bài 6Bộ ba tăng dầnKhó

Cho dãy $n$ số nguyên dương $a_1, \ldots, a_n$. Đếm số bộ ba chỉ số $i < j < k$ sao cho $a_i < a_j < a_k$.

Giới hạn: $1 \le n \le 2 \cdot 10^5$, $1 \le a_i \le 10^9$. Subtask 1 (20%): $n \le 200$. Subtask 2 (30%): $n \le 5000$. Subtask 3 (50%): không có ràng buộc thêm.

Input
5
1 3 2 4 5
Output
7

📝 Nộp bài, chấm tự động trên ClueOJ ↗

💡 Gợi ý
Cố định phần tử giữa $j$: số bộ ba = (số phần tử bên trái nhỏ hơn $a_j$) × (số phần tử bên phải lớn hơn $a_j$). Hai đại lượng này tính bằng Fenwick (bài 31) trên giá trị đã nén: quét trái → phải để có $L_j$, quét phải → trái để có $R_j$. Ví dụ: với $a_j = 3$ có $L = 1$, $R = 2$ → 2 bộ; tổng cộng 7. Đáp án tới $\approx 1{,}3 \cdot 10^{15}$, dùng long long.
Xem code tham khảo (C++ / Python)
#include <bits/stdc++.h>
using namespace std;

int n, bit[200005];
void add(int i){ for(; i <= n; i += i & -i) bit[i]++; }
int get(int i){ int s = 0; for(; i > 0; i -= i & -i) s += bit[i]; return s; }

int main(){
    ios::sync_with_stdio(false); cin.tie(nullptr);
    cin >> n;
    vector<int> a(n), v;
    for(auto &x : a) cin >> x;
    v = a; sort(v.begin(), v.end()); v.erase(unique(v.begin(), v.end()), v.end());
    for(auto &x : a) x = lower_bound(v.begin(), v.end(), x) - v.begin() + 1;   // nén về 1..k
    vector<long long> L(n), R(n);
    for(int j = 0; j < n; j++){ L[j] = get(a[j] - 1); add(a[j]); }          // bên trái, nhỏ hơn
    fill(bit, bit + n + 1, 0);
    for(int j = n - 1; j >= 0; j--){ R[j] = (n - 1 - j) - get(a[j]); add(a[j]); }  // bên phải, lớn hơn
    long long ans = 0;
    for(int j = 0; j < n; j++) ans += L[j] * R[j];
    cout << ans;
    return 0;
}
import sys

def main():
    data = sys.stdin.buffer.read().split()
    n = int(data[0])
    a = list(map(int, data[1:n + 1]))
    rank = {x: i + 1 for i, x in enumerate(sorted(set(a)))}
    a = [rank[x] for x in a]                     # nén về 1..k
    k = len(rank)
    bit = [0] * (k + 1)
    L = [0] * n
    for j in range(n):                           # bên trái, nhỏ hơn
        i, s = a[j] - 1, 0
        while i > 0: s += bit[i]; i -= i & -i
        L[j] = s
        i = a[j]
        while i <= k: bit[i] += 1; i += i & -i
    bit = [0] * (k + 1)
    ans = 0
    for j in range(n - 1, -1, -1):               # bên phải, lớn hơn
        i, s = a[j], 0
        while i > 0: s += bit[i]; i -= i & -i
        ans += L[j] * ((n - 1 - j) - s)
        i = a[j]
        while i <= k: bit[i] += 1; i += i & -i
    print(ans)

main()

Phần 4Lời giải chi tiết từng câu

Với mỗi câu, mình đi theo đúng thứ tự em nên nghĩ trong phòng thi: nhận dạng → vét cạn lấy subtask → tối ưu. Code vét cạn và code đầy đủ đều đã được đối chiếu với nhau trên hàng trăm test ngẫu nhiên.

Câu 1. Xếp hàng chào cờ (nghịch thế, bài 24)

Nhận dạng. “Đổi chỗ hai phần tử kề nhau, ít nhất bao nhiêu lần để sắp xếp” là câu hỏi kinh điển, đáp án là số cặp nghịch thế. Vì sao? Đổi chỗ hai bạn kề nhau $h_i > h_{i+1}$ chỉ làm thay đổi thứ tự của đúng cặp đó, nên số nghịch thế giảm đúng 1. Không có cách nào giảm nhiều hơn 1 mỗi lần, và hàng còn nghịch thế thì luôn có cặp kề nhau bị ngược. Đây cũng chính là số lần đổi chỗ của thuật toán nổi bọt.

Subtask 1 ($n \le 1000$): hai vòng for đếm cặp, $O(n^2) = 10^6$.

Xem code C++ / Python: câu 1 vét cạn $O(n^2)$ (bấm để mở)
#include <bits/stdc++.h>
using namespace std;

int main(){
    int n;
    cin >> n;
    vector<int> a(n);
    for(auto &x : a) cin >> x;
    long long cnt = 0;
    for(int i = 0; i < n; i++)
        for(int j = i + 1; j < n; j++)
            if(a[i] > a[j]) cnt++;          // mỗi cặp nghịch thế cần đúng 1 lần đổi chỗ
    cout << cnt;
    return 0;
}
n = int(input())
a = list(map(int, input().split()))
print(sum(1 for i in range(n) for j in range(i + 1, n) if a[i] > a[j]))

Subtask 2 ($n = 2 \cdot 10^5$): $O(n^2)$ là $2 \cdot 10^{10}$, quá chậm. Dùng merge sort đếm nghịch thế (bài 24): khi trộn, lấy $a_j$ từ nửa phải thì cộng $m - i + 1$. Lưu ý chiều cao bằng nhau không cần đổi, nên khi $a_i = a_j$ phải lấy bên trái trước (dấu <=).

Xem code C++ / Python: câu 1 đầy đủ, merge sort $O(n \log n)$ (bấm để mở)
#include <bits/stdc++.h>
using namespace std;

int a[200005], tmp[200005];

long long countInv(int l, int r){
    if(l >= r) return 0;
    int m = (l + r) / 2;
    long long res = countInv(l, m) + countInv(m + 1, r);
    int i = l, j = m + 1, k = l;
    while(i <= m && j <= r){
        if(a[i] <= a[j]) tmp[k++] = a[i++];      // bằng nhau: không phải nghịch thế
        else { res += m - i + 1; tmp[k++] = a[j++]; }
    }
    while(i <= m) tmp[k++] = a[i++];
    while(j <= r) tmp[k++] = a[j++];
    for(int t = l; t <= r; t++) a[t] = tmp[t];
    return res;
}

int main(){
    ios::sync_with_stdio(false); cin.tie(nullptr);
    int n;
    cin >> n;
    for(int i = 0; i < n; i++) cin >> a[i];
    cout << countInv(0, n - 1);
    return 0;
}
import sys

def count_inv(a):
    if len(a) <= 1:
        return a, 0
    m = len(a) // 2
    left, x = count_inv(a[:m])
    right, y = count_inv(a[m:])
    res, i, j, cnt = [], 0, 0, x + y
    while i < len(left) and j < len(right):
        if left[i] <= right[j]:
            res.append(left[i]); i += 1
        else:
            cnt += len(left) - i          # left[i:] đều lớn hơn right[j]
            res.append(right[j]); j += 1
    res.extend(left[i:]); res.extend(right[j:])
    return res, cnt

data = sys.stdin.buffer.read().split()
n = int(data[0])
print(count_inv(list(map(int, data[1:n + 1])))[1])

Độ phức tạp: $O(n \log n)$ thời gian, $O(n)$ bộ nhớ. Cách khác: nén giá trị rồi dùng Fenwick đếm “số đã gặp lớn hơn $h_j$”, cũng $O(n \log n)$.

Câu 2. Đoạn ổn định dài nhất (hàng đợi đơn điệu, bài 25)

Nhận dạng. “Đoạn liên tiếp dài nhất thoả điều kiện” + điều kiện có tính đơn điệu (đoạn tốt thì đoạn con cũng tốt) → hai con trỏ (bài 17). Điều kiện cần max và min của cửa sổ đang trượt → hàng đợi đơn điệu.

Subtask 1 ($n \le 300$): thử mọi đoạn, tính max − min bằng vòng lặp, $O(n^3)$. Subtask 2 ($n \le 5000$): cố định $l$, mở rộng $r$ và cập nhật min/max dần, đoạn hỏng thì dừng ngay vì kéo dài thêm vẫn hỏng: $O(n^2)$.

Xem code C++ / Python: câu 2 vét cạn $O(n^2)$ (bấm để mở)
#include <bits/stdc++.h>
using namespace std;

int main(){
    int n; long long K;
    cin >> n >> K;
    vector<long long> a(n);
    for(auto &x : a) cin >> x;
    int best = 0;
    for(int l = 0; l < n; l++){
        long long lo = a[l], hi = a[l];
        for(int r = l; r < n; r++){              // mở rộng dần, cập nhật min/max
            lo = min(lo, a[r]); hi = max(hi, a[r]);
            if(hi - lo > K) break;               // đã hỏng thì kéo dài thêm cũng hỏng
            best = max(best, r - l + 1);
        }
    }
    cout << best;
    return 0;
}
n, K = map(int, input().split())
a = list(map(int, input().split()))
best = 0
for l in range(n):
    lo = hi = a[l]
    for r in range(l, n):              # mở rộng dần, cập nhật min/max
        lo = min(lo, a[r]); hi = max(hi, a[r])
        if hi - lo > K:                # đã hỏng thì kéo dài thêm cũng hỏng
            break
        best = max(best, r - l + 1)
print(best)

Subtask 3. Với mỗi $r$, ta muốn $l$ nhỏ nhất để $[l, r]$ ổn định. Khi $r$ tăng, $l$ không bao giờ giảm (vì đoạn $[l, r+1]$ hỏng thì $[l-1, r+1]$ càng hỏng). Vậy chỉ cần hai con trỏ, mỗi con trỏ đi tối đa $n$ bước. Max của cửa sổ lấy từ deque mx: khi thêm $a_r$, bỏ khỏi cuối mọi chỉ số có giá trị $\le a_r$ (chúng đứng trước và không lớn hơn $a_r$ nên không bao giờ là max nữa). Tương tự mn cho min. Khi $l$ tăng vượt chỉ số ở đầu deque thì bỏ đầu.

Xem code C++ / Python: câu 2 đầy đủ, hai deque $O(n)$ (bấm để mở)
#include <bits/stdc++.h>
using namespace std;

int main(){
    ios::sync_with_stdio(false); cin.tie(nullptr);
    int n; long long K;
    cin >> n >> K;
    vector<long long> a(n);
    for(auto &x : a) cin >> x;
    deque<int> mx, mn;               // chỉ số; a[mx] giảm dần, a[mn] tăng dần
    int l = 0, best = 0;
    for(int r = 0; r < n; r++){
        while(!mx.empty() && a[mx.back()] <= a[r]) mx.pop_back();
        mx.push_back(r);
        while(!mn.empty() && a[mn.back()] >= a[r]) mn.pop_back();
        mn.push_back(r);
        while(a[mx.front()] - a[mn.front()] > K){   // cửa sổ hỏng: thu hẹp bên trái
            l++;
            if(mx.front() < l) mx.pop_front();
            if(mn.front() < l) mn.pop_front();
        }
        best = max(best, r - l + 1);
    }
    cout << best;
    return 0;
}
import sys
from collections import deque

def main():
    data = sys.stdin.buffer.read().split()
    n, K = int(data[0]), int(data[1])
    a = list(map(int, data[2:2 + n]))
    mx, mn = deque(), deque()        # chỉ số; a[mx] giảm dần, a[mn] tăng dần
    l = best = 0
    for r in range(n):
        x = a[r]
        while mx and a[mx[-1]] <= x: mx.pop()
        mx.append(r)
        while mn and a[mn[-1]] >= x: mn.pop()
        mn.append(r)
        while a[mx[0]] - a[mn[0]] > K:   # cửa sổ hỏng: thu hẹp bên trái
            l += 1
            if mx[0] < l: mx.popleft()
            if mn[0] < l: mn.popleft()
        if r - l + 1 > best: best = r - l + 1
    print(best)

main()

Em chạy tay từng dòng dưới đây để thấy hai deque co giãn thế nào. Ô đỏ là vị trí đang giữ max (đầu mx), ô xanh lá là vị trí đang giữ min (đầu mn):

C++▶ Chạy tay từng dòng codeHai con trỏ l, r và hai deque: mx giữ chỉ số có giá trị giảm dần, mn giữ chỉ số có giá trị tăng dần. Cửa sổ hỏng thì tăng l.
Dữ liệu mẫu:
1#include <bits/stdc++.h>
2using namespace std;
3
4int main(){
5 int n; long long K;
6 cin >> n >> K;
7 vector<long long> a(n);
8 for(int i = 0; i < n; i++) cin >> a[i];
9 deque<int> mx, mn;
10 int l = 0, best = 0;
11 for(int r = 0; r < n; r++){
12 while(!mx.empty() && a[mx.back()] <= a[r]) mx.pop_back();
13 mx.push_back(r);
14 while(!mn.empty() && a[mn.back()] >= a[r]) mn.pop_back();
15 mn.push_back(r);
16 while(a[mx.front()] - a[mn.front()] > K){
17 l++;
18 if(mx.front() < l) mx.pop_front();
19 if(mn.front() < l) mn.pop_front();
20 }
21 best = max(best, r - l + 1);
22 }
23 cout << best;
24 return 0;
25}

Độ phức tạp: mỗi chỉ số vào và ra mỗi deque nhiều nhất một lần, $l$ chỉ tăng, nên tổng $O(n)$.

Lỗi hay gặp ở câu 2
Lưu chỉ số trong deque chứ không lưu giá trị, nếu không em không biết phần tử ở đầu đã rời cửa sổ hay chưa. Và vòng thu hẹp phải là while, không phải if: có khi phải tăng $l$ nhiều bước mới hết hỏng.

Câu 3. Đường về tránh ô sửa (tổ hợp modulo, bài 26)

Nhận dạng. “Đếm số đường đi”, “modulo $10^9 + 7$”, $q$ tới $10^5$ câu hỏi → không thể QHĐ cho từng câu, phải có công thức và trả lời $O(1)$.

Subtask 1 và 2: QHĐ trên lưới như bài 19: $f[x][y] = f[x-1][y] + f[x][y-1]$, ô sửa gán $f = 0$. Mỗi câu tốn $O(nm)$, đủ cho $n, m \le 1000$ và $q = 1$.

Xem code C++ / Python: câu 3 vét cạn bằng QHĐ trên lưới (bấm để mở)
#include <bits/stdc++.h>
using namespace std;

const long long MOD = 1000000007;

int main(){
    int q;
    cin >> q;
    while(q--){
        int n, m, a, b;
        cin >> n >> m >> a >> b;
        vector<vector<long long>> f(n + 1, vector<long long>(m + 1, 0));
        for(int x = 0; x <= n; x++)
            for(int y = 0; y <= m; y++){
                if(x == a && y == b) { f[x][y] = 0; continue; }   // ô đang sửa
                if(x == 0 && y == 0) { f[x][y] = 1; continue; }
                if(x > 0) f[x][y] += f[x - 1][y];
                if(y > 0) f[x][y] += f[x][y - 1];
                f[x][y] %= MOD;
            }
        cout << f[n][m] << "\n";
    }
    return 0;
}
MOD = 10**9 + 7
q = int(input())
for _ in range(q):
    n, m, a, b = map(int, input().split())
    f = [[0] * (m + 1) for _ in range(n + 1)]
    for x in range(n + 1):
        for y in range(m + 1):
            if x == a and y == b:          # ô đang sửa
                continue
            if x == 0 and y == 0:
                f[x][y] = 1
            else:
                f[x][y] = ((f[x - 1][y] if x else 0) + (f[x][y - 1] if y else 0)) % MOD
    print(f[n][m])

Subtask 3. Một đường đi bất kỳ gồm đúng $n$ bước “phải” và $m$ bước “lên” xếp theo thứ tự nào đó, nên có $C(n+m, n)$ đường. Đường đi qua ô $(a, b)$ = (đường từ $(0,0)$ tới $(a,b)$) × (đường từ $(a,b)$ tới $(n,m)$) = $C(a+b, a) \cdot C(n-a+m-b, n-a)$. Đáp án là hiệu hai số này, lấy modulo (nhớ cộng thêm $p$ trước khi lấy dư vì hiệu có thể âm).

Để tính $C(n, k) = \frac{n!}{k!\,(n-k)!}$ dưới modulo, ta tiền xử lý $f_i = i! \bmod p$ và $\mathit{inv}_i = (i!)^{-1} \bmod p$. Chỉ cần một lần lũy thừa nhanh: $\mathit{inv}_N = f_N^{\,p-2}$, rồi lùi xuống $\mathit{inv}_{i-1} = \mathit{inv}_i \cdot i$.

Câu hỏiTất cả $C(n+m, n)$Qua ô sửaĐáp án
$n=2, m=2$, ô $(1,1)$$C(4,2) = 6$$C(2,1) \cdot C(2,1) = 4$2
$n=3, m=3$, ô $(0,0)$$C(6,3) = 20$$C(0,0) \cdot C(6,3) = 20$0
$n=2, m=3$, ô $(2,0)$$C(5,2) = 10$$C(2,2) \cdot C(3,0) = 1$9

Bảng 5. Tính tay ví dụ của câu 3.

Xem code C++ / Python: câu 3 đầy đủ, giai thừa và nghịch đảo giai thừa (bấm để mở)
#include <bits/stdc++.h>
using namespace std;

const long long MOD = 1000000007;
const int N = 1000005;
long long f[N], inv[N];

long long power(long long a, long long b){
    long long r = 1; a %= MOD;
    while(b){ if(b & 1) r = r * a % MOD; a = a * a % MOD; b >>= 1; }
    return r;
}
long long C(int n, int k){
    if(k < 0 || k > n) return 0;
    return f[n] * inv[k] % MOD * inv[n - k] % MOD;
}

int main(){
    ios::sync_with_stdio(false); cin.tie(nullptr);
    f[0] = 1;
    for(int i = 1; i < N; i++) f[i] = f[i - 1] * i % MOD;
    inv[N - 1] = power(f[N - 1], MOD - 2);              // Fermat
    for(int i = N - 1; i > 0; i--) inv[i - 1] = inv[i] * i % MOD;
    int q;
    cin >> q;
    while(q--){
        int n, m, a, b;
        cin >> n >> m >> a >> b;
        long long all = C(n + m, n);
        long long bad = C(a + b, a) * C(n - a + m - b, n - a) % MOD;   // đi qua ô (a, b)
        cout << ((all - bad) % MOD + MOD) % MOD << "\n";
    }
    return 0;
}
import sys

def main():
    MOD = 10**9 + 7
    data = sys.stdin.buffer.read().split()
    q = int(data[0])
    Q = [tuple(map(int, data[1 + 4 * i: 5 + 4 * i])) for i in range(q)]
    N = max(n + m for n, m, a, b in Q) + 1
    f = [1] * (N + 1)
    for i in range(1, N + 1):
        f[i] = f[i - 1] * i % MOD
    inv = [1] * (N + 1)
    inv[N] = pow(f[N], MOD - 2, MOD)                # Fermat
    for i in range(N, 0, -1):
        inv[i - 1] = inv[i] * i % MOD

    def C(n, k):
        return f[n] * inv[k] % MOD * inv[n - k] % MOD

    out = []
    for n, m, a, b in Q:
        bad = C(a + b, a) * C(n - a + m - b, n - a)   # đi qua ô (a, b)
        out.append((C(n + m, n) - bad) % MOD)
    print('\n'.join(map(str, out)))

main()

Độ phức tạp: $O(N + \log p)$ tiền xử lý với $N = n + m \le 10^6$, mỗi câu hỏi $O(1)$.

Câu 4. Phiếu giảm giá taxi (Dijkstra trên đồ thị trạng thái, bài 29)

Nhận dạng. Đường đi chi phí nhỏ nhất, trọng số dương, $n$ lớn → Dijkstra. Nhưng có thêm “quyền đặc biệt dùng một lần”, nên một đỉnh thôi chưa đủ mô tả tình trạng: đứng ở $v$ khi còn phiếu khác hẳn đứng ở $v$ khi hết phiếu.

Subtask 1 ($m \le 200$): thử dùng phiếu cho từng cạnh (và trường hợp không dùng), mỗi lần chạy Dijkstra một lần: $O(m \cdot m \log n)$.

Xem code C++ / Python: câu 4 vét cạn: thử phiếu cho từng cạnh (bấm để mở)
#include <bits/stdc++.h>
using namespace std;

typedef long long ll;
const ll INF = LLONG_MAX / 4;
int n, m;
vector<array<ll, 3>> E;

ll dijkstra(int half){                 // half = chỉ số cạnh dùng phiếu (-1: không dùng)
    vector<vector<pair<int, ll>>> g(n + 1);
    for(int i = 0; i < m; i++){
        ll w = (i == half) ? E[i][2] / 2 : E[i][2];
        g[E[i][0]].push_back({(int)E[i][1], w});
        g[E[i][1]].push_back({(int)E[i][0], w});
    }
    vector<ll> d(n + 1, INF);
    priority_queue<pair<ll, int>, vector<pair<ll, int>>, greater<>> pq;
    d[1] = 0; pq.push({0, 1});
    while(!pq.empty()){
        auto [du, u] = pq.top(); pq.pop();
        if(du != d[u]) continue;
        for(auto [v, w] : g[u]) if(du + w < d[v]){ d[v] = du + w; pq.push({d[v], v}); }
    }
    return d[n];
}

int main(){
    cin >> n >> m;
    E.resize(m);
    for(auto &e : E) cin >> e[0] >> e[1] >> e[2];
    ll ans = dijkstra(-1);
    for(int i = 0; i < m; i++) ans = min(ans, dijkstra(i));   // thử phiếu cho từng cạnh
    cout << (ans >= INF ? -1 : ans);
    return 0;
}
import heapq

def dijkstra(n, E, half):              # half = chỉ số cạnh dùng phiếu (-1: không dùng)
    g = [[] for _ in range(n + 1)]
    for i, (u, v, w) in enumerate(E):
        c = w // 2 if i == half else w
        g[u].append((v, c)); g[v].append((u, c))
    d = [float('inf')] * (n + 1); d[1] = 0
    pq = [(0, 1)]
    while pq:
        du, u = heapq.heappop(pq)
        if du != d[u]:
            continue
        for v, w in g[u]:
            if du + w < d[v]:
                d[v] = du + w; heapq.heappush(pq, (d[v], v))
    return d[n]

n, m = map(int, input().split())
E = [tuple(map(int, input().split())) for _ in range(m)]
ans = min(dijkstra(n, E, h) for h in range(-1, m))   # thử phiếu cho từng cạnh
print(-1 if ans == float('inf') else ans)

Subtask 2. Xây đồ thị trạng thái $2n$ đỉnh, $(v, t)$ với $t \in \{0, 1\}$ là đã dùng phiếu chưa. Mỗi cạnh $u - v$ trọng số $w$ sinh ra các cung:

  • $(u, 0) \to (v, 0)$ giá $w$ và $(u, 1) \to (v, 1)$ giá $w$: đi bình thường;
  • $(u, 0) \to (v, 1)$ giá $\lfloor w/2 \rfloor$: dùng phiếu cho cạnh này.

Không cần xây đồ thị mới, chỉ cần mảng $d[v][t]$ và sinh cung ngay lúc duyệt. Đáp án là $\min(d[n][0], d[n][1])$.

Đường điKhông dùng phiếuPhiếu cho cạnh đắt nhấtTốt nhất
1 → 2 → 410 + 10 = 2010 + 5 = 1515
1 → 3 → 43 + 18 = 213 + 9 = 1212

Bảng 6. Ví dụ câu 4: đường 1 → 3 → 4 đắt hơn khi không có phiếu, nhưng rẻ nhất khi dùng phiếu.

Xem code C++ / Python: câu 4 đầy đủ, Dijkstra trên $2n$ trạng thái (bấm để mở)
#include <bits/stdc++.h>
using namespace std;

typedef long long ll;
const ll INF = LLONG_MAX / 4;

int main(){
    ios::sync_with_stdio(false); cin.tie(nullptr);
    int n, m;
    cin >> n >> m;
    vector<vector<pair<int, ll>>> g(n + 1);
    for(int i = 0; i < m; i++){
        int u, v; ll w;
        cin >> u >> v >> w;
        g[u].push_back({v, w});
        g[v].push_back({u, w});
    }
    // d[v][t]: chi phí nhỏ nhất tới v, t = 1 nếu đã dùng phiếu
    vector<array<ll, 2>> d(n + 1, {INF, INF});
    priority_queue<tuple<ll, int, int>, vector<tuple<ll, int, int>>, greater<>> pq;
    d[1][0] = 0; pq.push({0, 1, 0});
    while(!pq.empty()){
        auto [du, u, t] = pq.top(); pq.pop();
        if(du != d[u][t]) continue;                    // bản ghi cũ
        for(auto [v, w] : g[u]){
            if(du + w < d[v][t]){ d[v][t] = du + w; pq.push({d[v][t], v, t}); }
            if(t == 0 && du + w / 2 < d[v][1]){        // dùng phiếu cho cạnh này
                d[v][1] = du + w / 2; pq.push({d[v][1], v, 1});
            }
        }
    }
    ll ans = min(d[n][0], d[n][1]);
    cout << (ans >= INF ? -1 : ans);
    return 0;
}
import sys, heapq

def main():
    data = sys.stdin.buffer.read().split()
    n, m = int(data[0]), int(data[1])
    g = [[] for _ in range(n + 1)]
    for i in range(m):
        u, v, w = int(data[2 + 3 * i]), int(data[3 + 3 * i]), int(data[4 + 3 * i])
        g[u].append((v, w)); g[v].append((u, w))
    INF = float('inf')
    d0 = [INF] * (n + 1)                         # chưa dùng phiếu
    d1 = [INF] * (n + 1)                         # đã dùng phiếu
    d0[1] = 0
    pq = [(0, 1, 0)]
    pop, push = heapq.heappop, heapq.heappush
    while pq:
        du, u, t = pop(pq)
        if t == 0:
            if du != d0[u]:
                continue                         # bản ghi cũ
            for v, w in g[u]:
                nd = du + w
                if nd < d0[v]:
                    d0[v] = nd; push(pq, (nd, v, 0))
                nd = du + w // 2                 # dùng phiếu cho cạnh này
                if nd < d1[v]:
                    d1[v] = nd; push(pq, (nd, v, 1))
        else:
            if du != d1[u]:
                continue
            for v, w in g[u]:
                nd = du + w
                if nd < d1[v]:
                    d1[v] = nd; push(pq, (nd, v, 1))
    ans = min(d0[n], d1[n])
    print(-1 if ans == INF else ans)

main()

Độ phức tạp: $O(m \log n)$ (số trạng thái và số cung chỉ gấp đôi, gấp ba). Chi phí tới $2 \cdot 10^5 \cdot 10^9$, phải long long.

Mẹo “nhân đôi đỉnh” dùng được rất nhiều
Có $k$ phiếu → trạng thái $(v, \text{số phiếu đã dùng})$, $k+1$ tầng. Được “đi miễn phí $k$ cạnh”, “đổi phương tiện”, “mang chìa khoá”… đều là thêm một chiều vào trạng thái rồi chạy Dijkstra hoặc BFS. Cách nghĩ này giống hệt QHĐ: trạng thái phải đủ thông tin để quyết định bước tiếp theo.

Câu 5. Kéo điện về làng (DSU và Kruskal, bài 30)

Nhận dạng. “Nối mọi điểm với tổng chi phí nhỏ nhất” → cây khung nhỏ nhất. Điểm lạ là có nhiều trạm, nên đáp án không còn là một cây mà là một rừng, mỗi cây chứa ít nhất một trạm.

Subtask 1 ($m \le 12$): thử mọi tập con đường dây ($2^{12} = 4096$), dùng DSU kiểm tra mọi làng nối tới trạm. Subtask 2 ($p = 1$): Kruskal bình thường.

Xem code C++ / Python: câu 5 vét cạn: thử mọi tập đường dây (bấm để mở)
#include <bits/stdc++.h>
using namespace std;

int n, m, p, par[20];
int findRoot(int x){ return par[x] == x ? x : par[x] = findRoot(par[x]); }

int main(){
    cin >> n >> m >> p;
    vector<int> P(p);
    for(auto &x : P) cin >> x;
    vector<array<long long, 3>> E(m);
    for(auto &e : E) cin >> e[0] >> e[1] >> e[2];
    long long best = -1;
    for(int mask = 0; mask < (1 << m); mask++){      // thử mọi tập đường dây
        for(int i = 0; i <= n; i++) par[i] = i;
        for(int x : P) par[findRoot(x)] = findRoot(0);
        long long cost = 0;
        for(int k = 0; k < m; k++) if(mask >> k & 1){
            cost += E[k][2];
            par[findRoot(E[k][0])] = findRoot(E[k][1]);
        }
        bool ok = true;
        for(int v = 1; v <= n; v++) if(findRoot(v) != findRoot(0)) ok = false;
        if(ok && (best < 0 || cost < best)) best = cost;
    }
    cout << best;
    return 0;
}
n, m, p = map(int, input().split())
P = list(map(int, input().split()))
E = [tuple(map(int, input().split())) for _ in range(m)]
best = -1
for mask in range(1 << m):                       # thử mọi tập đường dây
    par = list(range(n + 1))
    def find(x):
        while par[x] != x:
            x = par[x]
        return x
    for x in P:
        par[find(x)] = find(0)
    cost = 0
    for k in range(m):
        if mask >> k & 1:
            u, v, c = E[k]; cost += c
            par[find(u)] = find(v)
    if all(find(v) == find(0) for v in range(1, n + 1)) and (best < 0 or cost < best):
        best = cost
print(best)

Subtask 3: mẹo đỉnh ảo. Thêm đỉnh $0$ đại diện cho “lưới điện quốc gia”, nối tới mọi làng có trạm bằng cạnh giá $0$. Một rừng hợp lệ (mỗi cây có trạm) cộng thêm các cạnh ảo chính là một cây khung của đồ thị $n+1$ đỉnh, cùng chi phí; và ngược lại. Vậy chỉ cần Kruskal trên đồ thị mới: sắp các cạnh tăng dần, cạnh nào nối hai nhóm khác nhau thì lấy, đủ $n$ cạnh thì dừng. Em xem mô phỏng ví dụ của đề:

MÔ PHỎNGCâu 5: Kruskal với đỉnh ảo ⚡ nối tới mọi nhà máyNhập số thành phố, các nhà máy và các đường dây “u v c” cách nhau bởi dấu phẩy.
cạnh ảo giá 0cạnh đang xétđược chọn / có điệnbỏ (tạo chu trình)
Xem code C++ / Python: câu 5 đầy đủ, Kruskal với đỉnh ảo (bấm để mở)
#include <bits/stdc++.h>
using namespace std;

int par[100005];
int findRoot(int x){ return par[x] == x ? x : par[x] = findRoot(par[x]); }

int main(){
    ios::sync_with_stdio(false); cin.tie(nullptr);
    int n, m, p;
    cin >> n >> m >> p;
    vector<array<long long, 3>> E;             // {chi phí, u, v}
    for(int i = 0; i < p; i++){ int x; cin >> x; E.push_back({0, 0, x}); }   // cạnh ảo 0 – nhà máy
    for(int i = 0; i < m; i++){ long long u, v, c; cin >> u >> v >> c; E.push_back({c, u, v}); }
    sort(E.begin(), E.end());
    for(int i = 0; i <= n; i++) par[i] = i;
    long long cost = 0; int used = 0;
    for(auto &e : E){
        int a = findRoot(e[1]), b = findRoot(e[2]);
        if(a == b) continue;                   // tạo chu trình
        par[a] = b; cost += e[0]; used++;
        if(used == n) break;                   // n + 1 đỉnh cần n cạnh
    }
    cout << (used == n ? cost : -1);
    return 0;
}
import sys

def main():
    data = sys.stdin.buffer.read().split()
    n, m, p = int(data[0]), int(data[1]), int(data[2])
    E = [(0, 0, int(x)) for x in data[3:3 + p]]           # cạnh ảo 0 – nhà máy
    pos = 3 + p
    for i in range(m):
        u, v, c = int(data[pos]), int(data[pos + 1]), int(data[pos + 2]); pos += 3
        E.append((c, u, v))
    E.sort()
    par = list(range(n + 1))

    def find(x):
        while par[x] != x:
            par[x] = par[par[x]]                          # nén đường đi (chia đôi)
            x = par[x]
        return x
    cost = used = 0
    for c, u, v in E:
        a, b = find(u), find(v)
        if a == b:
            continue                                      # tạo chu trình
        par[a] = b; cost += c; used += 1
        if used == n:
            break
    print(cost if used == n else -1)

main()

Độ phức tạp: $O((m + p) \log (m + p))$ cho việc sắp xếp, DSU gần như $O(1)$ mỗi thao tác.

Câu 6. Bộ ba tăng dần (Fenwick, bài 31)

Nhận dạng. Đếm bộ $i < j < k$ với điều kiện so sánh: cùng họ với đếm nghịch thế, nhưng ba chỉ số. Mẹo chuẩn: cố định phần tử giữa, khi đó hai đầu độc lập với nhau, đáp án tách thành tích $L_j \cdot R_j$.

Subtask 1 ($n \le 200$): ba vòng for, $O(n^3)$. Subtask 2 ($n \le 5000$): cố định $j$, đếm $L_j$ và $R_j$ bằng hai vòng lặp, $O(n^2)$.

Xem code C++ / Python: câu 6 vét cạn $O(n^2)$, cố định phần tử giữa (bấm để mở)
#include <bits/stdc++.h>
using namespace std;

int main(){
    int n;
    cin >> n;
    vector<long long> a(n);
    for(auto &x : a) cin >> x;
    long long ans = 0;
    for(int j = 0; j < n; j++){                 // cố định phần tử giữa
        long long L = 0, R = 0;
        for(int i = 0; i < j; i++) if(a[i] < a[j]) L++;
        for(int k = j + 1; k < n; k++) if(a[k] > a[j]) R++;
        ans += L * R;
    }
    cout << ans;
    return 0;
}
n = int(input())
a = list(map(int, input().split()))
ans = 0
for j in range(n):                             # cố định phần tử giữa
    L = sum(1 for i in range(j) if a[i] < a[j])
    R = sum(1 for k in range(j + 1, n) if a[k] > a[j])
    ans += L * R
print(ans)

Subtask 3. $L_j$ = số phần tử đứng trước nhỏ hơn $a_j$: quét từ trái sang, Fenwick lưu “giá trị $x$ đã xuất hiện bao nhiêu lần”, hỏi tổng tiền tố tới $a_j - 1$ rồi cộng 1 tại $a_j$. Giá trị tới $10^9$ nên phải nén về $1..k$ trước. $R_j$ làm y hệt nhưng quét từ phải sang và đếm số lớn hơn: (số đã thêm) − (tiền tố tới $a_j$). Dùng tiền tố tới $a_j - 1$ và tiền tố tới $a_j$ đúng chỗ là để loại các số bằng $a_j$, vì đề yêu cầu tăng ngặt.

MÔ PHỎNGCâu 6: đếm bộ ba tăng bằng hai lượt quét FenwickNhập dãy (3 đến 10 số từ 1 đến 99). Hàng giữa là số lần mỗi giá trị đã xuất hiện, đúng thứ Fenwick lưu.
phần tử giữa jđược đếm vào L[j] hoặc R[j]ô vừa cộng 1chưa xét
Xem code C++ / Python: câu 6 đầy đủ, hai lượt quét Fenwick (bấm để mở)
#include <bits/stdc++.h>
using namespace std;

int n, bit[200005];
void add(int i){ for(; i <= n; i += i & -i) bit[i]++; }
int get(int i){ int s = 0; for(; i > 0; i -= i & -i) s += bit[i]; return s; }

int main(){
    ios::sync_with_stdio(false); cin.tie(nullptr);
    cin >> n;
    vector<int> a(n), v;
    for(auto &x : a) cin >> x;
    v = a; sort(v.begin(), v.end()); v.erase(unique(v.begin(), v.end()), v.end());
    for(auto &x : a) x = lower_bound(v.begin(), v.end(), x) - v.begin() + 1;   // nén về 1..k
    vector<long long> L(n), R(n);
    for(int j = 0; j < n; j++){ L[j] = get(a[j] - 1); add(a[j]); }          // bên trái, nhỏ hơn
    fill(bit, bit + n + 1, 0);
    for(int j = n - 1; j >= 0; j--){ R[j] = (n - 1 - j) - get(a[j]); add(a[j]); }  // bên phải, lớn hơn
    long long ans = 0;
    for(int j = 0; j < n; j++) ans += L[j] * R[j];
    cout << ans;
    return 0;
}
import sys

def main():
    data = sys.stdin.buffer.read().split()
    n = int(data[0])
    a = list(map(int, data[1:n + 1]))
    rank = {x: i + 1 for i, x in enumerate(sorted(set(a)))}
    a = [rank[x] for x in a]                     # nén về 1..k
    k = len(rank)
    bit = [0] * (k + 1)
    L = [0] * n
    for j in range(n):                           # bên trái, nhỏ hơn
        i, s = a[j] - 1, 0
        while i > 0: s += bit[i]; i -= i & -i
        L[j] = s
        i = a[j]
        while i <= k: bit[i] += 1; i += i & -i
    bit = [0] * (k + 1)
    ans = 0
    for j in range(n - 1, -1, -1):               # bên phải, lớn hơn
        i, s = a[j], 0
        while i > 0: s += bit[i]; i -= i & -i
        ans += L[j] * ((n - 1 - j) - s)
        i = a[j]
        while i <= k: bit[i] += 1; i += i & -i
    print(ans)

main()

Độ phức tạp: $O(n \log n)$. Đáp án tối đa $C(2 \cdot 10^5, 3) \approx 1{,}3 \cdot 10^{15}$, cần long long; $L_j$, $R_j$ cũng nên để long long để tích không tràn.

Tự chấm điểm
Câu 1 – 3 làm trọn: 45 điểm, đủ giải khuyến khích ở nhiều tỉnh. Thêm subtask vét cạn của câu 4 – 6 (30% + 30% + 50% số điểm): khoảng 70 điểm. Trọn cả đề là em đã nắm chắc series. Câu nào sai, hãy quay lại bài học tương ứng trong Bảng 1 rồi làm thêm 2 – 3 bài cùng dạng.

Phần 5Lời khuyên ôn thi và lộ trình tiếp theo

  • Có thư viện code của riêng em. Lũy thừa nhanh, giai thừa nghịch đảo, Dijkstra, DSU, Fenwick, segment tree, KMP, hash: mỗi cái em phải gõ lại được trong 3 – 5 phút, không cần nhìn.
  • Luyện theo dấu hiệu, không theo tên bài. Khi làm xong một bài, ghi vào sổ: “dấu hiệu nào làm mình nghĩ tới kỹ thuật này?”. Lần sau gặp dấu hiệu đó, em sẽ nhớ ra ngay.
  • Luôn viết vét cạn trước cho câu khó, và tập thói quen stress test như Phần 3.
  • Soi biên và kiểu dữ liệu. Hơn một nửa số bài “đúng ý tưởng mà mất điểm” là do tràn int, quên modulo âm, hoặc nhầm < với <=.
  • Thi thử định kỳ. Mỗi tuần một đề 180 phút, sau đó dành thời gian chữa kỹ hơn cả thời gian làm. Đề bài 23 và đề bài này là hai đề đầu tiên của em.

Tiếp theo học gì? Series đến đây là khép lại, nhưng kỹ năng chỉ thật sự chắc khi em giải nhiều bài. Mình đã sắp xếp toàn bộ bài tập trên blog thành lộ trình học thuật toán theo từng chặng: mỗi chặng ứng với một nhóm kỹ thuật, bài xếp từ dễ tới khó, bài nào cũng có lời giải C++ và Python. Em đi lần lượt từng chặng, chặng nào làm được khoảng 70% số bài thì chuyển sang chặng sau.

Luyện tập trên web
Các bài dưới đây trên blog đều có lời giải chi tiết bằng C++ và Python, xếp từ dễ tới khó. Em hãy tự làm trước rồi mới xem lời giải nhé.Xem toàn bộ bài cùng chủ đề: series Thuật toán nâng cao.

Cảm ơn em đã đồng hành cùng mình suốt 9 bài của series Thuật toán nâng cao. Chúc em giữ được nhịp luyện đều đặn và đạt kết quả thật tốt trong kỳ thi sắp tới!

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 😇
Bản quyền thuộc về Thầy Nguyễn Việt Nam Sơn. Bài viết, lời giải, hình minh hoạ và mã nguồn trên trang là công sức biên soạn của thầy qua nhiều năm giảng dạy. Vui lòng không sao chép, đăng lại hay sử dụng dưới mọi hình thức khi chưa được sự đồng ý của tác giả. Nếu thấy hữu ích, hãy chia sẻ bằng đường link bài gốc nhé. Cảm ơn bạn!
Tự học thuật toán: khó ở đâu?
Đọc xong một bài giải là bước đầu. Phần lớn học sinh tự học sẽ gặp những trở ngại sau:
  • 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.
Học cùng thầy Nam Sơn, các trở ngại đó được gỡ bỏ:
  • 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