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 →

Chuyên đề 8: Python khi đi thi: viết sao cho đủ nhanh, đọc ghi nhanh, hàm có sẵn, đệ quy sâu và khi nào nên chuyển sang C++

Chuyên đề 8: Python khi đi thi: viết sao cho đủ nhanh, đọc ghi nhanh, hàm có sẵn, đệ quy sâu và khi nào nên chuyển sang C++
Series: Chuyên đề nhập môn Độ khó: Dễ Chuyên đề 8 / Nhập môn lập trình Python Tối ưu tốc độ
// Chuyên đề nhập môn · Chuyên đề 8
Python khi đi thi: viết sao cho đủ nhanh và khi nào nên chuyển sang C++
Cùng một thuật toán đúng, bạn này được 100 điểm, bạn kia bị “quá thời gian” chỉ vì cách đọc dữ liệu hay một vòng lặp viết vụng. Bài này dạy em viết Python nhanh gấp 5 đến 100 lần mà không đổi thuật toán, và biết lúc nào nên chuyển hẳn sang C++.

Lời mở đầu

Ở bài 1, mình đã đo một vòng lặp $10^8$ lần: C++ chạy khoảng 0,1 giây, Python mất cỡ 8 giây. Nhiều bạn đọc tới đó liền kết luận “thi thì phải dùng C++”. Thực tế không đơn giản như vậy: rất nhiều đề HSG cấp tỉnh, đề chuyên Tin có giới hạn vừa phải, và một bài Python viết đúng cách vẫn đạt trọn điểm. Ngược lại, một bài Python viết “kiểu C++” (đọc từng dòng bằng input(), in từng số bằng print, tìm kiếm bằng list.index) có thể chậm hơn bản viết đúng tới cả trăm lần.

Chuyên đề cuối cùng này là “sổ tay đi thi” cho bạn nào chọn Python. Mình giả sử em đã nắm cách ước lượng độ phức tạp ở bài 9, biết các cấu trúc dữ liệu ở bài 11: STL cơ bản và đã đọc phần đọc ghi file ở chuyên đề 7. Mọi đoạn code vẫn có cả C++ lẫn Python để em thấy cách viết tương đương; các bảng thời gian đều do thầy đo thật trên máy mình (Python 3.13, g++ -O2), con số trên máy chấm sẽ khác đôi chút nhưng tỉ lệ giữa các cách viết thì gần như giữ nguyên.

Trong bài này bạn sẽ học: vì sao Python chậm và quy tắc khoảng $10^7$ phép mỗi giây để biết bài Python có kịp không; mẹo bọc code trong hàm main(); đọc nhanh bằng sys.stdin.buffer.read().split(), in nhanh bằng join; dùng các hàm có sẵn chạy bằng C: sum, sorted, min/max với key, Counter, bisect, itertools, set/dict tra cứu $O(1)$; đệ quy sâu với setrecursionlimit, luồng có ngăn xếp lớn và cách khử đệ quy bằng ngăn xếp tự quản; các bẫy và mẹo hay gặp: mảng 2 chiều, heapq, deque, số nguyên lớn; và bảng khi nào nên dùng C++. Có 2 khung chạy tay, 3 mô phỏng bấm từng bước và 8 bài tập kèm gợi ý.

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

Phần 1Vì sao Python chậm và làm sao biết bài có kịp không?

Chương trình C++ được biên dịch một lần ra mã máy, rồi CPU chạy thẳng mã đó. Python thì khác: mã nguồn được dịch ra bytecode (các lệnh trung gian), và mỗi lần chạy, trình thông dịch phải đọc từng lệnh bytecode, xem đó là lệnh gì, kiểm tra kiểu dữ liệu rồi mới thực hiện. Thêm nữa, mỗi số nguyên trong Python là một đối tượng nằm trong bộ nhớ, nên phép s += x không chỉ là một phép cộng mà còn tạo đối tượng mới, cập nhật bộ đếm tham chiếu… Hình dưới so sánh một vòng của vòng lặp cộng dồn:

C++ (đã dịch ra mã máy)Python (thông dịch từng lệnh)add rax, [rdi]cộng thẳng vào thanh ghiadd rdi, 8sang phần tử kếcmp / jnechưa hết thì quay lạiFOR_ITERlấy phần tử kế, hết thì thoátSTORE_FAST xgán vào biến xLOAD_FAST s, xđẩy s, x lên ngăn xếpBINARY_OP +=kiểm tra kiểu, cộng, tạo số mớiSTORE_FAST sgán lại s, đếm tham chiếuJUMP_BACKWARDquay lại đầu vòng≈ 3 lệnh máy mỗi vòng10⁸ vòng ≈ 0,1 giây6 lệnh bytecode, mỗi lệnh hàng chục lệnh máy10⁸ vòng ≈ 7 giây (đo trên Python 3.13)

Hình 1. Một vòng for x in a: s += x. Bên phải là bytecode thật mà Python 3.13 sinh ra (xem bằng import dis; dis.dis(f)).

Từ đó ta có quy tắc ước lượng cho người dùng Python, đặt cạnh quy tắc $10^8$ phép mỗi giây của C++ mà em đã học ở bài 9:

Quy tắc ước lượng cho Python
Vòng lặp viết bằng Python thuần chạy khoảng $10^7$ vòng đơn giản mỗi giây (nhanh hơn một chút nếu thân vòng lặp rất ngắn, chậm hơn nếu có nhiều phép tính hay truy cập list lồng nhau). Còn những việc chạy bên trong hàm có sẵn (sum, sorted, x in set, split, join…) thì nhanh gần bằng C++. Vậy khi đọc đề, em đếm số vòng lặp Python chứ không chỉ đếm độ phức tạp.
Đoạn codeSố vòngC++Python
s += i * i % 7$10^7$0,015 s0,71 s
s += i * i % 7$10^8$0,13 s7,3 s
s += i viết thẳng ngoài hàm (biến toàn cục)$10^7$–0,66 s
s += i viết trong def main()$10^7$–0,31 s
s = sum(range(n))$10^7$–0,17 s
s = n * (n - 1) // 2 (công thức)1–≈ 0 s

Bảng 1. Thời gian đo trên máy của thầy (Python 3.13, g++ -O2), chỉ mang tính tương đối. Dòng cuối nhắc em: tìm được công thức hay thuật toán tốt hơn luôn là cách tăng tốc mạnh nhất.

Hai dòng giữa cho thấy một mẹo miễn phí: chỉ cần bọc code vào một hàm là nhanh gần gấp đôi. Lý do: biến toàn cục được tìm trong một từ điển tên, còn biến cục bộ của hàm được Python đánh số sẵn và truy cập theo chỉ số. Vì vậy mọi code Python trong chuyên đề này đều viết trong hàm main():

Xem code C++ / Python: tổng i * i % 7 viết trong hàm main (bấm để mở)
#include <bits/stdc++.h>
using namespace std;

int main(){
    long long n;
    cin >> n;
    long long s = 0;
    for(long long i = 1; i <= n; i++) s += i * i % 7;
    cout << s;
    return 0;
}
import sys

def main():                          # đặt code trong hàm: biến cục bộ nhanh gần gấp đôi
    n = int(sys.stdin.readline())
    s = 0
    for i in range(1, n + 1):
        s += i * i % 7
    print(s)

main()

Đọc giới hạn để biết Python có kịp không. Ghép quy tắc trên với độ phức tạp của thuật toán, em được bảng tra nhanh dưới đây cho giới hạn 1 giây. Cột Python giả sử phần nặng nhất là vòng lặp tự viết; nếu phần nặng nhất là sorted hay thao tác trên set, Python gần theo kịp cột C++. Cách đọc giới hạn để đoán thuật toán em xem thêm ở bài 22: Kỹ năng luyện thi.

Độ phức tạpC++ (~$10^8$ phép/s)Python vòng lặp thuần (~$10^7$/s)Ghi chú cho Python
$O(n)$$n \le 10^8$$n \le 10^7$với $n = 10^6$ thoải mái nếu đọc nhanh
$O(n \log n)$$n \le 5 \cdot 10^6$$n \le 5 \cdot 10^5$sorted $10^6$ số mất khoảng 0,33 s
$O(n \sqrt n)$$n \le 2 \cdot 10^5$$n \le 4 \cdot 10^4$thường phải tối ưu kỹ
$O(n^2)$$n \le 10^4$$n \le 3000$$n = 5000$: 2,5 giây, quá giờ
$O(n^3)$$n \le 450$$n \le 200$Floyd, QHĐ đoạn: cân nhắc C++
$O(2^n)$$n \le 26$$n \le 22$quay lui: Python chậm hơn nhiều

Bảng 2. Giới hạn $n$ ước lượng để chạy trong 1 giây. Đây là con số để định hướng, không phải luật cứng: hằng số trong thân vòng lặp có thể làm lệch 2–3 lần.

Máy chấm có “nhân thời gian” cho Python không?
Một số máy chấm (và một số kỳ thi) cho Python giới hạn thời gian gấp 2–5 lần C++, nhưng nhiều kỳ thi HSG không làm vậy: cùng một giới hạn cho mọi ngôn ngữ. Trước khi thi em phải hỏi rõ (hoặc đọc kỹ quy chế). Nếu không có hệ số bù, những bài cần $10^8$ phép tính gần như chắc chắn phải viết bằng C++.

Phần 2Đọc, ghi nhanh: sys.stdin.buffer, map và join

Với dữ liệu lớn, đọc và in thường chiếm phần lớn thời gian chạy của bài Python. Nguyên nhân là input() và print() làm rất nhiều việc phụ mỗi lần gọi (tìm luồng, xử lý mã hoá, bỏ dấu xuống dòng, đổi số ra xâu, kiểm tra tham số sep, end…). Gọi một triệu lần là một triệu lần làm việc phụ đó. Phần C++ (ios::sync_with_stdio(false), '\n' thay cho endl) em đã học ở bài 9 và chuyên đề 7; ở đây mình đo cho rõ và tập trung vào Python.

Cách làm (10⁶ số, mỗi số một dòng)Thời gianNhận xét
int(input()) trong vòng lặp0,75 – 0,83 scách “mặc định”, chậm nhất
input = sys.stdin.readline rồi int(input())0,22 snhanh gấp gần 4 lần, đổi đúng một dòng
sys.stdin.buffer.read().split() + map(int, …)0,17 sđọc hết một lần, nhanh nhất
C++: cin chưa tắt đồng bộ0,32 sC++ viết vụng còn chậm hơn Python viết khéo!
C++: cin sau ios::sync_with_stdio(false)0,07 s
print(x) từng dòng (in 10⁶ số)1,1 – 1,6 schậm nhất trong cả bảng
sys.stdout.write('\n'.join(map(str, a)))0,15 sghép một lần, in một lần
C++: cout << x << '\n' / endl0,06 s / 0,5 sendl xả bộ đệm mỗi dòng

Bảng 3. Đọc và in $10^6$ số (đo trên máy thầy, chỉ để so sánh tương đối). Đọc kiểu nào cũng được khi $n \le 10^4$; khi $n$ cỡ $10^5$ trở lên thì cách đọc bắt đầu quyết định điểm số.

Đọc hết một lần rồi tách. sys.stdin.buffer.read() lấy toàn bộ dữ liệu vào dưới dạng bytes chỉ với một lần gọi, .split() tách theo mọi khoảng trắng (dấu cách, tab, xuống dòng) bằng mã C, cho ra list các mẩu bytes. Hàm int() nhận thẳng được bytes nên không cần đổi sang xâu. Ưu điểm phụ: em không phải quan tâm dữ liệu nằm trên mấy dòng, đề có xuống dòng lung tung cũng không sao.

stdinb"3\n7 2 9\n".buffer.read()một khối bytes.split()[b'3', b'7', b'2', b'9']int(...) / map[3, 7, 2, 9]Chỉ 1 lần gọi hệ thống để đọc, 1 lần tách bằng mã C. Không quan tâm dữ liệu nằm trên mấy dòng.Ngược lại, input() gọi n lần, mỗi lần tạo một xâu, bỏ dấu xuống dòng rồi mới tách.

Hình 2. Quy trình đọc nhanh. Mọi mẩu dữ liệu nằm sẵn trong list, em lấy dần theo chỉ số hoặc bằng một con trỏ.

Khuôn mẫu đọc ghi nhanh trong Python
import sys

def main():
    data = sys.stdin.buffer.read().split()
    it = iter(data)                  # con trỏ đọc lần lượt
    n = int(next(it))
    a = [int(next(it)) for _ in range(n)]
    # ... hoặc cắt lát: a = list(map(int, data[1:n + 1]))
    out = []
    for x in a:
        out.append(x * 2)            # gom kết quả vào list
    sys.stdout.write('\n'.join(map(str, out)) + '\n')

main()
Ba ý chính: đọc một lần, xử lý trong hàm main(), gom kết quả rồi in một lần.

Ví dụ. Đề cho $n$ học sinh, mỗi dòng gồm tên (không có dấu cách) và điểm. In tên các bạn có điểm từ 5 trở lên theo thứ tự đầu vào, mỗi tên một dòng; không có ai thì in KHONG CO. Chú ý: tên đọc từ buffer là bytes (b'An'), phải .decode() trước khi ghép với xâu thường.

Xem code C++ / Python: đọc nhanh tên và điểm, in một lần bằng join (bấm để mở)
#include <bits/stdc++.h>
using namespace std;

int main(){
    ios::sync_with_stdio(false); cin.tie(nullptr);   // tăng tốc cin/cout
    int n;
    cin >> n;
    vector<string> res;
    for(int i = 0; i < n; i++){
        string name;
        int d;
        cin >> name >> d;
        if(d >= 5) res.push_back(name);
    }
    if(res.empty()) cout << "KHONG CO\n";
    for(auto &s : res) cout << s << '\n';             // '\n' thay cho endl
    return 0;
}
import sys

def main():
    data = sys.stdin.buffer.read().split()   # đọc HẾT một lần, tách theo dấu cách và xuống dòng
    n = int(data[0])
    res = []
    for i in range(n):
        name = data[1 + 2 * i]               # kiểu bytes, ví dụ b'An'
        d = int(data[2 + 2 * i])             # int() nhận được cả bytes
        if d >= 5:
            res.append(name.decode())        # đổi bytes -> str trước khi in
    if not res:
        res.append('KHONG CO')
    sys.stdout.write('\n'.join(res) + '\n')  # ghép rồi in MỘT lần

main()
Ba lỗi hay gặp khi đọc nhanh
1. sys.stdin.readline() giữ lại ký tự '\n' ở cuối. Đọc số thì int() tự bỏ qua, nhưng đọc xâu thì phải .rstrip() (hoặc .strip()), nếu không xâu bị dài thêm 1 và so sánh sai.
2. Đọc từ buffer được bytes: b'abc' == 'abc' là False. Hoặc so sánh bytes với bytes, hoặc .decode() trước.
3. Đề có dòng chứa dấu cách (họ tên đầy đủ, câu văn) thì không dùng split() toàn bộ được, vì nó tách cả dấu cách trong dòng. Khi đó đọc theo dòng: lines = sys.stdin.buffer.read().decode().split('\n').

Phần 3Dùng hàm có sẵn chạy bằng C, chọn đúng cấu trúc dữ liệu

Nguyên tắc vàng của Python: đẩy vòng lặp vào bên trong hàm có sẵn. Khi em gọi sum(a), vòng lặp cộng chạy trong mã C của Python, không phải thông dịch từng lệnh. Nhưng còn quan trọng hơn: nhiều thao tác trông “vô hại” lại ẩn một vòng lặp $O(n)$ bên trong, đặt nó vào vòng lặp ngoài là thành $O(n^2)$:

  • x in a, a.index(x), a.count(x), a.remove(x) với a là list: duyệt cả list, $O(n)$. Với set/dict: $O(1)$.
  • a.pop(0), a.insert(0, x): dồn cả list, $O(n)$. Dùng collections.deque để thêm, bớt ở đầu trong $O(1)$.
  • s = s + t trong vòng lặp với xâu: mỗi lần tạo xâu mới và chép lại phần cũ. Gom vào list rồi ''.join(...).
  • a[l:r] (cắt lát) tạo list mới, tốn $O(r - l)$. Cắt lát trong vòng lặp lớn cũng là $O(n^2)$.

Mô phỏng dưới đây “đua” các cách viết cho cùng một việc. Em đổi việc cần làm và đổi $n$ để thấy cách nào đỏ trước:

MÔ PHỎNGCuộc đua các cách viết: cùng một việc, nhanh chậm khác nhau thế nào?Chọn bài, nhập n (và q), giới hạn thời gian rồi bấm Tiếp. Số liệu là ước lượng từ phép đo thật.
kịp giới hạnquá giờcách vừa đo
Việc cần làmCách chậmThời gianCách nhanhThời gian
$10^4$ câu hỏi “có trong dãy $10^4$ số?”x in a (list)0,39 sx in s (set)0,0003 s
Lấy đầu hàng $10^5$ lầna.pop(0)0,94 sdq.popleft()0,005 s
Ghép $10^5$ ký tựs = s + c (*)0,14 s''.join(parts)0,004 s
Đếm tần suất $10^6$ sốvòng for + dict0,10 – 0,14 sCounter(a)0,07 – 0,09 s
Tìm max của $10^6$ sốvòng for + if0,018 smax(a)0,009 s
Tạo list $10^6$ phần tửvòng for + append0,049 slist comprehension0,042 s

Bảng 4. Đo trên máy thầy. Ba dòng đầu là đổi độ phức tạp (nhanh hơn hàng trăm lần), ba dòng sau chỉ đổi hằng số (nhanh hơn 1–2 lần). (*) CPython có tối ưu riêng cho s += c khi không biến nào khác cùng giữ xâu s, nhưng điều đó không được đảm bảo (PyPy, hay chỉ cần thêm một biến tham chiếu là mất), nên đừng dựa vào nó.

Ví dụ: đếm cặp có tổng bằng $x$. Cho dãy $n$ số và số $x$, đếm số cặp $i < j$ có $a_i + a_j = x$. Hai vòng for là $O(n^2)$: với $n = 2 \cdot 10^5$ thì C++ còn chật vật, Python thì không có cửa. Cách tốt: duyệt $j$ từ trái sang phải, giữ một bảng đếm cnt các số đã gặp. Số $a_j$ ghép được với mọi số bên trái có giá trị $x - a_j$, mà số lượng của chúng chính là cnt[x - a_j], tra trong $O(1)$. Cùng ý tưởng này em đã gặp ở bài 13: mảng đánh dấu, chỉ khác là giá trị có thể rất lớn hoặc âm nên ta dùng dict thay cho mảng.

C++▶ Chạy tay từng dòng codeDuyệt từng a[j]: tra bảng cnt xem đã gặp bao nhiêu số bằng x − a[j], cộng vào ans, rồi ghi nhận a[j] (ô đỏ: khóa vừa tra trúng, ô xanh: khóa vừa thêm).
Dữ liệu mẫu:
1#include <bits/stdc++.h>
2using namespace std;
3
4int main(){
5 int n;
6 long long x;
7 cin >> n >> x;
8 map<long long, int> cnt;
9 long long ans = 0;
10 for(int j = 0; j < n; j++){
11 long long a;
12 cin >> a;
13 if(cnt.count(x - a)) ans += cnt[x - a];
14 cnt[a]++;
15 }
16 cout << ans;
17 return 0;
18}
Xem code C++ / Python: đếm cặp có tổng bằng x bằng map / dict (bấm để mở)
#include <bits/stdc++.h>
using namespace std;

int main(){
    ios::sync_with_stdio(false); cin.tie(nullptr);
    int n;
    long long x;
    cin >> n >> x;
    map<long long, int> cnt;           // cnt[v] = số lần v đã gặp ở bên trái
    long long ans = 0;
    for(int j = 0; j < n; j++){
        long long a;
        cin >> a;
        auto it = cnt.find(x - a);     // ghép a với mọi số bên trái bằng x - a
        if(it != cnt.end()) ans += it->second;
        cnt[a]++;
    }
    cout << ans;
    return 0;
}
import sys

def main():
    data = sys.stdin.buffer.read().split()
    n, x = int(data[0]), int(data[1])
    cnt = {}                                 # cnt[v] = số lần v đã gặp ở bên trái
    ans = 0
    for a in map(int, data[2:2 + n]):
        ans += cnt.get(x - a, 0)             # tra dict: O(1), không duyệt lại dãy
        cnt[a] = cnt.get(a, 0) + 1
    print(ans)

main()

Bảng dưới là “từ điển dịch” các hàm có sẵn em hay dùng nhất, kèm bản tương đương trong C++ (nhớ lại bài 11 và bài 10). Thuộc bảng này là em đã có gần đủ “vũ khí” cho các bài nhập môn bằng Python.

ViệcPythonC++ tương đươngĐộ phức tạp
Tổng, min, maxsum(a), min(a), max(a)accumulate, *min_element, *max_element$O(n)$
Vị trí của số lớn nhấtmax(range(n), key=a.__getitem__)max_element(a, a + n) - a$O(n)$
Sắp xếp nhiều tiêu chísorted(a, key=lambda t: (-t[1], t[0]))sort với hàm cmp$O(n \log n)$
Đếm tần suấtCounter(a), c.most_common(k)map<int, int> cnt$O(n)$
Tìm kiếm nhị phânbisect_left, bisect_rightlower_bound, upper_bound$O(\log n)$
Mảng cộng dồnlist(accumulate(a))partial_sum$O(n)$
Duyệt tổ hợp, hoán vịcombinations(a, k), permutations(a)next_permutationtheo số cấu hình
Tập hợp, từ điểnset, dict (bảng băm)unordered_set, unordered_map$O(1)$ trung bình
Đảo ngược, ghép xâua[::-1], ' '.join(...)reverse, vòng cout$O(n)$

Bảng 5. Hàm có sẵn trong Python và bản tương đương C++. Counter ở collections; bisect_left, bisect_right ở bisect; accumulate, combinations, permutations ở itertools.

Chú ý khi dùng key
min, max, sorted đều nhận tham số key: hàm biến mỗi phần tử thành “giá trị để so sánh”. Muốn sắp giảm dần theo điểm nhưng tăng dần theo tên, dùng key là bộ (-diem, ten): bộ được so sánh lần lượt từng thành phần, và đổi dấu giúp đảo chiều một tiêu chí số. Với tiêu chí là xâu không đổi dấu được thì sắp 2 lần (sắp theo tiêu chí phụ trước, rồi tiêu chí chính sau), vì sorted của Python là sắp xếp ổn định.

Phần 4Đệ quy sâu: setrecursionlimit, luồng có ngăn xếp lớn và ngăn xếp tự quản

Ở bài 5 em đã viết đệ quy tính $n!$. Mỗi lời gọi hàm chưa xong được cất vào ngăn xếp lời gọi (call stack) để chờ lời gọi con trả về. Python chặn độ sâu này ở 1000 tầng theo mặc định: gọi sâu hơn là nhận ngay RecursionError: maximum recursion depth exceeded. Bài Python dùng đệ quy (DFS, quay lui, đệ quy có nhớ) với dữ liệu kiểu “chuỗi dài” $10^5$ phần tử sẽ chết ngay ở test lớn, dù thuật toán hoàn toàn đúng.

Bài toán mẫu: cấp bậc trong công ty. Có $n$ người, người $i$ có sếp trực tiếp là $p_i$ ($p_i = 0$ nghĩa là giám đốc, cấp 1). Cấp của một người bằng cấp của sếp cộng 1. In cấp của mọi người. Công thức đệ quy rất tự nhiên: level(u) = level(p[u]) + 1, có nhớ lại kết quả để mỗi người chỉ tính một lần. Nhưng nếu công ty là một “chuỗi” $1 \to 2 \to \ldots \to n$ thì đệ quy sâu đúng $n$ tầng.

Cách 1: nâng giới hạn. sys.setrecursionlimit(10**6) bỏ chốt chặn 1000 tầng. Tuy vậy, với Python 3.10 trở về trước (nhiều máy chấm vẫn dùng), mỗi tầng đệ quy Python còn chiếm cả ngăn xếp của C (mặc định chỉ vài MB), nên đệ quy quá sâu sẽ làm chương trình chết hẳn (Segmentation fault, máy chấm báo RE) thay vì báo lỗi. Cách chữa là chạy hàm chính trong một luồng (thread) mới được cấp ngăn xếp lớn. Từ Python 3.11, lời gọi giữa các hàm Python không còn ăn ngăn xếp C nên chỉ cần setrecursionlimit, nhưng khuôn mẫu dưới đây chạy được trên mọi phiên bản:

Xem code C++ / Python: đệ quy có nhớ với setrecursionlimit và threading (bấm để mở)
#include <bits/stdc++.h>
using namespace std;

int n, p[200005], lv[200005];

int level(int u){
    if(lv[u]) return lv[u];                // đã tính rồi thì dùng lại
    if(p[u] == 0) return lv[u] = 1;        // giám đốc: cấp 1
    return lv[u] = level(p[u]) + 1;        // cấp của sếp + 1
}

int main(){
    ios::sync_with_stdio(false); cin.tie(nullptr);
    cin >> n;
    for(int i = 1; i <= n; i++) cin >> p[i];
    for(int i = 1; i <= n; i++) level(i);
    for(int i = 1; i <= n; i++) cout << lv[i] << " ";
    return 0;
}
import sys, threading

def main():
    data = sys.stdin.buffer.read().split()
    n = int(data[0])
    p = [0] + [int(x) for x in data[1:n + 1]]
    lv = [0] * (n + 1)

    def level(u):
        if lv[u]:                            # đã tính rồi thì dùng lại
            return lv[u]
        lv[u] = 1 if p[u] == 0 else level(p[u]) + 1
        return lv[u]

    for i in range(1, n + 1):
        level(i)
    sys.stdout.write(' '.join(map(str, lv[1:])) + '\n')

sys.setrecursionlimit(1 << 25)           # bỏ giới hạn 1000 tầng của Python
threading.stack_size(1 << 26)            # xin 64 MB ngăn xếp cho luồng mới
t = threading.Thread(target=main)
t.start()
t.join()
Đo thật: đệ quy sâu tốn bao nhiêu?
Thầy thử hàm đệ quy đơn giản sâu $10^6$ tầng trên Python 3.13: chạy 0,22 giây nhưng dùng tới khoảng 150 MB bộ nhớ, vì mỗi tầng giữ một khung hàm. Với giới hạn bộ nhớ 256 MB và hàm có nhiều biến cục bộ hơn, em rất dễ bị MLE. C++ cũng không miễn nhiễm: trên Windows (Code::Blocks), ngăn xếp mặc định chỉ khoảng 1 MB, đệ quy vài chục nghìn tầng đã có thể tràn. Vì thế khi độ sâu có thể tới $10^5$ trở lên, cách chắc ăn nhất là khử đệ quy.

Cách 2: ngăn xếp tự quản. Ý tưởng: thứ mà đệ quy cất lên ngăn xếp lời gọi chính là “những người đang chờ biết cấp của sếp”. Ta tự cất họ vào một list bình thường st: từ người $i$, đi lên theo $p$ và đẩy từng người chưa biết cấp vào st, tới khi gặp người đã biết cấp (hoặc đi quá giám đốc). Sau đó lấy dần từ đỉnh st ra: người lấy ra trước là người được đẩy vào sau cùng, tức là người gần giám đốc nhất, nên sếp của họ chắc chắn đã có cấp. List có thể dài $10^6$ phần tử mà không cần giới hạn nào cả.

Em so sánh hai cách trong mô phỏng: chọn “Đệ quy có nhớ” rồi “Ngăn xếp tự quản”, để ý chiều cao ngăn xếp ở hai bên luôn giống nhau, chỉ khác ai giữ nó:

MÔ PHỎNGĐệ quy và ngăn xếp tự quản: ai đang giữ các lời gọi dở dang?Nhập dãy sếp p[1..n] (0 là giám đốc), chọn cách tính rồi bấm Tiếp. Thử cả dãy “chuỗi dài” 2 3 4 5 6 7 8 0.
đang xétđang nằm trong ngăn xếpđã biết cấpchưa biết
C++▶ Chạy tay từng dòng codeKhử đệ quy: đi lên theo p và đẩy người chưa biết cấp vào st (ô vàng), rồi lấy ra từ đỉnh để điền lv (ô đỏ: người vừa lấy ra).
Dữ liệu mẫu:
1#include <bits/stdc++.h>
2using namespace std;
3
4int p[105], lv[105];
5
6int main(){
7 int n;
8 cin >> n;
9 for(int i = 1; i <= n; i++) cin >> p[i];
10 vector<int> st;
11 for(int i = 1; i <= n; i++){
12 int u = i;
13 while(u != 0 && lv[u] == 0){
14 st.push_back(u);
15 u = p[u];
16 }
17 while(!st.empty()){
18 int v = st.back(); st.pop_back();
19 lv[v] = (p[v] == 0) ? 1 : lv[p[v]] + 1;
20 }
21 }
22 for(int i = 1; i <= n; i++) cout << lv[i] << " ";
23 return 0;
24}
Xem code C++ / Python: tính cấp bậc bằng ngăn xếp tự quản (không đệ quy) (bấm để mở)
#include <bits/stdc++.h>
using namespace std;

int n, p[200005], lv[200005];

int main(){
    ios::sync_with_stdio(false); cin.tie(nullptr);
    cin >> n;
    for(int i = 1; i <= n; i++) cin >> p[i];
    vector<int> st;                                   // ngăn xếp tự quản
    for(int i = 1; i <= n; i++){
        int u = i;
        while(u != 0 && lv[u] == 0){                  // đi lên, cất những người chưa biết cấp
            st.push_back(u);
            u = p[u];
        }
        while(!st.empty()){                           // lấy ra theo thứ tự ngược lại
            int v = st.back(); st.pop_back();
            lv[v] = (p[v] == 0) ? 1 : lv[p[v]] + 1;   // sếp của v chắc chắn đã có cấp
        }
    }
    for(int i = 1; i <= n; i++) cout << lv[i] << " ";
    return 0;
}
import sys

def main():
    data = sys.stdin.buffer.read().split()
    n = int(data[0])
    p = [0] + [int(x) for x in data[1:n + 1]]
    lv = [0] * (n + 1)
    st = []                                   # ngăn xếp tự quản: list bình thường
    for i in range(1, n + 1):
        u = i
        while u != 0 and lv[u] == 0:          # đi lên, cất những người chưa biết cấp
            st.append(u)
            u = p[u]
        while st:                             # lấy ra theo thứ tự ngược lại
            v = st.pop()
            lv[v] = 1 if p[v] == 0 else lv[p[v]] + 1
    sys.stdout.write(' '.join(map(str, lv[1:])) + '\n')

main()
Ba cách khử đệ quy hay dùng
1. Đổi thành vòng lặp theo thứ tự tính. Nếu đề đảm bảo $p_i < i$ (sếp luôn có số nhỏ hơn) thì chỉ cần for i in range(1, n + 1): lv[i] = lv[p[i]] + 1. Đây chính là tư tưởng của quy hoạch động ở bài 19: tính từ bài toán nhỏ lên.
2. Ngăn xếp tự quản như trên: đẩy những việc “đang chờ” vào list, xử lý khi điều kiện đã đủ.
3. Mô phỏng ngăn xếp lời gọi: mỗi phần tử của st lưu cả trạng thái lời gọi (đang xét tới con thứ mấy…). Cách này dùng cho DFS trên đồ thị, em sẽ gặp lại ở bài 21.

Phần 5Mẹo hay dùng, bẫy kinh điển và khi nào nên chuyển sang C++

Mảng 2 chiều đúng cách. Trong C++ em khai báo int a[1005][1005]; là xong. Trong Python, cách “ngắn gọn” a = [[0] * m] * n là một cái bẫy: phép * n không tạo ra $n$ hàng mới mà tạo $n$ mũi tên cùng trỏ vào một hàng. Gán a[1][2] = 7 thì cả cột 2 của mọi hàng đều thành 7! Cách đúng là a = [[0] * m for _ in range(n)]: biểu thức [0] * m được tính lại ở mỗi lượt nên mỗi hàng là một list riêng. Em xem tận mắt bằng mô phỏng hình hộp và mũi tên:

MÔ PHỎNGBẫy [[0] * m] * n: các hàng trỏ vào cùng một listChọn cách tạo bảng, nhập lệnh gán a[i][j] = v rồi bấm Tiếp để xem mũi tên tham chiếu.
list ngoài a (giữ mũi tên)ô vừa gánô bị đổi theo
Vì sao [0] * m thì không sao?
Phép * trên list luôn nhân bản tham chiếu. Với [0] * m, các ô cùng trỏ vào số 0, nhưng số nguyên không sửa được tại chỗ: gán a[j] = 5 chỉ đổi mũi tên của ô $j$ sang số 5, các ô khác không ảnh hưởng. Còn list thì sửa được tại chỗ, nên nhân bản tham chiếu tới list là nguy hiểm. Cùng lý do, b = a không sao chép list: muốn sao chép một hàng dùng a[:], sao chép bảng 2 chiều dùng [row[:] for row in a].

Truy cập mảng 2 chiều nhanh hơn. Mỗi lần viết a[i][j] Python phải tra hai lần. Trong vòng lặp lồng nhau, lấy sẵn hàng ra một biến (row = a[i], rồi dùng row[j]) bớt được một nửa số lần tra. Thầy đo việc điền bảng $1000 \times 1000$: khoảng 0,05 giây khi dùng biến row. Điều này rất có ích ở các bài lưới như chuyên đề 4.

Hàng đợi ưu tiên với heapq. Python không có lớp priority_queue mà có module heapq thao tác trên một list thường: heapify(h), heappush(h, x), heappop(h), phần tử nhỏ nhất luôn ở h[0]. Ngược với C++ (mặc định priority_queue lấy số lớn nhất). Muốn lấy số lớn nhất trong Python, đẩy vào -x rồi đổi dấu khi lấy ra. Phần tử có thể là bộ (khoá, giá trị), so sánh theo khoá trước. Ví dụ kinh điển gộp sỏi: có $n$ đống sỏi, mỗi lần gộp hai đống tốn chi phí bằng tổng hai đống; luôn gộp hai đống nhỏ nhất thì tổng chi phí nhỏ nhất.

Xem code C++ / Python: gộp sỏi bằng hàng đợi ưu tiên heapq / priority_queue (bấm để mở)
#include <bits/stdc++.h>
using namespace std;

int main(){
    ios::sync_with_stdio(false); cin.tie(nullptr);
    int n;
    cin >> n;
    priority_queue<long long, vector<long long>, greater<long long>> pq;   // đỉnh là số NHỎ nhất
    for(int i = 0; i < n; i++){ long long x; cin >> x; pq.push(x); }
    long long cost = 0;
    while(pq.size() > 1){
        long long a = pq.top(); pq.pop();
        long long b = pq.top(); pq.pop();
        cost += a + b;                 // gộp hai đống nhỏ nhất
        pq.push(a + b);
    }
    cout << cost;
    return 0;
}
import sys
from heapq import heapify, heappop, heappush

def main():
    data = sys.stdin.buffer.read().split()
    n = int(data[0])
    h = list(map(int, data[1:n + 1]))
    heapify(h)                         # biến list thành đống trong O(n); h[0] là số NHỎ nhất
    cost = 0
    while len(h) > 1:
        a = heappop(h)
        b = heappop(h)
        cost += a + b                  # gộp hai đống nhỏ nhất
        heappush(h, a + b)
    print(cost)

main()

deque cho hàng đợi. from collections import deque: append, appendleft, pop, popleft đều $O(1)$. Đừng dùng list.pop(0) làm hàng đợi (Bảng 4: chậm gấp gần 200 lần). Và số nguyên lớn là lợi thế lớn nhất của Python: không bao giờ tràn số, pow(a, b, m) tính lũy thừa modulo, math.isqrt(n) lấy căn nguyên chính xác (tránh lỗi làm tròn của int(n ** 0.5)), math.gcd, math.comb có sẵn. Những bài “số siêu lớn” ở chuyên đề 3 viết bằng Python chỉ vài dòng.

Vậy khi nào nên chuyển sang C++? Bảng dưới là kinh nghiệm thầy rút ra sau nhiều mùa thi. Ý chính: Python mạnh khi phần nặng nằm trong hàm có sẵn hoặc trong số học số lớn; C++ cần thiết khi phần nặng là vòng lặp tự viết với hàng chục triệu bước.

Tình huốngPythonLời khuyên
$O(n)$, $O(n \log n)$ với $n \le 2 \cdot 10^5$, dùng được sorted, set, dictổnviết Python, nhớ đọc ghi nhanh
Số nguyên lớn, xử lý xâu, công thức toánrất tốtPython ngắn gọn và ít lỗi hơn
Vòng lặp tự viết khoảng $10^7$ bướcsát núttối ưu hằng số hoặc chuyển C++
Hai vòng lồng nhau với $n \ge 5000$, QHĐ bảng $10^7$ ôquá giờC++
Quay lui, vét cạn nhiều nhánh, mô phỏng $10^8$ bướcquá giờC++
Đệ quy sâu trên $10^5$ tầngrủi rokhử đệ quy, hoặc C++ (cẩn thận ngăn xếp trên Windows)
Đề thi không có hệ số bù thời gian cho Pythonthiệtbài nặng nhất nên viết C++
Máy chấm có PyPy (như Codeforces)khá hơnvòng lặp số nguyên nhanh hơn nhiều lần, nhưng vẫn nên đo thử

Bảng 6. Chọn ngôn ngữ cho từng bài. Lời khuyên cho bạn thi HSG: thành thạo một ngôn ngữ chính, nhưng đọc hiểu được ngôn ngữ còn lại để chuyển khi cần.

Checklist trước khi nộp bài Python
1. Code nằm trong def main(). 2. Đọc bằng sys.stdin.buffer.read().split() hoặc sys.stdin.readline. 3. In một lần bằng join. 4. Không có x in list, list.index, pop(0), cắt lát trong vòng lặp lớn. 5. Mảng 2 chiều tạo bằng list comprehension. 6. Có đệ quy thì đã nâng giới hạn hoặc khử đệ quy. 7. Thử một test lớn nhất (sinh bằng code như bài 22) và đo thời gian trước khi nộp.
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ủ đề: nhãn CSES (giới hạn thời gian rất chặt, thích hợp luyện Python nhanh).

Bài tậpLuyện viết Python đủ nhanh

Bài 1Thống kê dãy lớnDễ

Cho dãy $n$ số nguyên ($1 \le n \le 10^6$, $|a_i| \le 10^9$). In trên một dòng bốn số: tổng, giá trị nhỏ nhất, giá trị lớn nhất và số lượng số chẵn của dãy.

Input
5
3 -4 8 1 6
Output
14 -4 8 3

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

💡 Gợi ý
Thuật toán chỉ là một lần duyệt, cái khó là $n = 10^6$: đọc bằng sys.stdin.buffer.read().split() và dùng sum, min, max có sẵn. Số lẻ có bit cuối bằng 1 nên số lượng số chẵn là n - sum(x & 1 for x in a) (trong Python, -3 & 1 vẫn bằng 1). Tổng có thể tới $10^{15}$: C++ phải dùng long long. Mẫu: tổng $3 - 4 + 8 + 1 + 6 = 14$, ba số chẵn là −4, 8, 6.
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;
    cin >> n;
    long long s = 0, mn = LLONG_MAX, mx = LLONG_MIN;
    int even = 0;
    for(int i = 0; i < n; i++){
        long long x;
        cin >> x;
        s += x;
        mn = min(mn, x);
        mx = max(mx, x);
        if(x % 2 == 0) even++;
    }
    cout << s << " " << mn << " " << mx << " " << even;
    return 0;
}
import sys

def main():
    data = sys.stdin.buffer.read().split()
    n = int(data[0])
    a = list(map(int, data[1:n + 1]))
    odd = sum(x & 1 for x in a)              # x & 1 = 1 khi x lẻ (kể cả số âm)
    print(sum(a), min(a), max(a), n - odd)

main()
Bài 2Tổng đoạn nhiều truy vấnDễ

Cho dãy $n$ số nguyên ($n \le 2 \cdot 10^5$, $|a_i| \le 10^9$) và $q$ truy vấn ($q \le 2 \cdot 10^5$), mỗi truy vấn gồm $l$, $r$ ($1 \le l \le r \le n$). Với mỗi truy vấn in ra $a_l + a_{l+1} + \cdots + a_r$ trên một dòng.

Dòng đầu: $n$, $q$. Dòng hai: dãy $a$. Tiếp theo là $q$ dòng truy vấn.

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

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

💡 Gợi ý
Dùng mảng cộng dồn ở bài 13: $P_0 = 0$, $P_i = P_{i-1} + a_i$, tổng đoạn là $P_r - P_{l-1}$. Trong Python dựng $P$ bằng [0] + list(accumulate(a)). Phần dễ bị quá giờ là in $2 \cdot 10^5$ dòng: gom vào list rồi join một lần. Mẫu: $1 + 2 + 3 = 6$, $2 + 3 + 4 + 5 = 14$.
Xem code tham khảo (C++ / Python)
#include <bits/stdc++.h>
using namespace std;

long long P[200005];

int main(){
    ios::sync_with_stdio(false); cin.tie(nullptr);
    int n, q;
    cin >> n >> q;
    for(int i = 1; i <= n; i++){ long long x; cin >> x; P[i] = P[i - 1] + x; }
    while(q--){
        int l, r;
        cin >> l >> r;
        cout << P[r] - P[l - 1] << '\n';
    }
    return 0;
}
import sys
from itertools import accumulate

def main():
    data = sys.stdin.buffer.read().split()
    n, q = int(data[0]), int(data[1])
    P = [0] + list(accumulate(map(int, data[2:2 + n])))
    out = []
    pos = 2 + n
    for _ in range(q):
        l, r = int(data[pos]), int(data[pos + 1])
        pos += 2
        out.append(P[r] - P[l - 1])
    sys.stdout.write('\n'.join(map(str, out)) + '\n')

main()
Bài 3Điểm danhDễ

Lớp có $n$ bạn đã có mặt, mỗi bạn có một mã gồm chữ cái và chữ số (không quá 20 ký tự). Thầy hỏi $q$ lần, mỗi lần một mã; với mỗi câu hỏi in CO nếu bạn có mã đó đã có mặt, ngược lại in KHONG. Giới hạn $n, q \le 2 \cdot 10^5$.

Dòng đầu: $n$, $q$. Dòng hai: $n$ mã có mặt. Dòng ba: $q$ mã được hỏi.

Input
3 4
HS01 HS07 an22
HS07 HS02 an22 AN22
Output
CO
KHONG
CO
KHONG

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

💡 Gợi ý
Nếu lưu các mã trong list rồi hỏi x in a thì mỗi câu hỏi tốn $O(n)$, tổng $4 \cdot 10^{10}$: quá giờ cả với C++. Lưu vào set (C++: unordered_set<string> hoặc set<string>) thì mỗi câu hỏi $O(1)$. Trong Python có thể để nguyên các mã ở dạng bytes vì ta chỉ so sánh bytes với bytes, không cần decode. Chú ý chữ hoa khác chữ thường: AN22 không phải an22.
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, q;
    cin >> n >> q;
    unordered_set<string> s;
    s.reserve(2 * n + 1);
    for(int i = 0; i < n; i++){ string x; cin >> x; s.insert(x); }
    while(q--){
        string x;
        cin >> x;
        cout << (s.count(x) ? "CO" : "KHONG") << '\n';
    }
    return 0;
}
import sys

def main():
    data = sys.stdin.buffer.read().split()
    n, q = int(data[0]), int(data[1])
    s = set(data[2:2 + n])                       # tập các mã (dạng bytes)
    out = ['CO' if x in s else 'KHONG' for x in data[2 + n:2 + n + q]]
    sys.stdout.write('\n'.join(out) + '\n')

main()
Bài 4Những từ xuất hiện nhiều nhấtDễ

Cho $n$ từ ($n \le 2 \cdot 10^5$, mỗi từ gồm chữ cái thường, dài không quá 15) và số $k$. In ra $k$ từ xuất hiện nhiều nhất, mỗi dòng gồm từ và số lần xuất hiện. Hai từ có cùng số lần thì từ nhỏ hơn theo thứ tự từ điển đứng trước. Nếu số từ khác nhau ít hơn $k$ thì in tất cả.

Dòng đầu: $n$, $k$. Dòng hai: $n$ từ.

Input
7 2
meo cho ga cho meo ca meo
Output
meo 3
cho 2

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

💡 Gợi ý
Đếm bằng Counter (C++: map<string, int>), rồi sắp các cặp (từ, số lần) với key (-số_lần, từ): đổi dấu để số lần giảm dần, còn từ vẫn tăng dần. Trong C++ viết hàm so sánh tương ứng. Mẫu: meo 3 lần, cho 2 lần; ga và ca cùng 1 lần, nếu $k = 3$ thì in ca vì ca < ga.
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, k;
    cin >> n >> k;
    map<string, int> cnt;
    for(int i = 0; i < n; i++){ string w; cin >> w; cnt[w]++; }
    vector<pair<string, int>> v(cnt.begin(), cnt.end());
    sort(v.begin(), v.end(), [](const pair<string, int> &a, const pair<string, int> &b){
        if(a.second != b.second) return a.second > b.second;   // nhiều lần hơn đứng trước
        return a.first < b.first;                               // bằng nhau: theo từ điển
    });
    for(int i = 0; i < k && i < (int)v.size(); i++) cout << v[i].first << " " << v[i].second << '\n';
    return 0;
}
import sys
from collections import Counter

def main():
    data = sys.stdin.buffer.read().split()
    n, k = int(data[0]), int(data[1])
    cnt = Counter(data[2:2 + n])
    best = sorted(cnt.items(), key=lambda t: (-t[1], t[0]))[:k]
    sys.stdout.write('\n'.join(w.decode() + ' ' + str(c) for w, c in best) + '\n')

main()
Bài 5Đếm số không vượt quá xTrung bình

Cho dãy $n$ số nguyên ($n \le 2 \cdot 10^5$, $|a_i| \le 10^9$) và $q$ câu hỏi ($q \le 2 \cdot 10^5$), mỗi câu cho một số $x$. Với mỗi câu, in số phần tử của dãy không vượt quá $x$.

Dòng đầu: $n$, $q$. Dòng hai: dãy $a$. Dòng ba: $q$ số $x$.

Input
5 4
7 2 9 2 5
2 6 1 100
Output
2
3
0
5

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

💡 Gợi ý
Đếm từng câu bằng vòng lặp là $O(nq) = 4 \cdot 10^{10}$. Sắp xếp dãy một lần, khi đó số phần tử $\le x$ chính là vị trí đầu tiên có giá trị $> x$: bisect_right(a, x) trong Python, upper_bound(a, a + n, x) - a trong C++ (xem lại bài 16). Mẫu: dãy sắp xếp là 2 2 5 7 9; có 2 số $\le 2$, 3 số $\le 6$.
Xem code tham khảo (C++ / Python)
#include <bits/stdc++.h>
using namespace std;

int a[200005];

int main(){
    ios::sync_with_stdio(false); cin.tie(nullptr);
    int n, q;
    cin >> n >> q;
    for(int i = 0; i < n; i++) cin >> a[i];
    sort(a, a + n);
    while(q--){
        int x;
        cin >> x;
        cout << upper_bound(a, a + n, x) - a << '\n';
    }
    return 0;
}
import sys
from bisect import bisect_right

def main():
    data = sys.stdin.buffer.read().split()
    n, q = int(data[0]), int(data[1])
    a = sorted(map(int, data[2:2 + n]))
    out = [bisect_right(a, x) for x in map(int, data[2 + n:2 + n + q])]
    sys.stdout.write('\n'.join(map(str, out)) + '\n')

main()
Bài 6Băng chuyền hai đầuTrung bình

Một băng chuyền ban đầu rỗng. Có $Q$ lệnh ($Q \le 2 \cdot 10^5$), mỗi lệnh thuộc một trong bốn loại:

  • 1 x: đặt kiện hàng mã $x$ vào cuối băng;
  • 2 x: đặt kiện hàng mã $x$ vào đầu băng;
  • 3: lấy kiện ở đầu băng ra và in mã của nó;
  • 4: lấy kiện ở cuối băng ra và in mã của nó.

Nếu băng rỗng khi gặp lệnh 3 hoặc 4 thì in RONG. Sau khi làm hết, in thêm một dòng là số kiện còn lại trên băng.

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

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

💡 Gợi ý
Đúng cấu trúc deque: thêm, bớt ở cả hai đầu đều $O(1)$. Nếu dùng list và insert(0, x), pop(0) thì mỗi lệnh ở đầu tốn $O(n)$, $2 \cdot 10^5$ lệnh có thể mất vài giây. Mẫu: sau ba lệnh đặt, băng là 3 5 7; lấy đầu được 3, lấy cuối được 7, lấy đầu được 5; còn 0 kiện.
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 Q;
    cin >> Q;
    deque<long long> d;
    while(Q--){
        int t;
        cin >> t;
        if(t == 1){ long long x; cin >> x; d.push_back(x); }
        else if(t == 2){ long long x; cin >> x; d.push_front(x); }
        else if(d.empty()) cout << "RONG\n";
        else if(t == 3){ cout << d.front() << '\n'; d.pop_front(); }
        else { cout << d.back() << '\n'; d.pop_back(); }
    }
    cout << d.size() << '\n';
    return 0;
}
import sys
from collections import deque

def main():
    data = sys.stdin.buffer.read().split()
    Q = int(data[0])
    d = deque()
    out = []
    pos = 1
    for _ in range(Q):
        t = data[pos]; pos += 1
        if t == b'1':
            d.append(data[pos]); pos += 1        # giữ mã ở dạng bytes, in ra mới decode
        elif t == b'2':
            d.appendleft(data[pos]); pos += 1
        elif not d:
            out.append('RONG')
        elif t == b'3':
            out.append(d.popleft().decode())
        else:
            out.append(d.pop().decode())
    out.append(str(len(d)))
    sys.stdout.write('\n'.join(out) + '\n')

main()
Bài 7Tổng hình chữ nhậtTrung bình

Cho bảng $n \times m$ số nguyên không âm ($n, m \le 1000$, mỗi số $\le 10^9$) và $q$ câu hỏi ($q \le 10^5$). Mỗi câu hỏi gồm $r_1, c_1, r_2, c_2$ ($1 \le r_1 \le r_2 \le n$, $1 \le c_1 \le c_2 \le m$): in tổng các số trong hình chữ nhật có góc trên trái $(r_1, c_1)$ và góc dưới phải $(r_2, c_2)$.

Input
3 4 3
1 2 3 4
5 6 7 8
9 10 11 12
1 1 2 2
2 2 3 4
3 1 3 1
Output
14
54
9

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

💡 Gợi ý
Cộng dồn 2 chiều: $P[i][j]$ là tổng hình chữ nhật từ $(1, 1)$ tới $(i, j)$, khi đó đáp án là $P[r_2][c_2] - P[r_1 - 1][c_2] - P[r_2][c_1 - 1] + P[r_1 - 1][c_1 - 1]$. Với Python có hai chỗ cần cẩn thận: tạo bảng 2 chiều đúng cách (không dùng [[0] * (m + 1)] * (n + 1)), và dựng $P$ không bằng hai vòng lặp $10^6$ bước thuần mà theo từng hàng: hàng $i$ của $P$ = hàng $i - 1$ của $P$ cộng với accumulate của hàng $i$ trong bảng, ghép bằng zip. Tổng có thể tới $10^{15}$ nên C++ dùng long long. Mẫu: $1 + 2 + 5 + 6 = 14$.
Xem code tham khảo (C++ / Python)
#include <bits/stdc++.h>
using namespace std;

long long P[1005][1005];

int main(){
    ios::sync_with_stdio(false); cin.tie(nullptr);
    int n, m, q;
    cin >> n >> m >> q;
    for(int i = 1; i <= n; i++)
        for(int j = 1; j <= m; j++){
            long long x;
            cin >> x;
            P[i][j] = P[i - 1][j] + P[i][j - 1] - P[i - 1][j - 1] + x;
        }
    while(q--){
        int r1, c1, r2, c2;
        cin >> r1 >> c1 >> r2 >> c2;
        cout << P[r2][c2] - P[r1 - 1][c2] - P[r2][c1 - 1] + P[r1 - 1][c1 - 1] << '\n';
    }
    return 0;
}
import sys
from itertools import accumulate

def main():
    data = sys.stdin.buffer.read().split()
    n, m, q = int(data[0]), int(data[1]), int(data[2])
    P = [[0] * (m + 1)]                       # hàng 0 toàn số 0, mỗi hàng là một list RIÊNG
    pos = 3
    for _ in range(n):
        row = accumulate(map(int, data[pos:pos + m]))     # cộng dồn trên hàng hiện tại
        pos += m
        up = P[-1]
        P.append([0] + [x + y for x, y in zip(up[1:], row)])
    out = []
    for _ in range(q):
        r1, c1, r2, c2 = map(int, data[pos:pos + 4])
        pos += 4
        out.append(P[r2][c2] - P[r1 - 1][c2] - P[r2][c1 - 1] + P[r1 - 1][c1 - 1])
    sys.stdout.write('\n'.join(map(str, out)) + '\n')

main()
Bài 8Số cấp dướiKhá

Công ty có $n$ người ($n \le 2 \cdot 10^5$), người $i$ có sếp trực tiếp $p_i$ ($p_i = 0$ nếu người đó không có sếp; có thể có nhiều người như vậy; đảm bảo không có vòng tròn). Cấp dưới của một người gồm cấp dưới trực tiếp, cấp dưới của cấp dưới, … In số cấp dưới của từng người $1, 2, \ldots, n$ trên một dòng.

Input
6
0 1 1 2 2 0
Output
4 2 0 0 0 0

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

💡 Gợi ý
Số cấp dưới của $u$ bằng tổng (số cấp dưới + 1) của các cấp dưới trực tiếp. Muốn cộng được cho $u$ thì phải xong hết những người dưới $u$ trước. Không dùng đệ quy (chuỗi sếp có thể dài $2 \cdot 10^5$): tính cấp của mọi người bằng ngăn xếp tự quản ở phần 4, rồi xét người theo cấp giảm dần, mỗi người $v$ có sếp thì cộng sub[p[v]] += sub[v] + 1. Khi xét $v$, mọi người dưới $v$ có cấp lớn hơn nên đã được xét xong. Mẫu: người 1 có cấp dưới 2, 3, 4, 5.
Xem code tham khảo (C++ / Python)
#include <bits/stdc++.h>
using namespace std;

int n, p[200005], lv[200005];
long long sub[200005];

int main(){
    ios::sync_with_stdio(false); cin.tie(nullptr);
    cin >> n;
    for(int i = 1; i <= n; i++) cin >> p[i];
    vector<int> st;
    for(int i = 1; i <= n; i++){                     // tính cấp bằng ngăn xếp tự quản
        int u = i;
        while(u != 0 && lv[u] == 0){ st.push_back(u); u = p[u]; }
        while(!st.empty()){
            int v = st.back(); st.pop_back();
            lv[v] = (p[v] == 0) ? 1 : lv[p[v]] + 1;
        }
    }
    vector<int> order(n);
    iota(order.begin(), order.end(), 1);
    sort(order.begin(), order.end(), [](int a, int b){ return lv[a] > lv[b]; });   // cấp sâu trước
    for(int v : order)
        if(p[v] != 0) sub[p[v]] += sub[v] + 1;
    for(int i = 1; i <= n; i++) cout << sub[i] << " ";
    return 0;
}
import sys

def main():
    data = sys.stdin.buffer.read().split()
    n = int(data[0])
    p = [0] + [int(x) for x in data[1:n + 1]]
    lv = [0] * (n + 1)
    st = []
    for i in range(1, n + 1):                        # tính cấp bằng ngăn xếp tự quản
        u = i
        while u != 0 and lv[u] == 0:
            st.append(u)
            u = p[u]
        while st:
            v = st.pop()
            lv[v] = 1 if p[v] == 0 else lv[p[v]] + 1
    sub = [0] * (n + 1)
    for v in sorted(range(1, n + 1), key=lv.__getitem__, reverse=True):   # cấp sâu trước
        if p[v]:
            sub[p[v]] += sub[v] + 1
    sys.stdout.write(' '.join(map(str, sub[1:])) + '\n')

main()
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