C++ cho THPT HSG
Tổng hợp kiến thức lập trình thi đấu cho học sinh THPT.
Template cơ bản
Khung code chuẩn cho mọi bài thi đấu. Dùng macro để gõ nhanh.
Template đầy đủ
#include <bits/stdc++.h>
using namespace std;
#define ll long long
#define ull unsigned long long
#define ld long double
#define fi first
#define se second
#define pb push_back
#define mp make_pair
#define all(x) (x).begin(), (x).end()
#define sz(x) (int)(x).size()
#define FOR(i,a,b) for(int i=(a);i<=(b);i++)
#define FORD(i,a,b) for(int i=(a);i>=(b);i--)
#define REP(i,n) for(int i=0;i<(n);i++)
typedef pair<int,int> pii;
typedef pair<ll,ll> pll;
typedef vector<int> vi;
typedef vector<ll> vll;
typedef vector<vi> vvi;
const int MOD = 1e9 + 7;
const int INF = 1e9;
const ll LINF = 1e18;
const double EPS = 1e-9;
const double PI = acos(-1.0);
template<typename T> bool ckmin(T& a, const T& b){return a>b?a=b,true:false;}
template<typename T> bool ckmax(T& a, const T& b){return a<b?a=b,true:false;}
int main(){
ios_base::sync_with_stdio(false);
cin.tie(nullptr);
// Code ở đây
return 0;
}
Template tối giản
#include <bits/stdc++.h>
using namespace std;
#define ll long long
int main(){
ios_base::sync_with_stdio(false);
cin.tie(nullptr);
// Code
return 0;
}
bits/stdc++.h, phải include từng thư viện cần dùng.Fast I/O
Với input lớn (>10^6 dòng) phải dùng fast I/O để tránh TLE.
Cách 1: Tắt đồng bộ (phổ biến nhất)
ios_base::sync_with_stdio(false);
cin.tie(nullptr);
// Sau đó dùng cin/cout bình thường nhưng nhanh hơn 5-10 lần
Cách 2: scanf / printf
int x; scanf("%d", &x);
ll y; scanf("%lld", &y);
double z; scanf("%lf", &z);
char c; scanf(" %c", &c); // dấu cách để bỏ whitespace
printf("%d %lld %.2lf\n", x, y, z);
Cách 3: Đọc nhanh bằng getchar
inline int readInt(){
int x=0, sign=1;
char c=getchar();
while(c<'0'||c>'9'){if(c=='-')sign=-1;c=getchar();}
while(c>='0'&&c<='9'){x=x*10+(c-'0');c=getchar();}
return x*sign;
}
inline ll readLL(){
ll x=0, sign=1;
char c=getchar();
while(c<'0'||c>'9'){if(c=='-')sign=-1;c=getchar();}
while(c>='0'&&c<='9'){x=x*10+(c-'0');c=getchar();}
return x*sign;
}
// Đọc nhiều số cùng lúc
inline void read(int &x){ x = readInt(); }
inline void read(ll &x){ x = readLL(); }
template<typename T, typename... Args>
void read(T& a, Args&... args){ read(a); read(args...); }
Cách 4: Đọc cả dòng bằng getline
string s;
getline(cin, s); // Đọc cả dòng kể cả dấu cách
// Nếu trước đó có cin >> x, cần cin.ignore() để bỏ ký tự newline
cin.ignore();
getline(cin, s);
Kiểu dữ liệu
Chọn đúng kiểu để tránh tràn số — lỗi phổ biến nhất khi làm bài.
Bảng so sánh
| Kiểu | Size | Giới hạn | Format |
|---|---|---|---|
| char | 1 byte | -128 → 127 | %c |
| bool | 1 byte | 0/1 | %d |
| short | 2 byte | ±3.2 × 10^4 | %hd |
| int | 4 byte | ±2.1 × 10^9 | %d |
| unsigned int | 4 byte | 0 → 4.3 × 10^9 | %u |
| long | 4/8 byte | tùy hệ thống | %ld |
| long long | 8 byte | ±9.2 × 10^18 | %lld |
| unsigned long long | 8 byte | 0 → 1.8 × 10^19 | %llu |
| __int128 | 16 byte | ±1.7 × 10^38 | custom |
| float | 4 byte | ~7 chữ số | %f |
| double | 8 byte | ~15 chữ số | %lf |
| long double | 16 byte | ~18 chữ số | %Lf |
Giá trị giới hạn
INT_MAX = 2147483647
INT_MIN = -2147483648
UINT_MAX = 4294967295
LLONG_MAX = 9223372036854775807
LLONG_MIN = -9223372036854775808
ULLONG_MAX = 18446744073709551615
CHAR_MAX = 127
CHAR_MIN = -128
Ứng dụng __int128
__int128 a = (__int128)x * y % MOD;
// In __int128
void printInt128(__int128 n){
if(n==0){cout<<'0';return;}
if(n<0){cout<<'-';n=-n;}
string s;
while(n>0){s+=(n%10)+'0';n/=10;}
reverse(all(s));
cout<<s;
}
int ~10^5 đã vượt giới hạn int (10^10). Luôn dùng long long khi nhân. VD: a, b ≤ 10^9 thì a*b ≤ 10^18 — vừa long long, KHÔNG vừa int.Độ phức tạp thuật toán
Độ phức tạp là thước đo hiệu năng — quyết định code có chạy kịp trong giới hạn thời gian hay không.
Bảng so sánh chi tiết
| Ký hiệu | Tên | n=10 | n=10^3 | n=10^5 | n=10^6 | n=10^7 |
|---|---|---|---|---|---|---|
| O(1) | Hằng số | 1 | 1 | 1 | 1 | 1 |
| O(log n) | Logarit | 3 | 10 | 17 | 20 | 23 |
| O(√n) | Căn | 3 | 32 | 316 | 1000 | 3162 |
| O(n) | Tuyến tính | 10 | 10^3 | 10^5 | 10^6 | 10^7 |
| O(n log n) | Log-tuyến | 30 | 10^4 | 1.7M | 20M | 230M |
| O(n²) | Bình phương | 100 | 10^6 | 10^10 | -- | -- |
| O(n³) | Lập phương | 1000 | 10^9 | -- | -- | -- |
| O(2ⁿ) | Hàm mũ | 1024 | -- | -- | -- | -- |
| O(n!) | Giai thừa | 3.6M | -- | -- | -- | -- |
Chọn thuật toán theo ràng buộc
- n ≤ 10 → O(n!), O(2^n), O(n^5)
- n ≤ 20 → O(2^n), O(n · 2^n)
- n ≤ 100 → O(n^4)
- n ≤ 500 → O(n³)
- n ≤ 5000 → O(n²)
- n ≤ 10^5 → O(n log n), O(n√n)
- n ≤ 10^6 → O(n log n), O(n)
- n ≤ 10^7 → O(n)
- n ≤ 10^9 → O(√n), O(log n)
- n ≤ 10^18 → O(log n), O(1)
Quy tắc tính
- Cộng: O(f(n)) + O(g(n)) = O(max(f(n), g(n)))
- Nhân: O(f(n)) × O(g(n)) = O(f(n) × g(n))
- Bỏ hằng số: O(5n) = O(n), O(2n² + 3n + 5) = O(n²)
- Chỉ giữ bậc cao nhất: O(n³ + n² + n) = O(n³)
Ví dụ tính độ phức tạp
// O(1) — hằng số
int x = a[5];
int sum = a[0] + a[1] + a[2];
// O(log n) — chia đôi mỗi lần
while (n > 0) n /= 2;
// O(√n) — duyệt đến căn
for (int i = 1; i * i <= n; i++) { }
// O(n) — 1 vòng lặp
for (int i = 0; i < n; i++) sum += a[i];
// O(n²) — 2 vòng lặp lồng
for (int i = 0; i < n; i++)
for (int j = 0; j < n; j++) cnt++;
// O(n log n) — sort, merge sort
sort(a.begin(), a.end());
// O(2^n) — duyệt tập con
for (int mask = 0; mask < (1 << n); mask++) { }
// O(n!) — hoán vị
do { /* ... */ } while (next_permutation(a.begin(), a.end()));
Đệ quy
Hàm tự gọi chính nó với input nhỏ hơn, dừng khi đạt điều kiện dừng (base case).
Cấu trúc
kieu_tra_ve ten_ham(tham_so){
if (dieu_kien_dung) return gia_tri_co_so;
return ket_hop(ten_ham(tham_so_nho_hon));
}
Giai thừa
ll fact(int n){
if (n <= 1) return 1;
return n * fact(n - 1);
}
// n=20 vừa long long (2.4×10^18)
Fibonacci đệ quy ngây thơ O(2^n)
ll fib(int n){
if (n <= 1) return n;
return fib(n-1) + fib(n-2);
}
// n=50 chạy mất hàng nghìn năm!
Fibonacci memo O(n)
ll memo[100];
bool done[100];
ll fibMemo(int n){
if (n <= 1) return n;
if (done[n]) return memo[n];
done[n] = true;
return memo[n] = fibMemo(n-1) + fibMemo(n-2);
}
Đệ quy trên mảng
// Tính tổng mảng
ll sumArr(vector<int>& a, int i){
if (i == a.size()) return 0;
return a[i] + sumArr(a, i+1);
}
// Tìm max
int findMax(vector<int>& a, int i){
if (i == a.size() - 1) return a[i];
return max(a[i], findMax(a, i+1));
}
// Tìm min
int findMin(vector<int>& a, int i){
if (i == a.size() - 1) return a[i];
return min(a[i], findMin(a, i+1));
}
// Đảo mảng
void reverseArr(vector<int>& a, int l, int r){
if (l >= r) return;
swap(a[l], a[r]);
reverseArr(a, l+1, r-1);
}
// Tìm kiếm tuyến tính
int linearSearch(vector<int>& a, int i, int x){
if (i == a.size()) return -1;
if (a[i] == x) return i;
return linearSearch(a, i+1, x);
}
// Đếm số chẵn
int countEven(vector<int>& a, int i){
if (i == a.size()) return 0;
return (a[i] % 2 == 0) + countEven(a, i+1);
}
Đệ quy trên chuỗi
// Đảo chuỗi
string reverseStr(string s){
if (s.size() <= 1) return s;
return reverseStr(s.substr(1)) + s[0];
}
// Đếm ký tự
int countChar(string& s, int i, char c){
if (i == s.size()) return 0;
return (s[i] == c) + countChar(s, i+1, c);
}
// In chuỗi ngược
void printReverse(string& s, int i){
if (i < 0) return;
cout << s[i];
printReverse(s, i-1);
}
// Kiểm tra palindrome
bool isPal(string& s, int l, int r){
if (l >= r) return true;
if (s[l] != s[r]) return false;
return isPal(s, l+1, r-1);
}
// Chuyển thập phân sang nhị phân
void toBinary(int n){
if (n == 0) return;
toBinary(n/2);
cout << n % 2;
}
// Chuyển thập phân sang bất kỳ cơ số
string toBase(int n, int base){
if (n == 0) return "";
string digits = "0123456789ABCDEF";
return toBase(n/base, base) + digits[n%base];
}
Tháp Hà Nội
void hanoi(int n, char a, char b, char c){
if (n == 1){
cout << "Chuyen dia 1 tu " << a << " sang " << c << "\n";
return;
}
hanoi(n-1, a, c, b);
cout << "Chuyen dia " << n << " tu " << a << " sang " << c << "\n";
hanoi(n-1, b, a, c);
}
// Số bước = 2^n - 1
Đếm số bước đệ quy
ll T(int n){
if (n <= 1) return 1;
return T(n-1) + T(n-2) + 1;
}
Bài tập tự luyện
- Tính 1 + 2 + ... + n
- Tính a^n bằng đệ quy
- Đếm số chữ số của n
- Tính USCLN bằng Euclid
- Tìm phần tử lớn thứ k trong mảng
- Sinh tất cả xâu nhị phân độ dài n
- Đếm số cách leo cầu thang với bước 1, 2, 3
Quay lui (Backtracking)
Thử từng khả năng, nếu sai thì quay lại (undo) thử khả năng khác. Bản chất là đệ quy có "hoàn tác trạng thái".
Template chung
void backtrack(int i){
if (i == n){
// Xử lý kết quả đầy đủ
return;
}
for (int choice : possible_choices){
if (valid(choice)){
make_choice(choice); // Đánh dấu đã chọn
backtrack(i + 1); // Đệ quy
undo_choice(choice); // Bỏ đánh dấu
}
}
}
Sinh hoán vị 1..n
int n;
vector<int> perm;
vector<bool> used;
void genPerm(){
if (perm.size() == n){
for (int x : perm) cout << x << " ";
cout << "\n";
return;
}
for (int i = 1; i <= n; i++){
if (used[i]) continue;
used[i] = true;
perm.pb(i);
genPerm();
perm.pop_back();
used[i] = false;
}
}
int main(){
cin >> n;
used.assign(n + 1, false);
genPerm();
}
Sinh tập con
int n;
vector<int> a, cur;
void genSubset(int i){
if (i == n){
for (int x : cur) cout << x << " ";
cout << "\n";
return;
}
// Không chọn a[i]
genSubset(i + 1);
// Chọn a[i]
cur.pb(a[i]);
genSubset(i + 1);
cur.pop_back();
}
Sinh tổ hợp chập k
int n, k;
vector<int> cur;
void genComb(int start){
if (cur.size() == k){
for (int x : cur) cout << x << " ";
cout << "\n";
return;
}
for (int i = start; i <= n; i++){
cur.pb(i);
genComb(i + 1);
cur.pop_back();
}
}
Sinh xâu nhị phân độ dài n
int n;
string s;
void genBinary(int i){
if (i == n){
cout << s << "\n";
return;
}
s += '0'; genBinary(i + 1); s.pop_back();
s += '1'; genBinary(i + 1); s.pop_back();
}
Sinh xâu nhị phân không có 2 số 1 liền kề
int n;
string s;
void gen(int i, bool prevOne){
if (i == n){
cout << s << "\n";
return;
}
s += '0'; gen(i+1, false); s.pop_back();
if (!prevOne){
s += '1'; gen(i+1, true); s.pop_back();
}
}
Bài N-Queens
int n, cnt = 0;
vector<bool> col, diag1, diag2;
void queens(int row){
if (row == n){ cnt++; return; }
for (int c = 0; c < n; c++){
if (col[c] || diag1[row + c] || diag2[row - c + n]) continue;
col[c] = diag1[row + c] = diag2[row - c + n] = true;
queens(row + 1);
col[c] = diag1[row + c] = diag2[row - c + n] = false;
}
}
int main(){
cin >> n;
col.assign(n, false);
diag1.assign(2*n, false);
diag2.assign(2*n, false);
queens(0);
cout << cnt;
}
Mã đi tuần (Knight's Tour)
int n, a[10][10];
int dx[] = {-2,-2,-1,-1, 1, 1, 2, 2};
int dy[] = {-1, 1,-2, 2,-2, 2,-1, 1};
bool found = false;
void knight(int x, int y, int step){
a[x][y] = step;
if (step == n * n){ found = true; return; }
for (int d = 0; d < 8; d++){
int nx = x + dx[d], ny = y + dy[d];
if (nx < 0 || nx >= n || ny < 0 || ny >= n) continue;
if (a[nx][ny] != 0) continue;
knight(nx, ny, step + 1);
if (found) return;
}
a[x][y] = 0;
}
Sudoku Solver
int a[9][9];
bool valid(int r, int c, int v){
for (int i = 0; i < 9; i++){
if (a[r][i] == v || a[i][c] == v) return false;
}
int br = r/3*3, bc = c/3*3;
for (int i = 0; i < 3; i++)
for (int j = 0; j < 3; j++)
if (a[br+i][bc+j] == v) return false;
return true;
}
bool solve(){
for (int r = 0; r < 9; r++)
for (int c = 0; c < 9; c++){
if (a[r][c] != 0) continue;
for (int v = 1; v <= 9; v++)
if (valid(r, c, v)){
a[r][c] = v;
if (solve()) return true;
a[r][c] = 0;
}
return false;
}
return true;
}
Bài toán chia kẹo
// Chia n kẹo cho k bạn, mỗi bạn ít nhất 1
// Số cách = C(n-1, k-1)
// Sinh các cách chia
int n, k;
vector<int> cur;
void gen(int remain, int idx){
if (idx == k){
if (remain == 0){
for (int x : cur) cout << x << " ";
cout << "\n";
}
return;
}
for (int i = 1; i <= remain; i++){
cur.pb(i);
gen(remain - i, idx + 1);
cur.pop_back();
}
}
Tham lam (Greedy)
Mỗi bước chọn phương án tốt nhất hiện tại, hy vọng đạt tối ưu toàn cục.
Đổi tiền (mệnh giá canonical)
int coinChange(vector<int>& coins, int X){
sort(coins.rbegin(), coins.rend());
int cnt = 0;
for (int c : coins){
cnt += X / c;
X %= c;
}
return X == 0 ? cnt : -1;
}
// Ví dụ SAI: coins={1, 3, 4}, X=6
// Greedy: 4+1+1 = 3 đồng
// Tối ưu: 3+3 = 2 đồng
Chọn hoạt động không chồng lấn
struct Act{ int s, e; };
int maxActivities(vector<Act>& a){
sort(all(a), [](Act x, Act y){ return x.e < y.e; });
int cnt = 1, lastEnd = a[0].e;
for (int i = 1; i < a.size(); i++)
if (a[i].s >= lastEnd){
cnt++;
lastEnd = a[i].e;
}
return cnt;
}
Ba lô phân số
struct Item{ int w, v; };
double fracKnapsack(vector<Item>& items, int W){
sort(all(items), [](Item a, Item b){
return (double)a.v / a.w > (double)b.v / b.w;
});
double total = 0;
for (auto& it : items){
if (W >= it.w){ total += it.v; W -= it.w; }
else { total += (double)it.v * W / it.w; break; }
}
return total;
}
Mã Huffman
ll huffman(vector<int>& freq){
priority_queue<ll, vector<ll>, greater<ll>> pq;
for (int f : freq) pq.push(f);
ll total = 0;
while (pq.size() > 1){
ll a = pq.top(); pq.pop();
ll b = pq.top(); pq.pop();
total += a + b;
pq.push(a + b);
}
return total;
}
Số phòng họp tối thiểu
int minRooms(vector<pii>& meetings){
sort(all(meetings));
priority_queue<int, vector<int>, greater<int>> pq;
for (auto [s, e] : meetings){
if (!pq.empty() && pq.top() <= s) pq.pop();
pq.push(e);
}
return pq.size();
}
Chia kẹo cho trẻ em
// Mỗi đứa có mức độ tham lam g[i], kẹo có kích thước s[j]
// Đứa i hài lòng nếu nhận kẹo s[j] >= g[i]
// Tối đa số đứa hài lòng
int findContentChildren(vector<int>& g, vector<int>& s){
sort(all(g));
sort(all(s));
int i = 0, j = 0;
while (i < g.size() && j < s.size()){
if (s[j] >= g[i]) i++;
j++;
}
return i;
}
Gas Station
// Tìm trạm xăng xuất phát để đi hết vòng
int canCompleteCircuit(vector<int>& gas, vector<int>& cost){
int total = 0, cur = 0, start = 0;
for (int i = 0; i < gas.size(); i++){
int diff = gas[i] - cost[i];
total += diff;
cur += diff;
if (cur < 0){ start = i + 1; cur = 0; }
}
return total >= 0 ? start : -1;
}
Bài tập tự luyện
- Chia kẹo cho trẻ em (greedy + sort)
- Tìm số lần ít nhất để gộp đống đá
- Bài toán trạm xăng (gas station)
- Trả tiền ít đồng xu nhất với mệnh giá VND
- Sắp lịch diễn cho ca sĩ
- Nối dây cáp với chi phí nhỏ nhất
Chia để trị
Chia bài toán lớn thành các bài con nhỏ hơn, giải từng bài rồi gộp lại.
Cấu trúc chung
solve(problem){
if (problem is small) return solve_directly(problem);
split problem into subproblems;
for each subproblem: solve(subproblem);
return merge(results);
}
Merge Sort — O(n log n)
void merge(vector<int>& a, int l, int m, int r){
vector<int> L(a.begin() + l, a.begin() + m + 1);
vector<int> R(a.begin() + m + 1, a.begin() + r + 1);
int i = 0, j = 0, k = l;
while (i < L.size() && j < R.size())
a[k++] = (L[i] <= R[j]) ? L[i++] : R[j++];
while (i < L.size()) a[k++] = L[i++];
while (j < R.size()) a[k++] = R[j++];
}
void mergeSort(vector<int>& a, int l, int r){
if (l >= r) return;
int m = (l + r) / 2;
mergeSort(a, l, m);
mergeSort(a, m + 1, r);
merge(a, l, m, r);
}
Quick Sort — O(n log n) trung bình
int partition(vector<int>& a, int lo, int hi){
int pivot = a[hi], i = lo - 1;
for (int j = lo; j < hi; j++)
if (a[j] <= pivot) swap(a[++i], a[j]);
swap(a[i + 1], a[hi]);
return i + 1;
}
void quickSort(vector<int>& a, int lo, int hi){
if (lo >= hi) return;
int p = partition(a, lo, hi);
quickSort(a, lo, p - 1);
quickSort(a, p + 1, hi);
}
QuickSelect — phần tử lớn thứ k
int quickSelect(vector<int>& a, int l, int r, int k){
if (l == r) return a[l];
int p = partition(a, l, r);
int cntLeft = p - l + 1;
if (k == cntLeft) return a[p];
if (k < cntLeft) return quickSelect(a, l, p - 1, k);
return quickSelect(a, p + 1, r, k - cntLeft);
}
// O(n) trung bình, O(n²) tệ nhất
Đếm số nghịch thế (Inversions)
ll mergeCount(vector<int>& a, int l, int m, int r){
vector<int> L(a.begin() + l, a.begin() + m + 1);
vector<int> R(a.begin() + m + 1, a.begin() + r + 1);
ll cnt = 0;
int i = 0, j = 0, k = l;
while (i < L.size() && j < R.size()){
if (L[i] <= R[j]) a[k++] = L[i++];
else {
cnt += L.size() - i;
a[k++] = R[j++];
}
}
while (i < L.size()) a[k++] = L[i++];
while (j < R.size()) a[k++] = R[j++];
return cnt;
}
ll countInv(vector<int>& a, int l, int r){
if (l >= r) return 0;
int m = (l + r) / 2;
ll cnt = countInv(a, l, m) + countInv(a, m + 1, r);
cnt += mergeCount(a, l, m, r);
return cnt;
}
Lũy thừa nhanh bằng chia để trị
ll power(ll a, ll n){
if (n == 0) return 1;
if (n % 2 == 0){
ll h = power(a, n / 2);
return h * h;
}
return a * power(a, n - 1);
}
Binary Search chia để trị
int binarySearch(vector<int>& a, int l, int r, int x){
if (l > r) return -1;
int m = (l + r) / 2;
if (a[m] == x) return m;
if (a[m] < x) return binarySearch(a, m + 1, r, x);
return binarySearch(a, l, m - 1, x);
}
Tìm cặp điểm gần nhất O(n log n)
struct pt{ double x, y; };
double dist(pt a, pt b){
return hypot(a.x - b.x, a.y - b.y);
}
double closestPair(vector<pt>& p, int l, int r){
if (r - l <= 3){
double mn = 1e18;
for (int i = l; i < r; i++)
for (int j = i + 1; j < r; j++)
mn = min(mn, dist(p[i], p[j]));
return mn;
}
int m = (l + r) / 2;
double midX = p[m].x;
double dl = closestPair(p, l, m);
double dr = closestPair(p, m, r);
double d = min(dl, dr);
vector<pt> strip;
for (int i = l; i < r; i++)
if (abs(p[i].x - midX) < d) strip.pb(p[i]);
sort(all(strip), [](pt a, pt b){ return a.y < b.y; });
for (int i = 0; i < strip.size(); i++)
for (int j = i + 1; j < strip.size() && strip[j].y - strip[i].y < d; j++)
d = min(d, dist(strip[i], strip[j]));
return d;
}
Bài toán nhân 2 số lớn (Karatsuba)
// Độ phức tạp O(n^1.585) thay vì O(n²)
// Karatsuba chia số thành 2 nửa
ll karatsuba(ll x, ll y){
if (x < 10 || y < 10) return x * y;
int n = max(to_string(x).size(), to_string(y).size());
int half = n / 2;
ll p = pow(10, half);
ll a = x / p, b = x % p;
ll c = y / p, d = y % p;
ll ac = karatsuba(a, c);
ll bd = karatsuba(b, d);
ll adbc = karatsuba(a + b, c + d) - ac - bd;
return ac * p * p + adbc * p + bd;
}
Big Integer
Khi số quá lớn vượt giới hạn long long, phải lưu dưới dạng chuỗi và tự mô phỏng phép tính.
Cộng hai số lớn
string addStr(string u, string v){
string s = "";
int tmp = 0;
int i = u.size() - 1, j = v.size() - 1;
while (i >= 0 || j >= 0 || tmp > 0){
int sum = tmp;
if (i >= 0) sum += (u[i--] - '0');
if (j >= 0) sum += (v[j--] - '0');
tmp = sum / 10;
s.pb((sum % 10) + '0');
}
reverse(all(s));
return s;
}
Trừ hai số lớn (u >= v)
string subStr(string u, string v){
string s = "";
int tmp = 0;
int i = u.size() - 1, j = v.size() - 1;
while (i >= 0){
int sub = (u[i--] - '0') - tmp;
if (j >= 0) sub -= (v[j--] - '0');
if (sub < 0){ sub += 10; tmp = 1; }
else tmp = 0;
s.pb(sub + '0');
}
while (s.size() > 1 && s.back() == '0') s.pop_back();
reverse(all(s));
return s;
}
So sánh hai số lớn
// Trả về -1 nếu a < b, 0 nếu bằng, 1 nếu a > b
int cmpStr(string a, string b){
while (a.size() < b.size()) a = "0" + a;
while (b.size() < a.size()) b = "0" + b;
if (a < b) return -1;
if (a > b) return 1;
return 0;
}
Nhân với số nhỏ
string mulStr(string u, int k){
string s = "";
int tmp = 0;
for (int i = u.size() - 1; i >= 0; i--){
int d = (u[i] - '0') * k + tmp;
tmp = d / 10;
s.pb((d % 10) + '0');
}
while (tmp > 0){
s.pb((tmp % 10) + '0');
tmp /= 10;
}
reverse(all(s));
return s;
}
Nhân hai số lớn — O(n·m)
string mulStr2(string a, string b){
int n = a.size(), m = b.size();
vector<int> res(n + m, 0);
for (int i = n - 1; i >= 0; i--)
for (int j = m - 1; j >= 0; j--){
int mul = (a[i] - '0') * (b[j] - '0');
int p1 = i + j, p2 = i + j + 1;
int sum = mul + res[p2];
res[p2] = sum % 10;
res[p1] += sum / 10;
}
string s = "";
for (int x : res) if (!(s.empty() && x == 0)) s += to_string(x);
return s.empty() ? "0" : s;
}
Chia số lớn cho số nhỏ
pair<string,int> divStr(string u, int k){
string q = "";
int du = 0;
for (char c : u){
int cur = du * 10 + (c - '0');
q += (cur / k) + '0';
du = cur % k;
}
int i = 0;
while (i < q.size() - 1 && q[i] == '0') i++;
return {q.substr(i), du};
}
Giai thừa số lớn
string factorial(int n){
string res = "1";
for (int i = 2; i <= n; i++)
res = mulStr(res, i);
return res;
}
GCD - LCM
Ước chung lớn nhất (GCD) và bội chung nhỏ nhất (LCM) — hai hàm cơ bản dùng suốt.
Thuật toán Euclid
ll gcd(ll a, ll b){
return b ? gcd(b, a % b) : a;
}
// Vòng lặp
ll gcd2(ll a, ll b){
while (b){ ll t = a % b; a = b; b = t; }
return a;
}
LCM
ll lcm(ll a, ll b){
return a / gcd(a, b) * b; // Chia trước để tránh tràn
}
GCD nhiều số
ll gcdAll(vector<ll>& a){
ll g = a[0];
for (int i = 1; i < a.size(); i++) g = gcd(g, a[i]);
return g;
}
ll lcmAll(vector<ll>& a){
ll l = a[0];
for (int i = 1; i < a.size(); i++) l = lcm(l, a[i]);
return l;
}
Extended Euclid
Giải $ax + by = \gcd(a,b)$.
ll extgcd(ll a, ll b, ll& x, ll& y){
if (b == 0){ x = 1; y = 0; return a; }
ll x1, y1;
ll g = extgcd(b, a % b, x1, y1);
x = y1;
y = x1 - (a / b) * y1;
return g;
}
Chia hết / Đồng dư
// a chia hết cho b
bool divisible(ll a, ll b){ return a % b == 0; }
// a ≡ b (mod m) ⟺ (a - b) chia hết cho m
bool congruent(ll a, ll b, ll m){
return (a - b) % m == 0;
}
Số nguyên tố
Kiểm tra và phân tích thừa số nguyên tố.
Kiểm tra nguyên tố — O(√n)
bool isPrime(ll n){
if (n < 2) return false;
if (n < 4) return true;
if (n % 2 == 0 || n % 3 == 0) return false;
for (ll i = 5; i * i <= n; i += 6)
if (n % i == 0 || n % (i + 2) == 0) return false;
return true;
}
Phân tích thừa số nguyên tố
vector<pair<ll,int>> factorize(ll n){
vector<pair<ll,int>> res;
for (ll i = 2; i * i <= n; i++){
if (n % i == 0){
int c = 0;
while (n % i == 0){ n /= i; c++; }
res.pb({i, c});
}
}
if (n > 1) res.pb({n, 1});
return res;
}
Đếm ước số
ll countDivisors(ll n){
ll cnt = 0;
for (ll i = 1; i * i <= n; i++){
if (n % i == 0){
cnt++;
if (i != n / i) cnt++;
}
}
return cnt;
}
Tổng ước số
ll sumDivisors(ll n){
ll sum = 0;
for (ll i = 1; i * i <= n; i++){
if (n % i == 0){
sum += i;
if (i != n / i) sum += n / i;
}
}
return sum;
}
Liệt kê ước số
vector<ll> getDivisors(ll n){
vector<ll> small, big;
for (ll i = 1; i * i <= n; i++){
if (n % i == 0){
small.pb(i);
if (i != n / i) big.pb(n / i);
}
}
reverse(all(big));
for (ll x : big) small.pb(x);
return small;
}
Số hoàn hảo
bool isPerfect(ll n){
if (n < 2) return false;
ll s = 1;
for (ll i = 2; i * i <= n; i++)
if (n % i == 0){
s += i;
if (i != n / i) s += n / i;
}
return s == n;
}
// Số hoàn hảo: 6, 28, 496, 8128...
Miller-Rabin (kiểm tra nhanh với n 10^18)
ll mulmod(ll a, ll b, ll m){
return (__int128)a * b % m;
}
ll powmod(ll a, ll b, ll m){
ll r = 1; a %= m;
while (b){ if (b&1) r = mulmod(r,a,m); a = mulmod(a,a,m); b >>= 1; }
return r;
}
bool miller(ll n){
if (n < 2) return false;
for (ll p : {2,3,5,7,11,13,17,19,23,29,31,37}){
if (n == p) return true;
if (n % p == 0) return false;
}
ll d = n - 1, s = 0;
while (d % 2 == 0){ d /= 2; s++; }
for (ll a : {2,3,5,7,11,13,17,19,23,29,31,37}){
ll x = powmod(a, d, n);
if (x == 1 || x == n - 1) continue;
bool ok = false;
for (int r = 1; r < s; r++){
x = mulmod(x, x, n);
if (x == n - 1){ ok = true; break; }
}
if (!ok) return false;
}
return true;
}
Sàng nguyên tố
Đánh dấu tất cả số nguyên tố trong [1, n] một lần, truy vấn O(1).
Sàng Eratosthenes
const int N = 1e7 + 5;
vector<bool> isPrime(N, true);
void sieve(){
isPrime[0] = isPrime[1] = false;
for (int i = 2; i * i < N; i++)
if (isPrime[i])
for (int j = i * i; j < N; j += i)
isPrime[j] = false;
}
// O(n log log n)
Sàng SPF — thừa số nguyên tố nhỏ nhất
const int N = 1e7 + 5;
int spf[N];
void buildSPF(){
for (int i = 0; i < N; i++) spf[i] = i;
for (int i = 2; i * i < N; i++)
if (spf[i] == i)
for (int j = i * i; j < N; j += i)
if (spf[j] == j) spf[j] = i;
}
// Phân tích nhanh O(log n)
vector<int> factorize(int n){
vector<int> res;
while (n > 1){ res.pb(spf[n]); n /= spf[n]; }
return res;
}
Sàng đoạn [L, R]
vector<bool> segSieve(ll L, ll R){
ll lim = sqrt(R);
vector<bool> mark(lim + 1, false);
vector<ll> primes;
for (ll i = 2; i <= lim; i++){
if (!mark[i]){
primes.pb(i);
for (ll j = i * i; j <= lim; j += i) mark[j] = true;
}
}
vector<bool> isPrime(R - L + 1, true);
for (ll p : primes){
ll start = max(p * p, (L + p - 1) / p * p);
for (ll j = start; j <= R; j += p)
isPrime[j - L] = false;
}
if (L == 1) isPrime[0] = false;
return isPrime;
}
Sàng số nguyên tố đến N nhưng chỉ lưu danh sách
vector<int> primes;
void genPrimes(int N){
vector<bool> comp(N + 1, false);
for (int i = 2; i <= N; i++){
if (!comp[i]) primes.pb(i);
for (int p : primes){
if (1LL * i * p > N) break;
comp[i * p] = true;
if (i % p == 0) break;
}
}
}
Sàng số nguyên tố đến 10^9 bằng chia khối
// Tính pi(n) — số nguyên tố <= n
ll countPrimes(ll n){
if (n < 2) return 0;
ll root = sqrt(n);
vector<ll> V;
for (ll i = 1; i <= n; i = n / (n / i) + 1) V.pb(n / i);
// Cài đặt phức tạp hơn, dùng khi n lớn
return V.size(); // placeholder
}
Lũy thừa nhanh
Tính a^n trong O(log n) thay vì O(n).
Cơ bản
ll power(ll a, ll n){
ll r = 1;
while (n){
if (n & 1) r *= a;
a *= a;
n >>= 1;
}
return r;
}
Lũy thừa modulo
ll powerMod(ll a, ll n, ll m){
ll r = 1; a %= m;
while (n){
if (n & 1) r = (__int128)r * a % m;
a = (__int128)a * a % m;
n >>= 1;
}
return r;
}
Lũy thừa modulo không dùng __int128
ll mulMod(ll a, ll b, ll m){
ll res = 0;
a %= m;
while (b){
if (b & 1) res = (res + a) % m;
a = (a * 2) % m;
b >>= 1;
}
return res;
}
ll powerMod2(ll a, ll n, ll m){
ll r = 1; a %= m;
while (n){
if (n & 1) r = mulMod(r, a, m);
a = mulMod(a, a, m);
n >>= 1;
}
return r;
}
Tổng 1 + a + a² + ... + a^n
// Đệ quy O(log n)
ll geoSum(ll a, ll n, ll m){
if (n == 0) return 1 % m;
if (n % 2 == 0)
return (1 + (__int128)a * geoSum(a, n-1, m)) % m;
ll half = geoSum(a, n/2, m);
return (__int128)half * (1 + powerMod(a, n/2+1, m)) % m;
}
Số học Modulo
Phép toán với số dư — nền tảng của mọi bài toán đếm.
Công thức cơ bản
(a + b) % m = ((a % m) + (b % m)) % m
(a - b) % m = ((a % m) - (b % m) + m) % m
(a * b) % m = ((a % m) * (b % m)) % m
Chuẩn hóa số âm
ll norm(ll x, ll m){
return (x % m + m) % m;
}
Nhân không tràn số
// a, b, m <= 10^18
ll mulmod(ll a, ll b, ll m){
return (__int128)a * b % m;
}
// Hoặc dùng binary multiplication
ll mulmod2(ll a, ll b, ll m){
ll res = 0;
a %= m;
while (b){
if (b & 1) res = (res + a) % m;
a = (a * 2) % m;
b >>= 1;
}
return res;
}
Tính giai thừa mod m
ll factorialMod(int n, ll m){
ll r = 1;
for (int i = 2; i <= n; i++) r = r * i % m;
return r;
}
Nghịch đảo Modulo
$a^{-1} \pmod m$ sao cho $a \cdot a^{-1} \equiv 1 \pmod m$.
Fermat (m nguyên tố)
ll inv(ll a, ll m){
return powerMod(a, m - 2, m);
}
Euclid mở rộng (m bất kỳ)
ll inv2(ll a, ll m){
ll x, y;
extgcd(a, m, x, y);
return (x % m + m) % m;
}
Tính nghịch đảo 1..n trong O(n)
vector<ll> inv(n + 1);
inv[1] = 1;
for (int i = 2; i <= n; i++)
inv[i] = (m - m / i) * inv[m % i] % m;
Chia modulo
// a / b mod m = a * inv(b, m) % m
ll divMod(ll a, ll b, ll m){
return a * inv2(b, m) % m;
}
gcd(b, m) = 1. Nếu m nguyên tố, mọi b không chia hết cho m đều nghịch đảo được.Phi Euler
φ(n) = số nguyên dương ≤ n và nguyên tố cùng nhau với n.
Tính φ(n)
ll phi(ll n){
ll r = n;
for (ll i = 2; i * i <= n; i++)
if (n % i == 0){
while (n % i == 0) n /= i;
r -= r / i;
}
if (n > 1) r -= r / n;
return r;
}
Sàng φ 1..n
vector<int> phiSieve(int n){
vector<int> phi(n + 1);
for (int i = 0; i <= n; i++) phi[i] = i;
for (int i = 2; i <= n; i++)
if (phi[i] == i)
for (int j = i; j <= n; j += i)
phi[j] -= phi[j] / i;
return phi;
}
Định lý Euler
Nếu gcd(a, m) = 1: $a^{\phi(m)} \equiv 1 \pmod m$.
// Rút gọn mũ khi cần
ll powEuler(ll a, ll n, ll m){
ll p = phi(m);
if (n < p) return powerMod(a, n, m);
return powerMod(a, n % p + p, m);
}
Định lý Fermat nhỏ
Nếu p nguyên tố và a không chia hết cho p: $a^{p-1} \equiv 1 \pmod p$.
Định lý số dư Trung Hoa (CRT)
Giải hệ $x \equiv a_i \pmod{m_i}$ với $m_i$ đôi một nguyên tố cùng nhau.
ll crt(vector<ll>& a, vector<ll>& m){
ll M = 1;
for (ll x : m) M *= x;
ll res = 0;
for (int i = 0; i < a.size(); i++){
ll Mi = M / m[i], x, y;
extgcd(Mi, m[i], x, y);
res = (res + (__int128)a[i] * Mi % M * x) % M;
}
return (res % M + M) % M;
}
Cấp số cộng
Dãy số mà hiệu hai số liên tiếp không đổi.
Công thức
Số hạng thứ n: $$a_n = a_1 + (n-1)d$$
Tổng n số hạng đầu: $$S_n = \frac{n(a_1 + a_n)}{2} = \frac{n(2a_1 + (n-1)d)}{2}$$
Code
ll aN(ll a1, ll d, ll n){
return a1 + (n - 1) * d;
}
ll sumN(ll a1, ll d, ll n){
return n * (2 * a1 + (n - 1) * d) / 2;
}
Ví dụ
Dãy 1, 3, 5, 7,... ($a_1=1, d=2$): $a_{10} = 19$, $S_{10} = 100$.
Cấp số nhân
Dãy số mà tỉ số hai số liên tiếp không đổi.
Công thức
Số hạng thứ n: $$a_n = a_1 q^{n-1}$$
Tổng n số hạng đầu: $$S_n = a_1 \frac{q^n - 1}{q - 1} \quad (q \neq 1)$$
Tổng vô hạn: $$S_\infty = \frac{a_1}{1 - q} \quad (|q| < 1)$$
Code
ll aN(ll a1, ll q, ll n){
return a1 * power(q, n - 1);
}
ll sumN(ll a1, ll q, ll n){
if (q == 1) return a1 * n;
return a1 * (power(q, n) - 1) / (q - 1);
}
Fibonacci
$F_0=0, F_1=1, F_n = F_{n-1} + F_{n-2}$.
O(n)
ll fib(int n){
if (n <= 1) return n;
ll a = 0, b = 1;
for (int i = 2; i <= n; i++){
ll c = a + b;
a = b; b = c;
}
return b;
}
Ma trận O(log n)
struct Mat{
ll a[2][2];
Mat operator*(Mat b){
Mat c;
for (int i = 0; i < 2; i++)
for (int j = 0; j < 2; j++){
c.a[i][j] = 0;
for (int k = 0; k < 2; k++)
c.a[i][j] = (c.a[i][j] + a[i][k] * b.a[k][j]) % MOD;
}
return c;
}
};
ll fibFast(ll n){
Mat res = {{{1,0},{0,1}}}, base = {{{1,1},{1,0}}};
while (n){
if (n & 1) res = res * base;
base = base * base;
n >>= 1;
}
return res.a[0][1];
}
Công thức Binet (dùng double)
$$F_n = \frac{\phi^n - \psi^n}{\sqrt{5}}, \quad \phi = \frac{1+\sqrt{5}}{2}$$
ll fibBinet(int n){
double phi = (1 + sqrt(5)) / 2;
double psi = (1 - sqrt(5)) / 2;
return round((pow(phi, n) - pow(psi, n)) / sqrt(5));
}
// Chỉ chính xác đến n ~ 70
GCD của Fibonacci
$$\gcd(F_m, F_n) = F_{\gcd(m,n)}$$
Pisano period
Fibonacci mod m có chu kỳ. Với m = 10: chu kỳ 60.
Tổ hợp - Chỉnh hợp
Đếm số cách chọn phần tử từ một tập hợp.
Công thức
- Hoán vị: $P(n) = n!$
- Chỉnh hợp chập k: $A(n,k) = \frac{n!}{(n-k)!}$
- Tổ hợp chập k: $C(n,k) = \frac{n!}{k!(n-k)!}$
- Pascal: $C(n,k) = C(n-1,k-1) + C(n-1,k)$
- Tổ hợp có lặp: $C(n+k-1, k)$
Tổ hợp mod p bằng factorial + inverse
const int MOD = 1e9 + 7, N = 1e6 + 5;
ll fact[N], inv_fact[N];
ll pm(ll a, ll b){
ll r = 1; a %= MOD;
while (b){ if (b&1) r = r*a%MOD; a = a*a%MOD; b >>= 1; }
return r;
}
void prep(){
fact[0] = 1;
for (int i = 1; i < N; i++) fact[i] = fact[i-1] * i % MOD;
inv_fact[N-1] = pm(fact[N-1], MOD-2);
for (int i = N-2; i >= 0; i--) inv_fact[i] = inv_fact[i+1] * (i+1) % MOD;
}
ll C(int n, int k){
if (k < 0 || k > n) return 0;
return fact[n] * inv_fact[k] % MOD * inv_fact[n-k] % MOD;
}
ll A(int n, int k){
if (k < 0 || k > n) return 0;
return fact[n] * inv_fact[n-k] % MOD;
}
Tam giác Pascal
const int N = 1005;
ll C[N][N];
void buildPascal(int n){
for (int i = 0; i <= n; i++){
C[i][0] = C[i][i] = 1;
for (int j = 1; j < i; j++)
C[i][j] = (C[i-1][j-1] + C[i-1][j]) % MOD;
}
}
Lucas theorem (p nhỏ, n lớn)
ll lucas(ll n, ll k, ll p){
if (k == 0) return 1;
return C(n % p, k % p) * lucas(n / p, k / p, p) % p;
}
Đếm số cách chia kẹo
// n kẹo chia cho k bạn, mỗi bạn >= 1
ll distribute(ll n, ll k){
return C(n - 1, k - 1);
}
// Cho phép 0 kẹo
ll distribute2(ll n, ll k){
return C(n + k - 1, k - 1);
}
Số Catalan
$$C_n = \frac{1}{n+1}\binom{2n}{n} = \binom{2n}{n} - \binom{2n}{n+1}$$
Code
ll catalan(int n){
return C(2*n, n) * pm(n+1, MOD-2) % MOD;
}
// Hoặc dùng đệ quy:
// C_0 = 1, C_n = sum_{i=0}^{n-1} C_i * C_{n-1-i}
Ứng dụng
- Số cây nhị phân tìm kiếm có n nút
- Số cách đặt n cặp ngoặc đúng
- Số đường đi từ (0,0) đến (n,n) không vượt trục
- Số cách chia đa giác thành tam giác
10 số Catalan đầu
1, 1, 2, 5, 14, 42, 132, 429, 1430, 4862
Nim Game
Lý thuyết trò chơi cơ bản — tìm người thắng trong trò chơi đối kháng.
Nim 1 đống
// Lấy 1..k viên, ai lấy cuối thắng
// Người đi trước thắng iff n % (k+1) != 0
bool win(int n, int k){
return n % (k + 1) != 0;
}
Nim nhiều đống
// Lấy tùy ý từ 1 đống, ai không lấy được thua
// XOR tất cả đống
bool firstWins(vector<int>& piles){
ll x = 0;
for (int p : piles) x ^= p;
return x != 0;
}
Sprague-Grundy
Với trò chơi chia thành nhiều trò độc lập, gán giá trị Grundy cho mỗi trạng thái. XOR tất cả giá trị = 0 thì người đi sau thắng.
int grundy(int x, vector<int>& moves){
set<int> s;
for (int m : moves)
if (x >= m) s.insert(grundy(x - m, moves));
int g = 0;
while (s.count(g)) g++;
return g;
}
Ví dụ: Wythoff's game
// 2 đống, lấy từ 1 đống hoặc cả 2 cùng số
// Vị trí thua: (⌊kφ⌋, ⌊kφ²⌋) với φ = (1+√5)/2
bool wythoffLose(ll a, ll b){
if (a > b) swap(a, b);
double phi = (1 + sqrt(5)) / 2;
ll k = b - a;
return a == (ll)(k * phi);
}
Hình học — Tam giác
Diện tích tam giác
$$S = |(x_2-x_1)(y_3-y_1)-(x_3-x_1)(y_2-y_1)|$$
struct pt{ ll x, y; };
ll cross(pt O, pt A, pt B){
return (A.x - O.x) * (B.y - O.y) - (A.y - O.y) * (B.x - O.x);
}
ll area(pt A, pt B, pt C){
return abs(cross(A, B, C));
}
Kiểm tra điểm trong tam giác
bool inTriangle(pt A, pt B, pt C, pt M){
return area(A,B,M) + area(A,M,C) + area(M,B,C) == area(A,B,C);
}
Định lý Pythagoras
$a^2 + b^2 = c^2$
bool isRightTriangle(ll a, ll b, ll c){
ll x = max({a,b,c}), y = a+b+c-x;
return x*x == y*y + (a+b+c-x-y)*(a+b+c-x-y) - 2*x*(a+b+c-x-y) + 2*x*(a+b+c-x-y);
// Đơn giản hơn:
}
bool pythagoras(ll a, ll b, ll c){
if (a > c) swap(a, c);
if (b > c) swap(b, c);
return a*a + b*b == c*c;
}
Định lý Sin / Cosin
Cosin: $c^2 = a^2 + b^2 - 2ab\cos C$
Sin: $\frac{a}{\sin A} = \frac{b}{\sin B} = \frac{c}{\sin C} = 2R$
Heron: $S = \sqrt{p(p-a)(p-b)(p-c)}$, $p = (a+b+c)/2$
Tâm đường tròn ngoại tiếp
struct ptD{ double x, y; };
ptD circumcenter(ptD A, ptD B, ptD C){
double D = 2 * (A.x*(B.y-C.y) + B.x*(C.y-A.y) + C.x*(A.y-B.y));
double Ux = ((A.x*A.x + A.y*A.y)*(B.y-C.y)
+ (B.x*B.x + B.y*B.y)*(C.y-A.y)
+ (C.x*C.x + C.y*C.y)*(A.y-B.y)) / D;
double Uy = ((A.x*A.x + A.y*A.y)*(C.x-B.x)
+ (B.x*B.x + B.y*B.y)*(A.x-C.x)
+ (C.x*C.x + C.y*C.y)*(B.x-A.x)) / D;
return {Ux, Uy};
}
Đường thẳng
Phương trình tổng quát
$Ax + By + C = 0$ với $A = y_2-y_1$, $B = x_1-x_2$, $C = -Ax_1-By_1$.
Khoảng cách điểm đến đường thẳng
$$d = \frac{|Ax_0 + By_0 + C|}{\sqrt{A^2 + B^2}}$$
double distPointLine(double A, double B, double C, double x0, double y0){
return abs(A*x0 + B*y0 + C) / sqrt(A*A + B*B);
}
Giao 2 đường thẳng
bool intersect(double A1, double B1, double C1,
double A2, double B2, double C2,
double& x, double& y){
double D = A1*B2 - A2*B1;
double Dx = C1*B2 - C2*B1;
double Dy = A1*C2 - A2*C1;
if (fabs(D) < 1e-9) return false;
x = Dx / D;
y = Dy / D;
return true;
}
Đường thẳng qua 2 điểm
// A*x + B*y + C = 0
void lineFrom2Points(pt P, pt Q, ll& A, ll& B, ll& C){
A = Q.y - P.y;
B = P.x - Q.x;
C = -A*P.x - B*P.y;
}
Kiểm tra 3 điểm thẳng hàng
bool collinear(pt A, pt B, pt C){
return cross(A, B, C) == 0;
}
Đường tròn
// Phương trình: (x-a)² + (y-b)² = R²
struct Circle{ double x, y, r; };
// Giao 2 đường tròn
int circleIntersect(Circle c1, Circle c2, ptD& p1, ptD& p2){
double d = hypot(c2.x - c1.x, c2.y - c1.y);
if (d > c1.r + c2.r || d < fabs(c1.r - c2.r)) return 0;
double a = (c1.r*c1.r - c2.r*c2.r + d*d) / (2*d);
double h = sqrt(c1.r*c1.r - a*a);
double xm = c1.x + a*(c2.x-c1.x)/d;
double ym = c1.y + a*(c2.y-c1.y)/d;
p1 = {xm + h*(c2.y-c1.y)/d, ym - h*(c2.x-c1.x)/d};
p2 = {xm - h*(c2.y-c1.y)/d, ym + h*(c2.x-c1.x)/d};
return 2;
}
Đa giác
Diện tích đa giác (Shoelace)
ll polyArea(vector<pt>& p){
ll s = 0;
int n = p.size();
for (int i = 0; i < n; i++){
int j = (i + 1) % n;
s += p[i].x * p[j].y - p[j].x * p[i].y;
}
return abs(s);
}
Kiểm tra điểm trong đa giác (Ray casting)
bool inPolygon(vector<pt>& poly, pt p){
int n = poly.size(), cnt = 0;
for (int i = 0; i < n; i++){
int j = (i + 1) % n;
if ((poly[i].y > p.y) != (poly[j].y > p.y)){
double xInt = (poly[j].x - poly[i].x) * (p.y - poly[i].y)
/ (poly[j].y - poly[i].y) + poly[i].x;
if (p.x < xInt) cnt++;
}
}
return cnt % 2 == 1;
}
Đa giác lồi
bool isConvex(vector<pt>& poly){
int n = poly.size();
int sign = 0;
for (int i = 0; i < n; i++){
ll c = cross(poly[i], poly[(i+1)%n], poly[(i+2)%n]);
if (c != 0){
if (sign == 0) sign = (c > 0) ? 1 : -1;
else if ((c > 0 ? 1 : -1) != sign) return false;
}
}
return true;
}
Chu vi đa giác
double polyPeri(vector<pt>& p){
double s = 0;
int n = p.size();
for (int i = 0; i < n; i++){
int j = (i + 1) % n;
s += hypot(p[j].x - p[i].x, p[j].y - p[i].y);
}
return s;
}
Convex Hull — Bao lồi
Đa giác lồi nhỏ nhất chứa tất cả các điểm.
Thuật toán Andrew (Monotone Chain)
struct pt{ ll x, y; };
ll cross(pt O, pt A, pt B){
return (A.x-O.x)*(B.y-O.y) - (A.y-O.y)*(B.x-O.x);
}
vector<pt> convexHull(vector<pt> p){
sort(all(p), [](pt a, pt b){
return a.x < b.x || (a.x == b.x && a.y < b.y);
});
vector<pt> h;
// Lower hull
for (auto& q : p){
while (h.size() >= 2 && cross(h[h.size()-2], h.back(), q) <= 0)
h.pop_back();
h.pb(q);
}
// Upper hull
int sz = h.size();
for (int i = p.size() - 2; i >= 0; i--){
while (h.size() > sz && cross(h[h.size()-2], h.back(), p[i]) <= 0)
h.pop_back();
h.pb(p[i]);
}
h.pop_back();
return h;
}
Rotating Calipers — đường kính bao lồi
ll diameterHull(vector<pt>& h){
int n = h.size();
if (n == 1) return 0;
if (n == 2) return (h[0].x-h[1].x)*(h[0].x-h[1].x) + (h[0].y-h[1].y)*(h[0].y-h[1].y);
ll best = 0;
int j = 1;
for (int i = 0; i < n; i++){
while (abs(cross(h[i], h[(i+1)%n], h[(j+1)%n])) >
abs(cross(h[i], h[(i+1)%n], h[j])))
j = (j + 1) % n;
best = max(best, max((h[i].x-h[j].x)*(h[i].x-h[j].x)+(h[i].y-h[j].y)*(h[i].y-h[j].y),
(h[(i+1)%n].x-h[j].x)*(h[(i+1)%n].x-h[j].x)+(h[(i+1)%n].y-h[j].y)*(h[(i+1)%n].y-h[j].y)));
}
return best;
}
Kiểm tra điểm có trên bao lồi không
bool onHull(vector<pt>& h, pt p){
int n = h.size();
for (int i = 0; i < n; i++)
if (cross(h[i], h[(i+1)%n], p) == 0)
return true;
return false;
}
Mảng cơ bản
Khai báo và thao tác
int a[100]; // Mảng tĩnh
vector<int> v; // Vector động
vector<int> v2(n); // n phần tử
vector<int> v3(n, 0); // n phần tử = 0
vector<vector<int>> g(n, vector<int>(m, 0));
v.pb(x); // Thêm cuối
v.pop_back(); // Xóa cuối
v.size(); // Kích thước
v.empty(); // Rỗng?
v.clear(); // Xóa hết
v.front(); v.back(); // Đầu / cuối
v.insert(v.begin() + i, x); // Chèn ở i
v.erase(v.begin() + i); // Xóa ở i
v.resize(n); // Đổi kích thước
reverse(all(v)); // Đảo
sort(all(v)); // Sắp xếp
Min, max, tổng
int mn = *min_element(all(v));
int mx = *max_element(all(v));
ll sum = accumulate(all(v), 0LL);
int posMin = min_element(all(v)) - v.begin();
int posMax = max_element(all(v)) - v.begin();
Xóa phần tử trùng
sort(all(v));
v.erase(unique(all(v)), v.end());
Đảo mảng
reverse(all(v));
// Tự viết
int l = 0, r = n - 1;
while (l < r) swap(a[l++], a[r--]);
Xoay mảng k bước
rotate(a.begin(), a.begin() + k, a.end());
Mảng 2 chiều — duyệt
for (int i = 0; i < n; i++)
for (int j = 0; j < m; j++)
// ...
// Duyệt 8 hướng
int dx[] = {-1,-1,-1, 0, 0, 1, 1, 1};
int dy[] = {-1, 0, 1,-1, 1,-1, 0, 1};
for (int d = 0; d < 8; d++){
int nx = i + dx[d], ny = j + dy[d];
if (nx < 0 || nx >= n || ny < 0 || ny >= m) continue;
// ...
}
Ma trận chuyển vị
for (int i = 0; i < n; i++)
for (int j = i + 1; j < m; j++)
swap(a[i][j], a[j][i]);
Mảng cộng dồn 2D
for (int i = 1; i <= n; i++)
for (int j = 1; j <= m; j++)
pre[i][j] = pre[i-1][j] + pre[i][j-1] - pre[i-1][j-1] + a[i][j];
Sắp xếp
Bubble Sort — O(n²)
for (int i = 0; i < n-1; i++)
for (int j = 0; j < n-i-1; j++)
if (a[j] > a[j+1]) swap(a[j], a[j+1]);
Insertion Sort — O(n²)
for (int i = 1; i < n; i++){
int k = a[i], j = i - 1;
while (j >= 0 && a[j] > k){ a[j+1] = a[j]; j--; }
a[j+1] = k;
}
Selection Sort — O(n²)
for (int i = 0; i < n-1; i++){
int mn = i;
for (int j = i+1; j < n; j++)
if (a[j] < a[mn]) mn = j;
swap(a[i], a[mn]);
}
Counting Sort — O(n + max)
void countingSort(vector<int>& a){
int mx = *max_element(all(a));
vector<int> cnt(mx + 1, 0);
for (int x : a) cnt[x]++;
int idx = 0;
for (int i = 0; i <= mx; i++)
while (cnt[i]--) a[idx++] = i;
}
Sort với comparator
// Giảm dần
sort(all(v), greater<int>());
// Theo cặp (first tăng, second giảm)
sort(all(v), [](pii a, pii b){
if (a.fi != b.fi) return a.fi < b.fi;
return a.se > b.se;
});
// Sort chuỗi theo độ dài
sort(all(vs), [](string a, string b){
return a.size() < b.size();
});
// Sort struct
struct S{ int x, y, z; };
sort(all(v), [](S a, S b){
if (a.x != b.x) return a.x < b.x;
if (a.y != b.y) return a.y < b.y;
return a.z < b.z;
});
Sắp xếp theo tần suất
map<int,int> cnt;
for (int x : a) cnt[x]++;
sort(all(a), [&](int x, int y){
if (cnt[x] != cnt[y]) return cnt[x] > cnt[y];
return x < y;
});
Binary Search
Tìm kiếm trên mảng đã sắp xếp — O(log n).
Cơ bản
int bs(vector<int>& a, int x){
int lo = 0, hi = a.size() - 1;
while (lo <= hi){
int mid = lo + (hi - lo) / 2;
if (a[mid] == x) return mid;
if (a[mid] < x) lo = mid + 1;
else hi = mid - 1;
}
return -1;
}
STL
lower_bound(all(a), x); // Vị trí đầu >= x
upper_bound(all(a), x); // Vị trí đầu > x
binary_search(all(a), x); // Có x không?
// Đếm số lần xuất hiện
int cnt = upper_bound(all(a), x) - lower_bound(all(a), x);
Tìm căn bậc 2 nguyên
ll isqrt(ll n){
ll lo = 0, hi = 1e9 + 1;
while (lo < hi){
ll mid = (lo + hi + 1) / 2;
if (mid * mid <= n) lo = mid;
else hi = mid - 1;
}
return lo;
}
Tìm phần tử lớn nhất < x
int pos = lower_bound(all(a), x) - a.begin() - 1;
if (pos >= 0) return a[pos];
Binary Search on Answer
Tìm giá trị nhỏ nhất/lớn nhất thỏa mãn điều kiện đơn điệu.
Template tìm min
ll lo = 1, hi = 1e18, ans = -1;
while (lo <= hi){
ll mid = lo + (hi - lo) / 2;
if (check(mid)){
ans = mid;
hi = mid - 1;
} else lo = mid + 1;
}
cout << ans;
Template tìm max
ll lo = 1, hi = 1e18, ans = -1;
while (lo <= hi){
ll mid = lo + (hi - lo) / 2;
if (check(mid)){
ans = mid;
lo = mid + 1;
} else hi = mid - 1;
}
cout << ans;
Ví dụ: Chia mảng thành k đoạn có tổng lớn nhất nhỏ nhất
bool canDivide(vector<int>& a, int k, ll limit){
int cnt = 1;
ll sum = 0;
for (int x : a){
if (x > limit) return false;
if (sum + x > limit){ cnt++; sum = x; }
else sum += x;
}
return cnt <= k;
}
// lo = max(a), hi = sum(a)
Ví dụ: Cắt gỗ
bool canCut(vector<int>& a, int k, ll len){
ll cnt = 0;
for (int x : a) cnt += x / len;
return cnt >= k;
}
Hai con trỏ
Dùng 2 chỉ số di chuyển để giải bài toán O(n) thay vì O(n²).
Đếm cặp có tổng = S
sort(all(a));
int l = 0, r = n - 1, cnt = 0;
while (l < r){
int s = a[l] + a[r];
if (s == S){ cnt++; l++; r--; }
else if (s < S) l++;
else r--;
}
Đếm cặp chênh lệch = k
sort(all(a));
int l = 0, cnt = 0;
for (int r = 1; r < n; r++){
while (a[r] - a[l] > k) l++;
if (a[r] - a[l] == k) cnt++;
}
Kiểm tra mảng đối xứng
bool isPal(vector<int>& a){
int l = 0, r = a.size() - 1;
while (l < r){
if (a[l] != a[r]) return false;
l++; r--;
}
return true;
}
Gộp hai mảng đã sắp xếp
vector<int> merge2(vector<int>& a, vector<int>& b){
vector<int> c;
int i = 0, j = 0;
while (i < a.size() && j < b.size()){
if (a[i] <= b[j]) c.pb(a[i++]);
else c.pb(b[j++]);
}
while (i < a.size()) c.pb(a[i++]);
while (j < b.size()) c.pb(b[j++]);
return c;
}
3Sum — bộ 3 có tổng = 0
vector<vector<int>> threeSum(vector<int>& a){
sort(all(a));
vector<vector<int>> res;
for (int i = 0; i < a.size(); i++){
if (i && a[i] == a[i-1]) continue;
int l = i+1, r = a.size()-1;
while (l < r){
int s = a[i] + a[l] + a[r];
if (s == 0){
res.pb({a[i], a[l], a[r]});
while (l < r && a[l] == a[l+1]) l++;
while (l < r && a[r] == a[r-1]) r--;
l++; r--;
}
else if (s < 0) l++;
else r--;
}
}
return res;
}
Prefix Sum — Mảng cộng dồn
Tính tổng đoạn [l, r] trong O(1) sau tiền xử lý O(n).
1D
vector<ll> pre(n + 1, 0);
for (int i = 1; i <= n; i++)
pre[i] = pre[i-1] + a[i];
// Tổng [l, r]
ll s = pre[r] - pre[l-1];
2D
// pre[i][j] = tổng hình chữ nhật (1,1) đến (i,j)
for (int i = 1; i <= n; i++)
for (int j = 1; j <= m; j++)
pre[i][j] = pre[i-1][j] + pre[i][j-1] - pre[i-1][j-1] + a[i][j];
// Tổng hình chữ nhật (x1,y1) đến (x2,y2)
ll s = pre[x2][y2] - pre[x1-1][y2] - pre[x2][y1-1] + pre[x1-1][y1-1];
Đếm số đoạn con có tổng = S
map<ll,int> mp;
mp[0] = 1;
ll sum = 0, cnt = 0;
for (int x : a){
sum += x;
cnt += mp[sum - S];
mp[sum]++;
}
Tổng con lớn nhất có độ dài ≤ k
// Dùng prefix + sliding window min
Sliding Window — Cửa sổ trượt
Duy trì một "cửa sổ" [l, r] trên mảng để giải bài toán O(n).
Tổng lớn nhất k phần tử liên tiếp
ll sum = 0, best;
for (int i = 0; i < k; i++) sum += a[i];
best = sum;
for (int i = k; i < n; i++){
sum += a[i] - a[i-k];
best = max(best, sum);
}
Đoạn ngắn nhất có tổng ≥ S
int l = 0, res = INT_MAX;
ll sum = 0;
for (int r = 0; r < n; r++){
sum += a[r];
while (sum >= S){
res = min(res, r - l + 1);
sum -= a[l++];
}
}
Đoạn dài nhất có tổng ≤ S
int l = 0, res = 0;
ll sum = 0;
for (int r = 0; r < n; r++){
sum += a[r];
while (sum > S) sum -= a[l++];
res = max(res, r - l + 1);
}
Đếm đoạn có đúng k số chẵn
// Đếm đoạn con có đúng k phần tử lẻ
int l = 0, cntOdd = 0;
ll res = 0;
for (int r = 0; r < n; r++){
if (a[r] % 2) cntOdd++;
while (cntOdd > k){
if (a[l] % 2) cntOdd--;
l++;
}
if (cntOdd == k) res += (r - l + 1);
}
Đoạn con dài nhất không có phần tử trùng
int l = 0, res = 0;
map<int,int> cnt;
for (int r = 0; r < n; r++){
cnt[a[r]]++;
while (cnt[a[r]] > 1){
cnt[a[l]]--;
l++;
}
res = max(res, r - l + 1);
}
Mảng hiệu (Difference Array)
Cập nhật đoạn [l, r] += v trong O(1), truy vấn O(n).
1D
vector<ll> d(n + 2, 0);
// Cập nhật [l, r] += v
d[l] += v; d[r+1] -= v;
// Truy vấn giá trị tại i (sau tất cả cập nhật)
ll cur = 0;
for (int i = 1; i <= n; i++){
cur += d[i];
a[i] = cur;
}
2D
// Cập nhật hình chữ nhật (x1,y1)-(x2,y2) += v
d[x1][y1] += v;
d[x1][y2+1] -= v;
d[x2+1][y1] -= v;
d[x2+1][y2+1] += v;
// Truy vấn
for (int i = 1; i <= n; i++)
for (int j = 1; j <= m; j++)
a[i][j] = d[i][j] + a[i-1][j] + a[i][j-1] - a[i-1][j-1];
Ứng dụng: Đếm số lần phủ
// Cho k đoạn, đếm số đoạn phủ mỗi điểm
vector<int> d(N, 0);
for (auto [l, r] : ranges){
d[l]++;
d[r+1]--;
}
int cur = 0;
for (int i = 1; i < N; i++){
cur += d[i];
// cur = số đoạn phủ điểm i
}
Nén tọa độ (Coordinate Compression)
Đưa các giá trị lớn về khoảng nhỏ [1, n] mà vẫn giữ thứ tự.
vector<int> sorted = a;
sort(all(sorted));
sorted.erase(unique(all(sorted)), sorted.end());
for (int& x : a)
x = lower_bound(all(sorted), x) - sorted.begin() + 1;
Ứng dụng
- Đếm tần suất các số lớn (10^9) bằng mảng thay vì map
- Sử dụng Segment Tree / BIT với chỉ số nén
- Bài toán đếm số nghịch thế với giá trị lớn
Đếm số nghịch thế bằng BIT + nén
// Sau khi nén a về 1..n
ll inv = 0;
for (int i = n - 1; i >= 0; i--){
inv += query(a[i] - 1);
update(a[i], 1);
}
Kadane — Đoạn con có tổng lớn nhất
Cơ bản
ll best = a[0], cur = a[0];
for (int i = 1; i < n; i++){
cur = max((ll)a[i], cur + a[i]);
best = max(best, cur);
}
cout << best;
Tìm cả vị trí đoạn
ll best = a[0], cur = a[0];
int start = 0, end = 0, s = 0;
for (int i = 1; i < n; i++){
if (cur + a[i] < a[i]){ cur = a[i]; s = i; }
else cur += a[i];
if (cur > best){ best = cur; start = s; end = i; }
}
Tổng con lớn nhất có độ dài chẵn
// Dùng 2 biến: maxEven, maxOdd
Tổng con lớn nhất không kề nhau (House Robber)
ll rob(vector<int>& a){
ll prev2 = 0, prev1 = 0;
for (int x : a){
ll cur = max(prev1, prev2 + x);
prev2 = prev1;
prev1 = cur;
}
return prev1;
}
Tổng con lớn nhất trên vòng tròn
ll maxSubarrayCircular(vector<int>& a){
ll total = 0;
for (int x : a) total += x;
// Max subarray bình thường
ll best1 = a[0], cur = a[0];
for (int i = 1; i < a.size(); i++){
cur = max((ll)a[i], cur + a[i]);
best1 = max(best1, cur);
}
// Max subarray bỏ 1 đoạn ở giữa = total - min subarray
ll best2 = a[0], cur2 = a[0];
for (int i = 1; i < a.size(); i++){
cur2 = min((ll)a[i], cur2 + a[i]);
best2 = min(best2, cur2);
}
if (best1 < 0) return best1;
return max(best1, total - best2);
}
Mo's Algorithm
Trả lời Q truy vấn đoạn ngoại tuyến trong $O((n+q)\sqrt{n})$.
int n, q;
int a[N];
int block;
struct Query{
int l, r, idx;
};
Query qs[N];
int res[N];
bool cmp(Query a, Query b){
if (a.l / block != b.l / block)
return a.l / block < b.l / block;
// Tối ưu: sort chẵn/lẻ
return ((a.l / block) & 1) ? a.r > b.r : a.r < b.r;
}
int curAns = 0;
int cnt[1000005];
void add(int i){
cnt[a[i]]++;
if (cnt[a[i]] == 1) curAns++;
}
void remove(int i){
cnt[a[i]]--;
if (cnt[a[i]] == 0) curAns--;
}
int main(){
cin >> n >> q;
for (int i = 0; i < n; i++) cin >> a[i];
for (int i = 0; i < q; i++){
cin >> qs[i].l >> qs[i].r;
qs[i].l--; qs[i].r--;
qs[i].idx = i;
}
block = sqrt(n) + 1;
sort(qs, qs + q, cmp);
int cl = 0, cr = -1;
for (int i = 0; i < q; i++){
while (cl > qs[i].l) add(--cl);
while (cr < qs[i].r) add(++cr);
while (cl < qs[i].l) remove(cl++);
while (cr > qs[i].r) remove(cr--);
res[qs[i].idx] = curAns;
}
for (int i = 0; i < q; i++) cout << res[i] << "\n";
}
Chuỗi cơ bản
Khai báo và nhập xuất
string s = "hello";
getline(cin, s); // Đọc cả dòng (có dấu cách)
cin >> s; // Đọc 1 từ (dừng ở dấu cách)
cout << s << "\n";
Truy cập ký tự
s[0]; // Ký tự đầu
s.at(0); // Có kiểm tra biên
s.front(); // Ký tự đầu
s.back(); // Ký tự cuối
s.size(); // Độ dài
s.length(); // Tương tự
Nối chuỗi
string a = "hello", b = "world";
string c = a + " " + b; // "hello world"
a += "!"; // "hello!"
a.append("!!!"); // Nối nhiều ký tự
a.append(b); // Nối chuỗi
Cắt chuỗi con
string s = "hello world";
s.substr(0, 5); // "hello"
s.substr(6); // "world"
s.substr(6, 3); // "wor"
Tìm kiếm
s.find("world"); // 6
s.find("xyz"); // string::npos
s.rfind("l"); // Vị trí cuối
s.find("l", 5); // Tìm từ vị trí 5
if (s.find("x") != string::npos) { /* tìm thấy */ }
Thay thế, chèn, xóa
s.replace(0, 2, "HE"); // Thay 2 ký tự từ vị trí 0
s.insert(2, "XX"); // Chèn tại vị trí 2
s.erase(2, 2); // Xóa 2 ký tự từ vị trí 2
s.pb('!'); // Thêm cuối
s.pop_back(); // Xóa cuối
s.clear(); // Xóa hết
Đảo ngược
reverse(all(s));
Chuyển string ↔ số
// String → số
int x = stoi("123");
ll y = stoll("123456789");
double z = stod("3.14");
// Số → string
string s1 = to_string(123);
string s2 = to_string(3.14);
Xử lý chuỗi
Ký tự
char c = 'A';
tolower(c); // 'a'
toupper(c); // 'A'
isdigit(c); // Có phải chữ số?
isalpha(c); // Chữ cái?
isalnum(c); // Chữ hoặc số?
isspace(c); // Khoảng trắng?
c - 'a'; // 0..25
'0' + 5; // Ký tự '5'
Đếm ký tự
int cnt[256] = {0};
for (char c : s) cnt[c]++;
cout << cnt['a'];
Tách từ theo dấu cách
stringstream ss(s);
string t;
while (ss >> t){
// Xử lý từng từ
}
Tách theo ký tự phân cách
stringstream ss(s);
string t;
while (getline(ss, t, ',')){
// Xử lý
}
Viết hoa chữ đầu mỗi từ
bool newWord = true;
for (char& c : s){
if (newWord) c = toupper(c);
newWord = (c == ' ');
}
Xóa khoảng trắng thừa
stringstream ss(s);
string t, res;
while (ss >> t){
res += t + " ";
}
if (!res.empty()) res.pop_back();
s = res;
Đảo ngược từng từ
// "hello world" → "olleh dlrow"
stringstream ss(s);
string t, res;
while (ss >> t){
reverse(all(t));
res += t + " ";
}
if (!res.empty()) res.pop_back();
Đảo thứ tự từ
// "hello world" → "world hello"
stringstream ss(s);
vector<string> words;
string t;
while (ss >> t) words.pb(t);
reverse(all(words));
s = "";
for (int i = 0; i < words.size(); i++){
if (i) s += " ";
s += words[i];
}
Đếm số từ
stringstream ss(s);
string t;
int cnt = 0;
while (ss >> t) cnt++;
Chuẩn hóa tên người
// " nguyen van a " → "Nguyen Van A"
stringstream ss(s);
string t, res;
while (ss >> t){
t[0] = toupper(t[0]);
for (int i = 1; i < t.size(); i++) t[i] = tolower(t[i]);
res += t + " ";
}
if (!res.empty()) res.pop_back();
s = res;
So sánh chuỗi không phân biệt hoa thường
string a = "Hello", b = "hello";
transform(all(a), a.begin(), ::tolower);
transform(all(b), b.begin(), ::tolower);
if (a == b) cout << "Bang";
Chuyển đổi cơ số
// Thập phân → nhị phân
string toBinary(int n){
if (n == 0) return "0";
string s;
while (n){ s += (n % 2) + '0'; n /= 2; }
reverse(all(s));
return s;
}
// Nhị phân → thập phân
ll fromBinary(string s){
ll x = 0;
for (char c : s) x = x * 2 + (c - '0');
return x;
}
// Thập phân → cơ số b (2 ≤ b ≤ 36)
string toBase(ll n, int b){
if (n == 0) return "0";
string d = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ";
string s;
while (n){ s += d[n % b]; n /= b; }
reverse(all(s));
return s;
}
// Cơ số b → thập phân
ll fromBase(string s, int b){
ll x = 0;
for (char c : s){
int v = isdigit(c) ? c - '0' : c - 'A' + 10;
x = x * b + v;
}
return x;
}
Palindrome
Chuỗi đọc xuôi ngược giống nhau.
Kiểm tra palindrome
bool isPalindrome(string s){
int l = 0, r = s.size() - 1;
while (l < r){
if (s[l] != s[r]) return false;
l++; r--;
}
return true;
}
// Hoặc
bool isPal2(string s){
string t = s;
reverse(all(t));
return s == t;
}
Palindrome dài nhất (Interval DP)
int longestPalindrome(string s){
int n = s.size();
vector<vector<bool>> dp(n, vector<bool>(n, false));
int maxLen = 1;
for (int i = 0; i < n; i++) dp[i][i] = true;
for (int len = 2; len <= n; len++){
for (int i = 0; i + len - 1 < n; i++){
int j = i + len - 1;
if (s[i] == s[j]){
if (len == 2) dp[i][j] = true;
else dp[i][j] = dp[i+1][j-1];
if (dp[i][j]) maxLen = max(maxLen, len);
}
}
}
return maxLen;
}
Đếm số palindrome con
int countPalindromes(string s){
int n = s.size();
vector<vector<bool>> dp(n, vector<bool>(n, false));
int cnt = n;
for (int i = 0; i < n; i++) dp[i][i] = true;
for (int len = 2; len <= n; len++){
for (int i = 0; i + len - 1 < n; i++){
int j = i + len - 1;
if (s[i] == s[j]){
if (len == 2 || dp[i+1][j-1]){
dp[i][j] = true;
cnt++;
}
}
}
}
return cnt;
}
Palindrome dài nhất bằng Manacher — O(n)
vector<int> manacher(string s){
string t = "#";
for (char c : s){ t += c; t += '#'; }
int n = t.size();
vector<int> p(n, 0);
int l = 0, r = 0;
for (int i = 0; i < n; i++){
p[i] = (i < r) ? min(r - i, p[l + r - i]) : 0;
while (i - p[i] - 1 >= 0 && i + p[i] + 1 < n
&& t[i - p[i] - 1] == t[i + p[i] + 1]) p[i]++;
if (i + p[i] > r){ l = i - p[i]; r = i + p[i]; }
}
return p;
}
// Độ dài palindrome dài nhất = max(p)
KMP — Knuth-Morris-Pratt
Tìm xâu con trong xâu mẹ O(n + m).
Xây dựng bảng prefix
vector<int> prefix(string p){
vector<int> pi(p.size(), 0);
for (int i = 1; i < p.size(); i++){
int j = pi[i-1];
while (j > 0 && p[i] != p[j]) j = pi[j-1];
if (p[i] == p[j]) j++;
pi[i] = j;
}
return pi;
}
Tìm tất cả vị trí xuất hiện
vector<int> kmp(string s, string p){
auto pi = prefix(p);
vector<int> res;
int j = 0;
for (int i = 0; i < s.size(); i++){
while (j > 0 && s[i] != p[j]) j = pi[j-1];
if (s[i] == p[j]) j++;
if (j == p.size()){
res.pb(i - j + 1);
j = pi[j-1];
}
}
return res;
}
Ứng dụng: Tìm chu kỳ nhỏ nhất
// Chu kỳ nhỏ nhất của xâu s
int minPeriod(string s){
auto pi = prefix(s);
int n = s.size();
int k = n - pi[n-1];
if (n % k == 0) return k;
return n;
}
Z-Algorithm
z[i] = độ dài xâu con dài nhất bắt đầu tại i và trùng prefix.
vector<int> zFunc(string s){
int n = s.size();
vector<int> z(n, 0);
int l = 0, r = 0;
for (int i = 1; i < n; i++){
if (i < r) z[i] = min(r - i, z[i - l]);
while (i + z[i] < n && s[z[i]] == s[i + z[i]]) z[i]++;
if (i + z[i] > r){ l = i; r = i + z[i]; }
}
return z;
}
Ứng dụng: Tìm xâu con
// Tìm p trong s
string t = p + "$" + s;
auto z = zFunc(t);
for (int i = 0; i < z.size(); i++)
if (z[i] == p.size())
cout << i - p.size() - 1 << "\n";
Ứng dụng: Đếm số xâu con bằng prefix
// Cho xâu s, đếm số lần mỗi prefix xuất hiện
Hash xâu
Băm chuỗi thành số để so sánh nhanh O(1).
Hash đơn
const ll BASE = 311, MOD = 1e9 + 7;
const int N = 1e6 + 5;
ll pw[N], hs[N];
void prep(string s){
pw[0] = 1;
for (int i = 1; i < N; i++) pw[i] = pw[i-1] * BASE % MOD;
hs[0] = 0;
for (int i = 0; i < s.size(); i++)
hs[i+1] = (hs[i] * BASE + s[i]) % MOD;
}
ll getHash(int l, int r){
return (hs[r] - hs[l-1] * pw[r-l+1] % MOD + MOD) % MOD;
}
Double hash (an toàn hơn)
const ll B1 = 311, M1 = 1e9+7, B2 = 313, M2 = 1e9+9;
ll pw1[N], pw2[N], hs1[N], hs2[N];
void prep(string s){
pw1[0] = pw2[0] = 1;
for (int i = 1; i < N; i++){
pw1[i] = pw1[i-1] * B1 % M1;
pw2[i] = pw2[i-1] * B2 % M2;
}
hs1[0] = hs2[0] = 0;
for (int i = 0; i < s.size(); i++){
hs1[i+1] = (hs1[i] * B1 + s[i]) % M1;
hs2[i+1] = (hs2[i] * B2 + s[i]) % M2;
}
}
pair<ll,ll> getHash2(int l, int r){
ll x = (hs1[r] - hs1[l-1] * pw1[r-l+1] % M1 + M1) % M1;
ll y = (hs2[r] - hs2[l-1] * pw2[r-l+1] % M2 + M2) % M2;
return {x, y};
}
Ứng dụng: Đếm số xâu con phân biệt
set<ll> se;
for (int i = 0; i < n; i++)
for (int j = i; j < n; j++)
se.insert(getHash(i, j));
cout << se.size();
Manacher
Tìm palindrome dài nhất trong O(n).
vector<int> manacher(string s){
string t = "#";
for (char c : s){ t += c; t += '#'; }
int n = t.size();
vector<int> p(n, 0);
int l = 0, r = 0;
for (int i = 0; i < n; i++){
p[i] = (i < r) ? min(r - i, p[l + r - i]) : 0;
while (i - p[i] - 1 >= 0 && i + p[i] + 1 < n
&& t[i - p[i] - 1] == t[i + p[i] + 1]) p[i]++;
if (i + p[i] > r){ l = i - p[i]; r = i + p[i]; }
}
return p;
}
// Độ dài palindrome dài nhất
int longestPal(string s){
auto p = manacher(s);
return *max_element(all(p));
}
// Tìm vị trí palindrome dài nhất trong xâu gốc
pair<int,int> longestPalRange(string s){
auto p = manacher(s);
int mx = 0, idx = 0;
for (int i = 0; i < p.size(); i++)
if (p[i] > mx){ mx = p[i]; idx = i; }
int start = (idx - mx) / 2;
return {start, start + mx - 1};
}
Đếm số palindrome con
ll countPals(string s){
auto p = manacher(s);
ll cnt = 0;
for (int x : p) cnt += (x + 1) / 2;
return cnt;
}
Stack - Queue - Deque
Ba CTDL cơ bản, dùng trong hầu hết bài toán mô phỏng.
Stack (LIFO)
stack<int> st;
st.push(1);
st.push(2);
cout << st.top(); // 2
st.pop();
cout << st.size(); // 1
cout << st.empty(); // false
Queue (FIFO)
queue<int> q;
q.push(1);
q.push(2);
cout << q.front(); // 1
cout << q.back(); // 2
q.pop();
cout << q.size(); // 1
Deque (2 đầu)
deque<int> dq;
dq.push_back(1);
dq.push_front(2);
dq.push_back(3);
// dq: 2 1 3
cout << dq.front(); // 2
cout << dq.back(); // 3
dq.pop_front();
dq.pop_back();
Ứng dụng 1: Kiểm tra ngoặc đúng
bool check(string s){
stack<char> st;
for (char c : s){
if (c == '(' || c == '[' || c == '{') st.push(c);
else {
if (st.empty()) return false;
char t = st.top(); st.pop();
if ((c == ')' && t != '(') ||
(c == ']' && t != '[') ||
(c == '}' && t != '{')) return false;
}
}
return st.empty();
}
Ứng dụng 2: Tính giá trị biểu thức hậu tố
int evalRPN(vector<string>& tokens){
stack<int> st;
for (string t : tokens){
if (t == "+" || t == "-" || t == "*" || t == "/"){
int b = st.top(); st.pop();
int a = st.top(); st.pop();
if (t == "+") st.push(a + b);
if (t == "-") st.push(a - b);
if (t == "*") st.push(a * b);
if (t == "/") st.push(a / b);
} else st.push(stoi(t));
}
return st.top();
}
Ứng dụng 3: Đảo ngược dãy
stack<int> st;
for (int x : a) st.push(x);
for (int i = 0; i < n; i++){ a[i] = st.top(); st.pop(); }
Monotonic Stack
Ngăn xếp đơn điệu — dùng để tìm "next greater/smaller element".
Next Greater Element
// Phần tử lớn hơn kế tiếp bên phải
vector<int> nge(n, -1);
stack<int> st;
for (int i = 0; i < n; i++){
while (!st.empty() && a[st.top()] < a[i]){
nge[st.top()] = a[i];
st.pop();
}
st.push(i);
}
Next Smaller Element
vector<int> nse(n, -1);
stack<int> st;
for (int i = 0; i < n; i++){
while (!st.empty() && a[st.top()] > a[i]){
nse[st.top()] = a[i];
st.pop();
}
st.push(i);
}
Previous Greater Element
vector<int> pge(n, -1);
stack<int> st;
for (int i = 0; i < n; i++){
while (!st.empty() && a[st.top()] <= a[i]) st.pop();
if (!st.empty()) pge[i] = a[st.top()];
st.push(i);
}
Largest Rectangle in Histogram
ll largestRectangle(vector<int>& h){
int n = h.size();
vector<int> left(n), right(n);
stack<int> st;
// Left smaller
for (int i = 0; i < n; i++){
while (!st.empty() && h[st.top()] >= h[i]) st.pop();
left[i] = st.empty() ? -1 : st.top();
st.push(i);
}
while (!st.empty()) st.pop();
// Right smaller
for (int i = n - 1; i >= 0; i--){
while (!st.empty() && h[st.top()] >= h[i]) st.pop();
right[i] = st.empty() ? n : st.top();
st.push(i);
}
ll best = 0;
for (int i = 0; i < n; i++)
best = max(best, 1LL * h[i] * (right[i] - left[i] - 1));
return best;
}
Max Rectangle in Binary Matrix
// Xử lý từng hàng như histogram
vector<int> hist(m, 0);
ll best = 0;
for (int i = 0; i < n; i++){
for (int j = 0; j < m; j++){
if (mat[i][j]) hist[j]++;
else hist[j] = 0;
}
best = max(best, largestRectangle(hist));
}
Heap / Priority Queue
Hàng đợi ưu tiên — truy vấn phần tử max/min trong O(log n).
Max Heap (mặc định)
priority_queue<int> pq;
pq.push(3);
pq.push(1);
pq.push(5);
cout << pq.top(); // 5
pq.pop();
Min Heap
priority_queue<int, vector<int>, greater<int>> pq;
pq.push(3); pq.push(1); pq.push(5);
cout << pq.top(); // 1
Custom Comparator
struct cmp{
bool operator()(pii a, pii b){
return a.fi > b.fi; // Min theo first
}
};
priority_queue<pii, vector<pii>, cmp> pq;
Ứng dụng: Gộp k mảng đã sort
// Mỗi mảng đã sort, gộp thành 1 mảng sort
struct Node{ int val, row, idx; };
priority_queue<Node, vector<Node>, decltype([](Node a, Node b){
return a.val > b.val;
})> pq;
Ứng dụng: Top k phần tử lớn nhất
priority_queue<int, vector<int>, greater<int>> pq;
for (int x : a){
pq.push(x);
if (pq.size() > k) pq.pop();
}
// pq chứa k phần tử lớn nhất
Ứng dụng: Median của stream
priority_queue<int> leftPQ; // Max-heap chứa nửa nhỏ
priority_queue<int, vector<int>, greater<int>> rightPQ; // Min-heap nửa lớn
double median(){
if (leftPQ.size() == rightPQ.size())
return (leftPQ.top() + rightPQ.top()) / 2.0;
return leftPQ.top();
}
void addNum(int x){
if (leftPQ.empty() || x <= leftPQ.top()) leftPQ.push(x);
else rightPQ.push(x);
// Cân bằng
if (leftPQ.size() > rightPQ.size() + 1){
rightPQ.push(leftPQ.top());
leftPQ.pop();
}
if (rightPQ.size() > leftPQ.size()){
leftPQ.push(rightPQ.top());
rightPQ.pop();
}
}
DSU — Disjoint Set Union
Union-Find với path compression và union by size — gần như O(1) mỗi thao tác.
Cài đặt chuẩn
vector<int> par, sz;
void init(int n){
par.resize(n + 1);
sz.assign(n + 1, 1);
for (int i = 1; i <= n; i++) par[i] = i;
}
int find(int u){
if (par[u] == u) return u;
return par[u] = find(par[u]); // Path compression
}
bool unite(int u, int v){
u = find(u); v = find(v);
if (u == v) return false;
if (sz[u] < sz[v]) swap(u, v); // Union by size
par[v] = u;
sz[u] += sz[v];
return true;
}
Kiểm tra cùng thành phần
if (find(u) == find(v)) cout << "Cung nhom";
else cout << "Khac nhom";
DSU Rollback (không dùng path compression)
stack<pair<int,int>> hist;
vector<int> par, sz;
void init(int n){
par.resize(n + 1);
sz.assign(n + 1, 1);
for (int i = 1; i <= n; i++) par[i] = i;
}
int find(int u){
while (par[u] != u) u = par[u];
return u;
}
void unite(int u, int v){
u = find(u); v = find(v);
if (u == v){ hist.push({-1, -1}); return; }
if (sz[u] < sz[v]) swap(u, v);
hist.push({u, v});
par[v] = u;
sz[u] += sz[v];
}
void rollback(){
auto [u, v] = hist.top(); hist.pop();
if (u == -1) return;
par[v] = v;
sz[u] -= sz[v];
}
DSU với khoảng cách (Weighted DSU)
vector<int> par, sz;
vector<ll> dist; // dist[u] = khoảng cách từ u đến par[u]
int find(int u){
if (par[u] == u) return u;
int root = find(par[u]);
dist[u] += dist[par[u]];
return par[u] = root;
}
bool unite(int u, int v, ll w){ // u - v = w
int pu = find(u), pv = find(v);
if (pu == pv) return (dist[u] - dist[v]) == w;
if (sz[pu] < sz[pv]){
swap(u, v); swap(pu, pv); w = -w;
}
par[pv] = pu;
dist[pv] = dist[u] - dist[v] - w;
sz[pu] += sz[pv];
return true;
}
Ứng dụng: Đếm số thành phần liên thông
int cnt = n;
// Mỗi lần unite thành công: cnt--
// Sau tất cả: cnt = số thành phần
Segment Tree
Cây phân đoạn — truy vấn đoạn và cập nhật điểm trong O(log n).
Cài đặt cơ bản (tổng)
const int N = 1e5 + 5;
ll tree[4 * N], a[N];
void build(int node, int l, int r){
if (l == r){ tree[node] = a[l]; return; }
int mid = (l + r) / 2;
build(2 * node, l, mid);
build(2 * node + 1, mid + 1, r);
tree[node] = tree[2 * node] + tree[2 * node + 1];
}
void update(int node, int l, int r, int i, ll v){
if (l == r){ tree[node] = v; return; }
int mid = (l + r) / 2;
if (i <= mid) update(2 * node, l, mid, i, v);
else update(2 * node + 1, mid + 1, r, i, v);
tree[node] = tree[2 * node] + tree[2 * node + 1];
}
ll query(int node, int l, int r, int u, int v){
if (v < l || r < u) return 0;
if (u <= l && r <= v) return tree[node];
int mid = (l + r) / 2;
return query(2 * node, l, mid, u, v) +
query(2 * node + 1, mid + 1, r, u, v);
}
Segment Tree tìm max
void build(int node, int l, int r){
if (l == r){ tree[node] = a[l]; return; }
int mid = (l + r) / 2;
build(2 * node, l, mid);
build(2 * node + 1, mid + 1, r);
tree[node] = max(tree[2 * node], tree[2 * node + 1]);
}
ll query(int node, int l, int r, int u, int v){
if (v < l || r < u) return -LINF;
if (u <= l && r <= v) return tree[node];
int mid = (l + r) / 2;
return max(query(2 * node, l, mid, u, v),
query(2 * node + 1, mid + 1, r, u, v));
}
Segment Tree với pair (max + đếm)
struct Node{
ll val;
int cnt;
};
Node tree[4 * N];
Node combine(Node a, Node b){
if (a.val > b.val) return a;
if (a.val < b.val) return b;
return {a.val, a.cnt + b.cnt};
}
Segment Tree tìm kiếm nhị phân trên cây
// Tìm vị trí đầu tiên có tổng prefix >= k
int findKth(int node, int l, int r, ll k){
if (l == r) return l;
int mid = (l + r) / 2;
if (tree[2 * node] >= k)
return findKth(2 * node, l, mid, k);
return findKth(2 * node + 1, mid + 1, r, k - tree[2 * node]);
}
Persistent Segment Tree
struct PNode{
ll val;
int left, right;
};
vector<PNode> tree;
vector<int> roots;
int build(int l, int r){
int id = tree.size();
tree.pb({0, 0, 0});
if (l == r) return id;
int mid = (l + r) / 2;
tree[id].left = build(l, mid);
tree[id].right = build(mid + 1, r);
return id;
}
int update(int prev, int l, int r, int pos, ll val){
int id = tree.size();
tree.pb(tree[prev]);
if (l == r){ tree[id].val += val; return id; }
int mid = (l + r) / 2;
if (pos <= mid)
tree[id].left = update(tree[prev].left, l, mid, pos, val);
else
tree[id].right = update(tree[prev].right, mid + 1, r, pos, val);
tree[id].val = tree[tree[id].left].val + tree[tree[id].right].val;
return id;
}
Lazy Propagation
Cập nhật đoạn [l, r] trong O(log n) — không cần đệ quy xuống từng phần tử.
Cài đặt đầy đủ (cộng đoạn, truy vấn tổng)
const int N = 1e5 + 5;
ll tree[4 * N], lazy[4 * N];
void build(int node, int l, int r){
lazy[node] = 0;
if (l == r){ tree[node] = a[l]; return; }
int mid = (l + r) / 2;
build(2 * node, l, mid);
build(2 * node + 1, mid + 1, r);
tree[node] = tree[2 * node] + tree[2 * node + 1];
}
void push(int node, int l, int r){
if (lazy[node]){
tree[node] += lazy[node] * (r - l + 1);
if (l != r){
lazy[2 * node] += lazy[node];
lazy[2 * node + 1] += lazy[node];
}
lazy[node] = 0;
}
}
void update(int node, int l, int r, int u, int v, ll val){
push(node, l, r);
if (v < l || r < u) return;
if (u <= l && r <= v){
tree[node] += val * (r - l + 1);
if (l != r){
lazy[2 * node] += val;
lazy[2 * node + 1] += val;
}
return;
}
int mid = (l + r) / 2;
update(2 * node, l, mid, u, v, val);
update(2 * node + 1, mid + 1, r, u, v, val);
tree[node] = tree[2 * node] + tree[2 * node + 1];
}
ll query(int node, int l, int r, int u, int v){
push(node, l, r);
if (v < l || r < u) return 0;
if (u <= l && r <= v) return tree[node];
int mid = (l + r) / 2;
return query(2 * node, l, mid, u, v) +
query(2 * node + 1, mid + 1, r, u, v);
}
Lazy gán giá trị (không phải cộng)
ll lazySet[4 * N]; // -1 = không có gì
void push(int node, int l, int r){
if (lazySet[node] != -1){
tree[node] = lazySet[node] * (r - l + 1);
if (l != r){
lazySet[2 * node] = lazySet[node];
lazySet[2 * node + 1] = lazySet[node];
}
lazySet[node] = -1;
}
}
Fenwick Tree (BIT)
Ngắn gọn hơn Segment Tree, nhưng chỉ làm được 1 số thao tác.
Cơ bản
const int N = 1e5 + 5;
ll bit[N];
int n;
void update(int i, ll v){
for (; i <= n; i += i & -i) bit[i] += v;
}
ll query(int i){
ll s = 0;
for (; i > 0; i -= i & -i) s += bit[i];
return s;
}
ll range(int l, int r){
return query(r) - query(l - 1);
}
Xây dựng BIT từ mảng trong O(n)
void build(){
for (int i = 1; i <= n; i++){
bit[i] += a[i];
int j = i + (i & -i);
if (j <= n) bit[j] += bit[i];
}
}
BIT 2D
ll bit[N][N];
int n, m;
void update(int x, int y, ll v){
for (int i = x; i <= n; i += i & -i)
for (int j = y; j <= m; j += j & -j)
bit[i][j] += v;
}
ll query(int x, int y){
ll s = 0;
for (int i = x; i > 0; i -= i & -i)
for (int j = y; j > 0; j -= j & -j)
s += bit[i][j];
return s;
}
ll range2D(int x1, int y1, int x2, int y2){
return query(x2, y2) - query(x1-1, y2)
- query(x2, y1-1) + query(x1-1, y1-1);
}
BIT với cập nhật đoạn, truy vấn điểm
// Cập nhật [l, r] += v: dùng BIT trên mảng hiệu
void updateRange(int l, int r, ll v){
update(l, v);
update(r + 1, -v);
}
// Truy vấn tại i: query(i)
BIT với cập nhật đoạn, truy vấn đoạn
// Dùng 2 BIT
ll bit1[N], bit2[N];
void _update(ll* bit, int i, ll v){
for (; i <= n; i += i & -i) bit[i] += v;
}
ll _query(ll* bit, int i){
ll s = 0;
for (; i > 0; i -= i & -i) s += bit[i];
return s;
}
void updateRange(int l, int r, ll v){
_update(bit1, l, v);
_update(bit1, r+1, -v);
_update(bit2, l, v * (l-1));
_update(bit2, r+1, -v * r);
}
ll prefixSum(int i){
return _query(bit1, i) * i - _query(bit2, i);
}
ll rangeSum(int l, int r){
return prefixSum(r) - prefixSum(l-1);
}
Đếm số nghịch thế bằng BIT
ll inv = 0;
for (int i = n - 1; i >= 0; i--){
inv += query(a[i] - 1);
update(a[i], 1);
}
BIT 2D — Nâng cao
Xử lý bài toán cập nhật điểm, truy vấn hình chữ nhật.
Cài đặt cơ bản
const int N = 1005;
ll bit[N][N];
int n, m;
void update(int x, int y, ll v){
for (int i = x; i <= n; i += i & -i)
for (int j = y; j <= m; j += j & -j)
bit[i][j] += v;
}
ll query(int x, int y){
ll s = 0;
for (int i = x; i > 0; i -= i & -i)
for (int j = y; j > 0; j -= j & -j)
s += bit[i][j];
return s;
}
Sparse Table
Truy vấn min/max đoạn trong O(1) sau tiền xử lý O(n log n). Không hỗ trợ cập nhật.
Cài đặt
const int LOG = 20;
const int N = 1e5 + 5;
int st[LOG][N];
int lg[N];
void build(vector<int>& a, int n){
lg[1] = 0;
for (int i = 2; i <= n; i++) lg[i] = lg[i/2] + 1;
for (int i = 0; i < n; i++) st[0][i] = a[i];
for (int j = 1; (1 << j) <= n; j++)
for (int i = 0; i + (1 << j) <= n; i++)
st[j][i] = min(st[j-1][i], st[j-1][i + (1 << (j-1))]);
}
int query(int l, int r){
int j = lg[r - l + 1];
return min(st[j][l], st[j][r - (1 << j) + 1]);
}
Ứng dụng: Đếm số đoạn con thỏa mãn
// Đếm số đoạn [l, r] mà max - min <= K
// Dùng 2 sparse table (max + min) + 2 pointers
Trie
Cây tiền tố — chèn và tìm xâu trong O(độ dài xâu).
Cài đặt cơ bản
struct Trie{
struct Node{
int child[26];
int cnt, end;
Node(){
fill(child, child + 26, -1);
cnt = end = 0;
}
};
vector<Node> t;
Trie(){ t.pb(Node()); }
void insert(string s){
int cur = 0;
for (char c : s){
int i = c - 'a';
if (t[cur].child[i] == -1){
t[cur].child[i] = t.size();
t.pb(Node());
}
cur = t[cur].child[i];
t[cur].cnt++;
}
t[cur].end++;
}
bool search(string s){
int cur = 0;
for (char c : s){
int i = c - 'a';
if (t[cur].child[i] == -1) return false;
cur = t[cur].child[i];
}
return t[cur].end > 0;
}
int countPrefix(string s){
int cur = 0;
for (char c : s){
int i = c - 'a';
if (t[cur].child[i] == -1) return 0;
cur = t[cur].child[i];
}
return t[cur].cnt;
}
};
Trie cho số nhị phân (Binary Trie)
struct BinaryTrie{
struct Node{
int child[2];
int cnt;
Node(){ child[0] = child[1] = -1; cnt = 0; }
};
vector<Node> t;
BinaryTrie(){ t.pb(Node()); }
void insert(int x){
int cur = 0;
for (int i = 30; i >= 0; i--){
int b = (x >> i) & 1;
if (t[cur].child[b] == -1){
t[cur].child[b] = t.size();
t.pb(Node());
}
cur = t[cur].child[b];
t[cur].cnt++;
}
}
// Max XOR của x với bất kỳ số trong trie
int maxXor(int x){
int cur = 0, res = 0;
for (int i = 30; i >= 0; i--){
int b = (x >> i) & 1;
int want = 1 - b;
if (t[cur].child[want] != -1 && t[t[cur].child[want]].cnt > 0){
res |= (1 << i);
cur = t[cur].child[want];
} else {
cur = t[cur].child[b];
}
}
return res;
}
};
Ứng dụng: Đếm số cặp XOR > K
// Dùng binary trie, với mỗi x đếm số y đã chèn mà x^y > K
Ordered Set (PBDS)
Set có thể tìm phần tử thứ k trong O(log n) — không có trong STL chuẩn.
#include <ext/pb_ds/assoc_container.hpp>
#include <ext/pb_ds/tree_policy.hpp>
using namespace __gnu_pbds;
template<class T>
using ordered_set = tree<T, null_type, less<T>,
rb_tree_tag,
tree_order_statistics_node_update>;
int main(){
ordered_set<int> s;
s.insert(5);
s.insert(3);
s.insert(8);
s.insert(1);
// Phần tử nhỏ thứ k (0-indexed)
cout << *s.find_by_order(0); // 1
cout << *s.find_by_order(2); // 5
// Số phần tử < x
cout << s.order_of_key(5); // 2 (1, 3)
// Số phần tử <= x
cout << s.order_of_key(6); // 3
}
Ứng dụng: Đếm số cặp nghịch thế online
ordered_set<int> s;
ll inv = 0;
for (int i = 0; i < n; i++){
inv += s.size() - s.order_of_key(a[i] + 1);
s.insert(a[i]);
}
Biểu diễn đồ thị
Danh sách kề
// Vô hướng
vector<vector<int>> adj(n + 1);
adj[u].pb(v);
adj[v].pb(u);
// Có hướng: chỉ adj[u].pb(v);
// Có trọng số
vector<vector<pii>> adj(n + 1);
adj[u].pb({v, w});
Ma trận kề
int g[N][N];
g[u][v] = 1; // Có cạnh
Cạnh có trọng số
struct Edge{
int u, v;
ll w;
};
vector<Edge> edges;
Lưới (Grid)
int n, m;
vector<string> grid(n);
// 4 hướng
int dx[] = {-1, 1, 0, 0};
int dy[] = {0, 0, -1, 1};
// 8 hướng
int dx8[] = {-1,-1,-1, 0, 0, 1, 1, 1};
int dy8[] = {-1, 0, 1,-1, 1,-1, 0, 1};
BFS - DFS
BFS trên đồ thị
vector<int> dist(n + 1, -1);
queue<int> q;
q.push(s);
dist[s] = 0;
while (!q.empty()){
int u = q.front(); q.pop();
for (int v : adj[u]){
if (dist[v] == -1){
dist[v] = dist[u] + 1;
q.push(v);
}
}
}
DFS đệ quy
vector<bool> visited(n + 1, false);
void dfs(int u){
visited[u] = true;
for (int v : adj[u]){
if (!visited[v]) dfs(v);
}
}
DFS stack (tránh stack overflow)
void dfsIter(int s){
vector<bool> visited(n + 1, false);
stack<int> st;
st.push(s);
while (!st.empty()){
int u = st.top(); st.pop();
if (visited[u]) continue;
visited[u] = true;
for (int v : adj[u])
if (!visited[v]) st.push(v);
}
}
Đếm số thành phần liên thông
int cnt = 0;
for (int i = 1; i <= n; i++){
if (!visited[i]){
cnt++;
dfs(i);
}
}
Đếm số đảo trên lưới
void dfs(int x, int y){
grid[x][y] = '0';
for (int d = 0; d < 4; d++){
int nx = x + dx[d], ny = y + dy[d];
if (nx < 0 || nx >= n || ny < 0 || ny >= m) continue;
if (grid[nx][ny] == '1') dfs(nx, ny);
}
}
int countIslands(){
int cnt = 0;
for (int i = 0; i < n; i++)
for (int j = 0; j < m; j++)
if (grid[i][j] == '1'){
cnt++;
dfs(i, j);
}
return cnt;
}
BFS trên lưới — tìm đường ngắn nhất
int bfsGrid(int sx, int sy, int ex, int ey){
vector<vector<int>> dist(n, vector<int>(m, -1));
queue<pii> q;
q.push({sx, sy});
dist[sx][sy] = 0;
while (!q.empty()){
auto [x, y] = q.front(); q.pop();
if (x == ex && y == ey) return dist[x][y];
for (int d = 0; d < 4; d++){
int nx = x + dx[d], ny = y + dy[d];
if (nx < 0 || nx >= n || ny < 0 || ny >= m) continue;
if (grid[nx][ny] == '#' || dist[nx][ny] != -1) continue;
dist[nx][ny] = dist[x][y] + 1;
q.push({nx, ny});
}
}
return -1;
}
Multi-source BFS
// Nhiều nguồn cùng lúc
vector<int> dist(n + 1, -1);
queue<int> q;
for (int s : sources){
dist[s] = 0;
q.push(s);
}
while (!q.empty()){
int u = q.front(); q.pop();
for (int v : adj[u]){
if (dist[v] == -1){
dist[v] = dist[u] + 1;
q.push(v);
}
}
}
0-1 BFS (cạnh trọng 0 hoặc 1)
vector<int> dist(n + 1, INT_MAX);
deque<int> dq;
dist[s] = 0;
dq.push_front(s);
while (!dq.empty()){
int u = dq.front(); dq.pop_front();
for (auto [v, w] : adj[u]){
if (dist[u] + w < dist[v]){
dist[v] = dist[u] + w;
if (w == 0) dq.push_front(v);
else dq.push_back(v);
}
}
}
0-1 BFS — Chi tiết
Dùng khi cạnh có trọng số 0 hoặc 1. Tốt hơn Dijkstra về hằng số.
Ứng dụng: Xoay bảng (xoay tốn 1, không xoay tốn 0)
// Grid toàn 0/1, xoay 4 ô tốn 1, đi thẳng tốn 0
int bfs01(){
vector<vector<int>> dist(n, vector<int>(m, INT_MAX));
deque<pii> dq;
dist[0][0] = 0;
dq.push_front({0, 0});
while (!dq.empty()){
auto [x, y] = dq.front(); dq.pop_front();
for (int d = 0; d < 4; d++){
int nx = x + dx[d], ny = y + dy[d];
if (nx < 0 || nx >= n || ny < 0 || ny >= m) continue;
int w = (grid[nx][ny] == '#') ? 1 : 0;
if (dist[x][y] + w < dist[nx][ny]){
dist[nx][ny] = dist[x][y] + w;
if (w == 0) dq.push_front({nx, ny});
else dq.push_back({nx, ny});
}
}
}
return dist[n-1][m-1];
}
Topological Sort
Sắp xếp topo của DAG — thứ tự công việc.
Thuật toán Kahn (BFS)
vector<int> indeg(n + 1, 0);
for (int u = 1; u <= n; u++)
for (int v : adj[u]) indeg[v]++;
queue<int> q;
for (int i = 1; i <= n; i++)
if (indeg[i] == 0) q.push(i);
vector<int> topo;
while (!q.empty()){
int u = q.front(); q.pop();
topo.pb(u);
for (int v : adj[u])
if (--indeg[v] == 0) q.push(v);
}
// Nếu topo.size() < n → có chu trình
if (topo.size() < n) cout << "Co chu trinh";
else for (int x : topo) cout << x << " ";
Topo bằng DFS
vector<bool> visited(n + 1, false);
vector<int> topo;
void dfs(int u){
visited[u] = true;
for (int v : adj[u])
if (!visited[v]) dfs(v);
topo.pb(u);
}
// main
for (int i = 1; i <= n; i++)
if (!visited[i]) dfs(i);
reverse(all(topo));
Ứng dụng: Đường đi dài nhất trên DAG
vector<ll> dp(n + 1, 0);
for (int u : topo)
for (auto [v, w] : adj[u])
dp[v] = max(dp[v], dp[u] + w);
cout << *max_element(all(dp));
Dijkstra
Đường đi ngắn nhất từ 1 đỉnh đến tất cả — cạnh không âm.
Cài đặt chuẩn
typedef pair<ll, int> pli;
vector<ll> dijkstra(int s, int n){
vector<ll> dist(n + 1, LLONG_MAX);
priority_queue<pli, vector<pli>, greater<pli>> pq;
dist[s] = 0;
pq.push({0, s});
while (!pq.empty()){
auto [d, u] = pq.top(); pq.pop();
if (d > dist[u]) continue;
for (auto [v, w] : adj[u]){
if (dist[u] + w < dist[v]){
dist[v] = dist[u] + w;
pq.push({dist[v], v});
}
}
}
return dist;
}
Lưu vết đường đi
vector<ll> dist(n + 1, LLONG_MAX);
vector<int> par(n + 1, -1);
priority_queue<pli, vector<pli>, greater<pli>> pq;
dist[s] = 0;
pq.push({0, s});
while (!pq.empty()){
auto [d, u] = pq.top(); pq.pop();
if (d > dist[u]) continue;
for (auto [v, w] : adj[u]){
if (dist[u] + w < dist[v]){
dist[v] = dist[u] + w;
par[v] = u;
pq.push({dist[v], v});
}
}
}
// In đường đi từ s đến t
if (dist[t] == LLONG_MAX) cout << "Khong co duong";
else {
vector<int> path;
for (int u = t; u != -1; u = par[u]) path.pb(u);
reverse(all(path));
for (int u : path) cout << u << " ";
}
Bellman-Ford
Đường đi ngắn nhất có cạnh âm. Phát hiện chu trình âm.
Cài đặt
struct Edge{
int u, v;
ll w;
};
vector<ll> bellman(int s, int n, vector<Edge>& edges){
vector<ll> dist(n + 1, LLONG_MAX);
dist[s] = 0;
for (int i = 1; i < n; i++){
for (auto& e : edges){
if (dist[e.u] != LLONG_MAX &&
dist[e.u] + e.w < dist[e.v]){
dist[e.v] = dist[e.u] + e.w;
}
}
}
// Kiểm tra chu trình âm
for (auto& e : edges){
if (dist[e.u] != LLONG_MAX &&
dist[e.u] + e.w < dist[e.v]){
cout << "Co chu trinh am";
// return {}; hoặc throw
}
}
return dist;
}
Floyd-Warshall
Đường đi ngắn nhất giữa mọi cặp đỉnh. Cực ngắn gọn.
Cài đặt
const int N = 505;
ll d[N][N];
// Khởi tạo
for (int i = 1; i <= n; i++)
for (int j = 1; j <= n; j++)
d[i][j] = (i == j) ? 0 : LINF;
// Đọc cạnh
for (auto [u, v, w] : edges){
d[u][v] = min(d[u][v], w);
d[v][u] = min(d[v][u], w);
}
// Floyd
for (int k = 1; k <= n; k++)
for (int i = 1; i <= n; i++)
for (int j = 1; j <= n; j++)
if (d[i][k] + d[k][j] < d[i][j])
d[i][j] = d[i][k] + d[k][j];
Đếm số đường đi ngắn nhất
ll d[N][N], cnt[N][N];
// cnt[i][j] = số đường đi ngắn nhất
for (int k = 1; k <= n; k++)
for (int i = 1; i <= n; i++)
for (int j = 1; j <= n; j++){
if (d[i][k] + d[k][j] < d[i][j]){
d[i][j] = d[i][k] + d[k][j];
cnt[i][j] = cnt[i][k] * cnt[k][j];
} else if (d[i][k] + d[k][j] == d[i][j]){
cnt[i][j] += cnt[i][k] * cnt[k][j];
}
}
Minimum Spanning Tree
Cây khung nhỏ nhất — nối tất cả đỉnh với tổng trọng số nhỏ nhất.
Kruskal (dùng DSU)
struct Edge{
int u, v;
ll w;
};
bool cmp(Edge a, Edge b){ return a.w < b.w; }
ll kruskal(int n, vector<Edge>& edges){
sort(all(edges), cmp);
init(n);
ll mst = 0;
int cnt = 0;
for (auto& e : edges){
if (unite(e.u, e.v)){
mst += e.w;
cnt++;
}
}
if (cnt < n - 1) return -1; // Không liên thông
return mst;
}
Prim (dùng heap)
ll prim(int s, int n){
vector<bool> visited(n + 1, false);
priority_queue<pli, vector<pli>, greater<pli>> pq;
pq.push({0, s});
ll mst = 0;
int cnt = 0;
while (!pq.empty() && cnt < n){
auto [w, u] = pq.top(); pq.pop();
if (visited[u]) continue;
visited[u] = true;
mst += w;
cnt++;
for (auto [v, wt] : adj[u]){
if (!visited[v]) pq.push({wt, v});
}
}
if (cnt < n) return -1;
return mst;
}
Ứng dụng: Maximum Spanning Tree
// Đổi dấu trọng số và chạy MST
// Hoặc sort giảm dần trong Kruskal
Thành phần liên thông mạnh (SCC)
Tarjan
int timer = 0, sccCnt = 0;
vector<int> num(n + 1, 0), low(n + 1, 0), id(n + 1, -1);
stack<int> st;
vector<bool> onSt(n + 1, false);
void dfs(int u){
num[u] = low[u] = ++timer;
st.push(u);
onSt[u] = true;
for (int v : adj[u]){
if (!num[v]){
dfs(v);
low[u] = min(low[u], low[v]);
} else if (onSt[v]){
low[u] = min(low[u], num[v]);
}
}
if (low[u] == num[u]){
int v;
do {
v = st.top(); st.pop();
onSt[v] = false;
id[v] = sccCnt;
} while (v != u);
sccCnt++;
}
}
int main(){
for (int i = 1; i <= n; i++)
if (!num[i]) dfs(i);
// sccCnt = số SCC
}
Kosaraju
vector<vector<int>> adj, radj;
vector<bool> visited;
vector<int> order;
void dfs1(int u){
visited[u] = true;
for (int v : adj[u])
if (!visited[v]) dfs1(v);
order.pb(u);
}
void dfs2(int u, int id){
visited[u] = true;
component[u] = id;
for (int v : radj[u])
if (!visited[v]) dfs2(v, id);
}
int main(){
for (int i = 1; i <= n; i++)
if (!visited[i]) dfs1(i);
reverse(all(order));
fill(all(visited), false);
int scc = 0;
for (int u : order){
if (!visited[u]){
dfs2(u, scc);
scc++;
}
}
}
Ứng dụng: Condensation Graph
// Rút gọn mỗi SCC thành 1 đỉnh
// Xây đồ thị mới giữa các SCC
vector<set<int>> cadj(sccCnt);
for (int u = 1; u <= n; u++)
for (int v : adj[u])
if (id[u] != id[v])
cadj[id[u]].insert(id[v]);
Cầu và khớp
Cầu: cạnh mà bỏ đi làm tăng số thành phần liên thông. Khớp: đỉnh tương tự.
Cài đặt Tarjan
int timer = 0;
vector<int> num(n + 1, 0), low(n + 1, 0);
vector<pii> bridges;
vector<int> articulations;
void dfs(int u, int p){
num[u] = low[u] = ++timer;
int children = 0;
for (int v : adj[u]){
if (v == p) continue;
if (!num[v]){
dfs(v, u);
low[u] = min(low[u], low[v]);
children++;
if (low[v] > num[u]) bridges.pb({u, v});
if (p != -1 && low[v] >= num[u]) articulations.pb(u);
} else {
low[u] = min(low[u], num[v]);
}
}
if (p == -1 && children > 1) articulations.pb(u);
}
Ứng dụng: Đếm số cầu
// Sau khi chạy dfs(1, -1), in bridges.size()
Đồ thị 2 phía (Bipartite)
Đồ thị tô được 2 màu sao cho 2 đỉnh kề khác màu.
Kiểm tra bằng BFS/DFS
vector<int> color(n + 1, -1);
bool ok = true;
function<void(int, int)> dfs = [&](int u, int c){
color[u] = c;
for (int v : adj[u]){
if (color[v] == -1) dfs(v, 1 - c);
else if (color[v] == c) ok = false;
}
};
for (int i = 1; i <= n; i++)
if (color[i] == -1) dfs(i, 0);
if (ok) cout << "Bipartite";
Ứng dụng: Tô màu 2 phía
// Đếm số đỉnh màu 0 và màu 1
int cnt0 = 0, cnt1 = 0;
for (int i = 1; i <= n; i++){
if (color[i] == 0) cnt0++;
else cnt1++;
}
LCA — Lowest Common Ancestor
Tổ tiên chung gần nhất của 2 đỉnh trong cây.
Binary Lifting
const int LOG = 20;
const int N = 1e5 + 5;
int up[LOG][N];
int depth[N];
void dfs(int u, int p){
up[0][u] = p;
for (int j = 1; j < LOG; j++)
up[j][u] = up[j-1][up[j-1][u]];
for (int v : adj[u])
if (v != p){
depth[v] = depth[u] + 1;
dfs(v, u);
}
}
int lca(int u, int v){
if (depth[u] < depth[v]) swap(u, v);
int diff = depth[u] - depth[v];
for (int j = 0; j < LOG; j++)
if (diff >> j & 1) u = up[j][u];
if (u == v) return u;
for (int j = LOG - 1; j >= 0; j--)
if (up[j][u] != up[j][v]){
u = up[j][u];
v = up[j][v];
}
return up[0][u];
}
int distTree(int u, int v){
int w = lca(u, v);
return depth[u] + depth[v] - 2 * depth[w];
}
LCA bằng Euler Tour + Sparse Table
// Duyệt cây, lưu thời điểm vào/ra
// Khoảng cách LCA(u, v) = min depth trong khoảng [tin[u], tin[v]]
Ứng dụng: Đếm cạnh trên đường đi
int edgeCount = depth[u] + depth[v] - 2 * depth[lca(u, v)];
Heavy-Light Decomposition
Chia cây thành các chuỗi để truy vấn đường đi trong O(log² n).
Cài đặt
vector<int> par, depth, sz, heavy, head, pos;
int curPos = 0;
void dfs1(int u){
sz[u] = 1;
for (int v : adj[u]) if (v != par[u]){
par[v] = u;
depth[v] = depth[u] + 1;
dfs1(v);
sz[u] += sz[v];
if (sz[v] > sz[heavy[u]]) heavy[u] = v;
}
}
void dfs2(int u, int h){
head[u] = h;
pos[u] = ++curPos;
if (heavy[u]) dfs2(heavy[u], h);
for (int v : adj[u])
if (v != par[u] && v != heavy[u]) dfs2(v, v);
}
// Truy vấn đường đi u-v
ll queryPath(int u, int v){
ll res = 0;
while (head[u] != head[v]){
if (depth[head[u]] < depth[head[v]]) swap(u, v);
res += segQuery(pos[head[u]], pos[u]);
u = par[head[u]];
}
if (depth[u] > depth[v]) swap(u, v);
res += segQuery(pos[u], pos[v]);
return res;
}
Ứng dụng: Cập nhật đường đi
void updatePath(int u, int v, ll val){
while (head[u] != head[v]){
if (depth[head[u]] < depth[head[v]]) swap(u, v);
segUpdate(pos[head[u]], pos[u], val);
u = par[head[u]];
}
if (depth[u] > depth[v]) swap(u, v);
segUpdate(pos[u], pos[v], val);
}
Max Flow — Dinic
Luồng cực đại trên mạng — dùng cho bài toán matching, cắt cực tiểu.
Cài đặt Dinic
struct Dinic{
struct Edge{
int to, rev;
ll cap;
};
vector<vector<Edge>> g;
vector<int> level, iter;
int n;
Dinic(int n) : n(n), g(n), level(n), iter(n) {}
void addEdge(int u, int v, ll cap){
g[u].pb({v, (int)g[v].size(), cap});
g[v].pb({u, (int)g[u].size() - 1, 0});
}
bool bfs(int s, int t){
fill(all(level), -1);
queue<int> q;
level[s] = 0;
q.push(s);
while (!q.empty()){
int u = q.front(); q.pop();
for (auto& e : g[u]){
if (e.cap > 0 && level[e.to] == -1){
level[e.to] = level[u] + 1;
q.push(e.to);
}
}
}
return level[t] != -1;
}
ll dfs(int u, int t, ll f){
if (u == t) return f;
for (int& i = iter[u]; i < g[u].size(); i++){
auto& e = g[u][i];
if (e.cap > 0 && level[u] + 1 == level[e.to]){
ll d = dfs(e.to, t, min(f, e.cap));
if (d > 0){
e.cap -= d;
g[e.to][e.rev].cap += d;
return d;
}
}
}
return 0;
}
ll maxFlow(int s, int t){
ll flow = 0;
while (bfs(s, t)){
fill(all(iter), 0);
ll f;
while ((f = dfs(s, t, LLONG_MAX)) > 0) flow += f;
}
return flow;
}
};
Ứng dụng: Min Cut = Max Flow
// Cắt cực tiểu = luồng cực đại (định lý max-flow min-cut)
Ứng dụng: Bipartite Matching
// Tạo S -> left, left -> right, right -> T
// Tất cả cạnh cap = 1
// Max flow = matching cực đại
Quy hoạch động cơ bản
Chia bài toán lớn thành bài toán con, lưu kết quả để tránh tính lại.
Đếm số cách leo cầu thang
// Bước 1 hoặc 2, đếm số cách đi hết n bậc
int climbStairs(int n){
if (n <= 2) return n;
int a = 1, b = 2;
for (int i = 3; i <= n; i++){
int c = a + b;
a = b;
b = c;
}
return b;
}
Đổi tiền — Coin Change
// Số đồng ít nhất để đổi amount
vector<int> dp(amount + 1, INT_MAX);
dp[0] = 0;
for (int i = 1; i <= amount; i++)
for (int c : coins)
if (i >= c && dp[i-c] != INT_MAX)
dp[i] = min(dp[i], dp[i-c] + 1);
cout << (dp[amount] == INT_MAX ? -1 : dp[amount]);
Đếm số cách đổi tiền
vector<ll> dp(amount + 1, 0);
dp[0] = 1;
for (int c : coins)
for (int i = c; i <= amount; i++)
dp[i] += dp[i-c];
cout << dp[amount];
Số cách đi trên lưới
// Từ (1,1) đến (n,m), chỉ đi phải/xuống
vector<vector<ll>> dp(n + 1, vector<ll>(m + 1, 0));
dp[1][1] = 1;
for (int i = 1; i <= n; i++)
for (int j = 1; j <= m; j++){
if (i == 1 && j == 1) continue;
dp[i][j] = dp[i-1][j] + dp[i][j-1];
}
cout << dp[n][m];
Số cách đi có vật cản
vector<vector<ll>> dp(n, vector<ll>(m, 0));
dp[0][0] = 1;
for (int i = 0; i < n; i++)
for (int j = 0; j < m; j++){
if (grid[i][j] == '#'){ dp[i][j] = 0; continue; }
if (i > 0) dp[i][j] += dp[i-1][j];
if (j > 0) dp[i][j] += dp[i][j-1];
}
Đường đi tổng nhỏ nhất
vector<vector<ll>> dp(n, vector<ll>(m, 0));
dp[0][0] = a[0][0];
for (int i = 0; i < n; i++)
for (int j = 0; j < m; j++){
if (i == 0 && j == 0) continue;
ll mn = LINF;
if (i > 0) mn = min(mn, dp[i-1][j]);
if (j > 0) mn = min(mn, dp[i][j-1]);
dp[i][j] = mn + a[i][j];
}
Nhà thám hiểm (House Robber)
// Không được trộm 2 nhà liền kề
ll rob(vector<int>& a){
ll prev2 = 0, prev1 = 0;
for (int x : a){
ll cur = max(prev1, prev2 + x);
prev2 = prev1;
prev1 = cur;
}
return prev1;
}
Nhà thám hiểm vòng tròn
// Nhà 0 và nhà n-1 kề nhau
ll robCircle(vector<int>& a){
if (a.size() == 1) return a[0];
vector<int> v1(a.begin(), a.end() - 1);
vector<int> v2(a.begin() + 1, a.end());
return max(rob(v1), rob(v2));
}
Biến đổi xâu
// Xóa 1 ký tự, chèn, thay thế tốn 1 phép
// Tính số phép ít nhất biến s1 → s2 (Edit Distance)
int editDistance(string a, string b){
int n = a.size(), m = b.size();
vector<vector<int>> dp(n + 1, vector<int>(m + 1, 0));
for (int i = 0; i <= n; i++) dp[i][0] = i;
for (int j = 0; j <= m; j++) dp[0][j] = j;
for (int i = 1; i <= n; i++)
for (int j = 1; j <= m; j++){
if (a[i-1] == b[j-1]) dp[i][j] = dp[i-1][j-1];
else dp[i][j] = 1 + min({dp[i-1][j-1], dp[i-1][j], dp[i][j-1]});
}
return dp[n][m];
}
LIS — Dãy con tăng dài nhất
O(n²)
vector<int> dp(n, 1);
for (int i = 1; i < n; i++)
for (int j = 0; j < i; j++)
if (a[j] < a[i])
dp[i] = max(dp[i], dp[j] + 1);
cout << *max_element(all(dp));
O(n log n)
vector<int> v;
for (int x : a){
auto it = lower_bound(all(v), x);
if (it == v.end()) v.pb(x);
else *it = x;
}
cout << v.size();
// strictly increasing: lower_bound
// non-decreasing: upper_bound
LIS có truy vết
vector<int> dp(n, 1), par(n, -1);
for (int i = 1; i < n; i++)
for (int j = 0; j < i; j++)
if (a[j] < a[i] && dp[j] + 1 > dp[i]){
dp[i] = dp[j] + 1;
par[i] = j;
}
int best = max_element(all(dp)) - dp.begin();
vector<int> path;
for (int i = best; i != -1; i = par[i]) path.pb(a[i]);
reverse(all(path));
Longest Bitonic Subsequence
// Tăng rồi giảm
vector<int> inc(n, 1), dec(n, 1);
for (int i = 1; i < n; i++)
for (int j = 0; j < i; j++)
if (a[j] < a[i]) inc[i] = max(inc[i], inc[j] + 1);
for (int i = n - 2; i >= 0; i--)
for (int j = n - 1; j > i; j--)
if (a[j] < a[i]) dec[i] = max(dec[i], dec[j] + 1);
int best = 0;
for (int i = 0; i < n; i++)
best = max(best, inc[i] + dec[i] - 1);
Đếm số LIS
vector<int> len(n, 1);
vector<ll> cnt(n, 1);
for (int i = 1; i < n; i++)
for (int j = 0; j < i; j++){
if (a[j] < a[i]){
if (len[j] + 1 > len[i]){
len[i] = len[j] + 1;
cnt[i] = cnt[j];
} else if (len[j] + 1 == len[i]){
cnt[i] += cnt[j];
}
}
}
int mx = *max_element(all(len));
ll total = 0;
for (int i = 0; i < n; i++) if (len[i] == mx) total += cnt[i];
LCS — Dãy con chung dài nhất
Cơ bản O(n·m)
int lcs(string a, string b){
int n = a.size(), m = b.size();
vector<vector<int>> dp(n + 1, vector<int>(m + 1, 0));
for (int i = 1; i <= n; i++)
for (int j = 1; j <= m; j++){
if (a[i-1] == b[j-1]) dp[i][j] = dp[i-1][j-1] + 1;
else dp[i][j] = max(dp[i-1][j], dp[i][j-1]);
}
return dp[n][m];
}
LCS có truy vết
string lcsPath(string a, string b){
int n = a.size(), m = b.size();
vector<vector<int>> dp(n + 1, vector<int>(m + 1, 0));
for (int i = 1; i <= n; i++)
for (int j = 1; j <= m; j++){
if (a[i-1] == b[j-1]) dp[i][j] = dp[i-1][j-1] + 1;
else dp[i][j] = max(dp[i-1][j], dp[i][j-1]);
}
string res;
int i = n, j = m;
while (i > 0 && j > 0){
if (a[i-1] == b[j-1]){ res += a[i-1]; i--; j--; }
else if (dp[i-1][j] >= dp[i][j-1]) i--;
else j--;
}
reverse(all(res));
return res;
}
LCS dùng O(min(n,m)) bộ nhớ
int lcsCompact(string a, string b){
if (a.size() < b.size()) swap(a, b);
int n = a.size(), m = b.size();
vector<int> prev(m + 1, 0), cur(m + 1, 0);
for (int i = 1; i <= n; i++){
for (int j = 1; j <= m; j++){
if (a[i-1] == b[j-1]) cur[j] = prev[j-1] + 1;
else cur[j] = max(prev[j], cur[j-1]);
}
swap(prev, cur);
}
return prev[m];
}
Shortest Common Supersequence
// Xâu ngắn nhất chứa cả a và b làm xâu con
int scs(string a, string b){
return a.size() + b.size() - lcs(a, b);
}
Ba lô (Knapsack)
0/1 Knapsack
vector<int> dp(W + 1, 0);
for (int i = 0; i < n; i++)
for (int j = W; j >= w[i]; j--)
dp[j] = max(dp[j], dp[j-w[i]] + v[i]);
cout << dp[W];
Unbounded Knapsack
for (int i = 0; i < n; i++)
for (int j = w[i]; j <= W; j++)
dp[j] = max(dp[j], dp[j-w[i]] + v[i]);
Bounded Knapsack (mỗi món có số lượng giới hạn)
// Chia món thành các nhóm 1, 2, 4, 8,... (binary decomposition)
for (int i = 0; i < n; i++){
int k = cnt[i];
for (int b = 1; k > 0; b *= 2){
int take = min(b, k);
k -= take;
for (int j = W; j >= take * w[i]; j--)
dp[j] = max(dp[j], dp[j - take * w[i]] + take * v[i]);
}
}
Đếm số cách chọn
vector<ll> dp(W + 1, 0);
dp[0] = 1;
for (int i = 0; i < n; i++)
for (int j = W; j >= w[i]; j--)
dp[j] += dp[j-w[i]];
Ba lô có in ra các món chọn
vector<vector<int>> dp(n + 1, vector<int>(W + 1, 0));
for (int i = 1; i <= n; i++)
for (int j = 0; j <= W; j++){
dp[i][j] = dp[i-1][j];
if (j >= w[i-1])
dp[i][j] = max(dp[i][j], dp[i-1][j-w[i-1]] + v[i-1]);
}
// Truy vết
int j = W;
vector<int> picked;
for (int i = n; i > 0; i--){
if (dp[i][j] != dp[i-1][j]){
picked.pb(i-1);
j -= w[i-1];
}
}
Kadane — Đoạn con tổng lớn nhất
Cơ bản
ll best = a[0], cur = a[0];
for (int i = 1; i < n; i++){
cur = max((ll)a[i], cur + a[i]);
best = max(best, cur);
}
cout << best;
Tìm vị trí đoạn
ll best = a[0], cur = a[0];
int start = 0, end = 0, s = 0;
for (int i = 1; i < n; i++){
if (cur + a[i] < a[i]){
cur = a[i];
s = i;
} else {
cur += a[i];
}
if (cur > best){
best = cur;
start = s;
end = i;
}
}
Tổng con lớn nhất trên vòng tròn
ll maxSubarrayCircular(vector<int>& a){
ll total = 0;
for (int x : a) total += x;
ll best1 = a[0], cur = a[0];
for (int i = 1; i < a.size(); i++){
cur = max((ll)a[i], cur + a[i]);
best1 = max(best1, cur);
}
ll best2 = a[0], cur2 = a[0];
for (int i = 1; i < a.size(); i++){
cur2 = min((ll)a[i], cur2 + a[i]);
best2 = min(best2, cur2);
}
if (best1 < 0) return best1;
return max(best1, total - best2);
}
Đoạn con có tổng = 0 dài nhất
map<ll,int> firstPos;
firstPos[0] = -1;
ll sum = 0;
int best = 0;
for (int i = 0; i < n; i++){
sum += a[i];
if (firstPos.count(sum)) best = max(best, i - firstPos[sum]);
else firstPos[sum] = i;
}
Max sum subarray có độ dài <= K
// Dùng prefix sum + deque min
vector<ll> pre(n + 1, 0);
for (int i = 1; i <= n; i++) pre[i] = pre[i-1] + a[i-1];
deque<int> dq;
ll best = LLONG_MIN;
for (int i = 0; i <= n; i++){
while (!dq.empty() && dq.front() < i - K) dq.pop_front();
if (!dq.empty()) best = max(best, pre[i] - pre[dq.front()]);
while (!dq.empty() && pre[dq.back()] >= pre[i]) dq.pop_back();
dq.push_back(i);
}
Bitmask DP
Dùng bit để biểu diễn tập hợp — thường gặp với n ≤ 20.
Traveling Salesman Problem
int n;
int dist[20][20];
int dp[1 << 20][20];
int tsp(int mask, int u){
if (mask == (1 << n) - 1) return dist[u][0];
if (dp[mask][u] != -1) return dp[mask][u];
int res = INT_MAX;
for (int v = 0; v < n; v++)
if (!(mask & (1 << v)))
res = min(res, dist[u][v] + tsp(mask | (1 << v), v));
return dp[mask][u] = res;
}
int main(){
memset(dp, -1, sizeof(dp));
cout << tsp(1, 0);
}
Đếm số tập con có tổng = S
ll dp[1 << 20];
dp[0] = 1;
for (int mask = 1; mask < (1 << n); mask++){
int sum = 0;
for (int i = 0; i < n; i++)
if (mask & (1 << i)) sum += a[i];
if (sum == S) /* đếm */;
}
DP trên tập hợp (Assignment Problem)
// Chia n việc cho n người, chi phí cost[i][j]
vector<int> dp(1 << n, INT_MAX);
dp[0] = 0;
for (int mask = 0; mask < (1 << n); mask++){
int i = __builtin_popcount(mask);
if (i >= n) continue;
for (int j = 0; j < n; j++){
if (!(mask & (1 << j))){
int nmask = mask | (1 << j);
dp[nmask] = min(dp[nmask], dp[mask] + cost[i][j]);
}
}
}
cout << dp[(1 << n) - 1];
Duyệt mọi tập con của mask
for (int sub = mask; sub; sub = (sub - 1) & mask){
// Xử lý sub là tập con của mask
// Số tập con: 2^popcount(mask)
}
Digit DP
Đếm số thỏa mãn điều kiện trong đoạn [L, R] — dựa vào từng chữ số.
Template chung
string s;
ll dp[20][2][2]; // [vị trí][tight][đã bắt đầu chưa]
ll solve(int pos, bool tight, bool started){
if (pos == s.size()) return 1; // Hoặc 0 tùy bài
if (dp[pos][tight][started] != -1)
return dp[pos][tight][started];
int lim = tight ? s[pos] - '0' : 9;
ll res = 0;
for (int d = 0; d <= lim; d++){
bool ntight = tight && (d == lim);
bool nstarted = started || (d != 0);
// Kiểm tra điều kiện tại đây
res += solve(pos + 1, ntight, nstarted);
}
return dp[pos][tight][started] = res;
}
ll count(ll N){
if (N < 0) return 0;
s = to_string(N);
memset(dp, -1, sizeof(dp));
return solve(0, true, false);
}
// Đếm [L, R] = count(R) - count(L-1)
Đếm số không chứa chữ số 4
ll solve(int pos, bool tight, bool started){
if (pos == s.size()) return started ? 1 : 1;
if (dp[pos][tight][started] != -1) return dp[pos][tight][started];
int lim = tight ? s[pos] - '0' : 9;
ll res = 0;
for (int d = 0; d <= lim; d++){
if (d == 4) continue;
bool ntight = tight && (d == lim);
res += solve(pos + 1, ntight, true);
}
return dp[pos][tight][started] = res;
}
Đếm số có tổng chữ số = K
ll dp[20][180][2];
ll solve(int pos, int sum, bool tight){
if (pos == s.size()) return sum == K;
if (dp[pos][sum][tight] != -1) return dp[pos][sum][tight];
int lim = tight ? s[pos] - '0' : 9;
ll res = 0;
for (int d = 0; d <= lim; d++)
res += solve(pos + 1, sum + d, tight && (d == lim));
return dp[pos][sum][tight] = res;
}
Tree DP
DP trên cây — dùng đệ quy DFS.
Đường kính cây
int diameter = 0;
int dfs(int u, int p){
int mx1 = 0, mx2 = 0;
for (int v : adj[u]) if (v != p){
int h = dfs(v, u) + 1;
if (h > mx1){ mx2 = mx1; mx1 = h; }
else if (h > mx2) mx2 = h;
}
diameter = max(diameter, mx1 + mx2);
return mx1;
}
Đếm số node của subtree
vector<int> sz(n + 1);
void dfs(int u, int p){
sz[u] = 1;
for (int v : adj[u]) if (v != p){
dfs(v, u);
sz[u] += sz[v];
}
}
Chọn tập độc lập lớn nhất trên cây
// Không chọn 2 đỉnh kề nhau
vector<vector<ll>> dp(n + 1, vector<ll>(2, 0));
void dfs(int u, int p){
dp[u][0] = 0;
dp[u][1] = a[u]; // Trọng số đỉnh u
for (int v : adj[u]) if (v != p){
dfs(v, u);
dp[u][0] += max(dp[v][0], dp[v][1]);
dp[u][1] += dp[v][0];
}
}
cout << max(dp[1][0], dp[1][1]);
Đếm số đường đi có tổng = K
// Dùng centroid decomposition hoặc dsu on tree
Rerooting — tính DP cho mọi gốc
vector<int> down(n + 1), up(n + 1);
void dfs1(int u, int p){
down[u] = 0;
for (int v : adj[u]) if (v != p){
dfs1(v, u);
down[u] = max(down[u], down[v] + 1);
}
}
void dfs2(int u, int p, int upVal){
up[u] = upVal;
// Tìm 2 giá trị down lớn nhất
int mx1 = 0, mx2 = 0;
for (int v : adj[u]) if (v != p){
if (down[v] + 1 > mx1){ mx2 = mx1; mx1 = down[v] + 1; }
else if (down[v] + 1 > mx2) mx2 = down[v] + 1;
}
for (int v : adj[u]) if (v != p){
int pass = (down[v] + 1 == mx1) ? mx2 : mx1;
dfs2(v, u, max(upVal, pass) + 1);
}
}
Interval DP
DP trên đoạn [i, j].
Palindrome dài nhất
int n = s.size();
vector<vector<int>> dp(n, vector<int>(n, 0));
for (int i = 0; i < n; i++) dp[i][i] = 1;
for (int len = 2; len <= n; len++){
for (int i = 0; i + len - 1 < n; i++){
int j = i + len - 1;
if (s[i] == s[j]){
if (len == 2) dp[i][j] = 2;
else dp[i][j] = dp[i+1][j-1] + 2;
} else {
dp[i][j] = max(dp[i+1][j], dp[i][j-1]);
}
}
}
Số phép ít nhất để palindrome
// Số ký tự cần thêm
int minInsertions(string s){
string t = s;
reverse(all(t));
return s.size() - lcs(s, t);
}
Matrix Chain Multiplication
// p[i-1] x p[i] là kích thước ma trận i
vector<vector<ll>> dp(n, vector<ll>(n, 0));
for (int len = 2; len <= n; len++){
for (int i = 0; i <= n - len; i++){
int j = i + len - 1;
dp[i][j] = LINF;
for (int k = i; k < j; k++)
dp[i][j] = min(dp[i][j],
dp[i][k] + dp[k+1][j] + p[i] * p[k+1] * p[j+1]);
}
}
Đập vỡ bóng bay (Burst Balloons)
int maxCoins(vector<int>& nums){
nums.insert(nums.begin(), 1);
nums.pb(1);
int n = nums.size();
vector<vector<int>> dp(n, vector<int>(n, 0));
for (int len = 2; len < n; len++)
for (int i = 0; i + len < n; i++){
int j = i + len;
for (int k = i + 1; k < j; k++)
dp[i][j] = max(dp[i][j],
dp[i][k] + dp[k][j] + nums[i] * nums[k] * nums[j]);
}
return dp[0][n-1];
}
Brackets Matching
// Số cách thêm ngoặc đúng cho xâu
// dp[i][j] = số cách
for (int len = 2; len <= n; len += 2)
for (int i = 0; i + len - 1 < n; i++){
int j = i + len - 1;
if ((s[i] == '(' && s[j] == ')') ||
(s[i] == '[' && s[j] == ']')){
dp[i][j] = dp[i+1][j-1];
}
}
SOS DP — Sum Over Subsets
Tính tổng các tập con trong O(2^n · n).
Cơ bản
// dp[mask] = tổng a[sub] với sub là tập con của mask
vector<ll> dp = a;
for (int i = 0; i < n; i++)
for (int mask = 0; mask < (1 << n); mask++)
if (mask & (1 << i))
dp[mask] += dp[mask ^ (1 << i)];
Superset Sum
// dp[mask] = tổng a[sup] với sup là superset của mask
vector<ll> dp = a;
for (int i = 0; i < n; i++)
for (int mask = 0; mask < (1 << n); mask++)
if (!(mask & (1 << i)))
dp[mask] += dp[mask | (1 << i)];
Đếm số cặp AND = 0
// Với mỗi mask, đếm số a[i] là tập con của mask
// Số cặp (i, j) mà a[i] & a[j] = 0 = tổng dp[mask] * cnt[~mask]
Matrix Exponentiation
Nhân ma trận nhanh để tính dãy truy hồi tuyến tính.
Fibonacci bằng ma trận
struct Mat{
ll a[2][2];
Mat operator*(Mat b){
Mat c;
for (int i = 0; i < 2; i++)
for (int j = 0; j < 2; j++){
c.a[i][j] = 0;
for (int k = 0; k < 2; k++)
c.a[i][j] = (c.a[i][j] + a[i][k] * b.a[k][j]) % MOD;
}
return c;
}
};
Mat power(Mat a, ll n){
Mat res = {{{1,0},{0,1}}};
while (n){
if (n & 1) res = res * a;
a = a * a;
n >>= 1;
}
return res;
}
ll fib(ll n){
if (n == 0) return 0;
Mat base = {{{1,1},{1,0}}};
Mat res = power(base, n);
return res.a[0][1];
}
Ma trận kích thước K bất kỳ
struct Mat{
int n;
vector<vector<ll>> a;
Mat(int _n) : n(_n), a(_n, vector<ll>(_n, 0)) {}
Mat operator*(const Mat& b){
Mat c(n);
for (int i = 0; i < n; i++)
for (int k = 0; k < n; k++) if (a[i][k])
for (int j = 0; j < n; j++)
c.a[i][j] = (c.a[i][j] + a[i][k] * b.a[k][j]) % MOD;
return c;
}
};
Mat power(Mat a, ll n){
Mat res(a.n);
for (int i = 0; i < a.n; i++) res.a[i][i] = 1;
while (n){
if (n & 1) res = res * a;
a = a * a;
n >>= 1;
}
return res;
}
Ứng dụng: Đếm đường đi độ dài k
// A^k[i][j] = số đường đi từ i đến j dài k
// (dùng ma trận kề A)
Convex Hull Trick
Tối ưu DP dạng $dp[i] = \min_j (m_j \cdot x_i + b_j)$.
Cài đặt cơ bản (slope tăng, query tăng)
struct Line{
ll m, b;
ll get(ll x){ return m * x + b; }
};
deque<Line> hull;
bool bad(Line a, Line b, Line c){
// Trả về true nếu b bị "che" bởi a và c
return (__int128)(c.b - a.b) * (a.m - b.m)
<= (__int128)(b.b - a.b) * (a.m - c.m);
}
void add(Line l){
while (hull.size() >= 2 && bad(hull[hull.size()-2], hull.back(), l))
hull.pop_back();
hull.push_back(l);
}
ll query(ll x){
while (hull.size() >= 2 && hull[0].get(x) >= hull[1].get(x))
hull.pop_front();
return hull[0].get(x);
}
Ứng dụng: DP tối ưu
// dp[i] = min_j (a[j] * b[i] + dp[j]) với b[i] tăng
// Sắp xếp mảng theo b[i] tăng dần, dùng CHT để tính
Li Chao Tree — query bất kỳ
struct Line{
ll m, b;
ll get(ll x){ return m * x + b; }
Line() : m(0), b(LINF) {}
Line(ll _m, ll _b) : m(_m), b(_b) {}
};
struct LiChao{
vector<Line> tree;
int n;
LiChao(int _n) : n(_n){ tree.assign(4 * n, Line()); }
void add(Line nw, int node, int l, int r){
int mid = (l + r) / 2;
bool left = nw.get(l) < tree[node].get(l);
bool m = nw.get(mid) < tree[node].get(mid);
if (m) swap(tree[node], nw);
if (l == r) return;
if (left != m) add(nw, 2 * node, l, mid);
else add(nw, 2 * node + 1, mid + 1, r);
}
ll query(int x, int node, int l, int r){
ll res = tree[node].get(x);
if (l == r) return res;
int mid = (l + r) / 2;
if (x <= mid) return min(res, query(x, 2 * node, l, mid));
return min(res, query(x, 2 * node + 1, mid + 1, r));
}
};
Divide & Conquer DP
Tối ưu DP khi hàm opt đơn điệu: $opt[i] \le opt[i+1]$.
vector<ll> dp, prevDP;
ll cost(int l, int r);
void compute(int l, int r, int optL, int optR){
if (l > r) return;
int mid = (l + r) / 2;
pair<ll,int> best = {LINF, -1};
for (int k = optL; k <= min(mid, optR); k++){
ll val = prevDP[k-1] + cost(k, mid);
if (val < best.first) best = {val, k};
}
dp[mid] = best.first;
compute(l, mid - 1, optL, best.second);
compute(mid + 1, r, best.second, optR);
}
Ứng dụng: Chia mảng thành k đoạn tối ưu
// dp[i][j] = min chi phí chia i phần tử đầu thành j đoạn
// Dùng D&C DP để tối ưu từ O(n²k) xuống O(nk log n)
Bit Tricks
Thao tác bit — nhanh và gọn cho nhiều bài toán.
Các thao tác cơ bản
x & 1 // Kiểm tra lẻ
x >> 1 // Chia 2
x << 1 // Nhân 2
x & (x - 1) // Xóa bit 1 phải nhất
x & -x // Lấy bit 1 phải nhất (lowbit)
x | (1 << k) // Bật bit k
x & ~(1 << k) // Tắt bit k
x ^ (1 << k) // Đảo bit k
(x >> k) & 1 // Kiểm tra bit k
x & ((1 << k) - 1) // Lấy k bit cuối
Builtin functions
__builtin_popcount(x) // Đếm bit 1 (int)
__builtin_popcountll(x) // Cho long long
__builtin_ctz(x) // Đếm bit 0 bên phải
__builtin_clz(x) // Đếm bit 0 bên trái
__builtin_parity(x) // 1 nếu số bit 1 lẻ
__builtin_ffs(x) // Vị trí bit 1 đầu tiên (1-indexed)
Duyệt mọi tập con của mask
for (int sub = mask; sub; sub = (sub - 1) & mask){
// Xử lý sub
}
Duyệt mọi tập con có k bit
int mask = (1 << k) - 1;
while (mask < (1 << n)){
// Xử lý mask
int c = mask & -mask;
int r = mask + c;
mask = (((r ^ mask) >> 2) / c) | r;
}
Gray Code
int gray(int n){ return n ^ (n >> 1); }
// Sinh dãy Gray n bit
for (int i = 0; i < (1 << n); i++)
cout << gray(i) << " ";
Đảo bit
unsigned int reverseBits(unsigned int n){
n = ((n >> 1) & 0x55555555) | ((n & 0x55555555) << 1);
n = ((n >> 2) & 0x33333333) | ((n & 0x33333333) << 2);
n = ((n >> 4) & 0x0F0F0F0F) | ((n & 0x0F0F0F0F) << 4);
n = ((n >> 8) & 0x00FF00FF) | ((n & 0x00FF00FF) << 8);
n = (n >> 16) | (n << 16);
return n;
}
STL hữu dụng
Vector
sort(all(v));
sort(all(v), greater<int>());
reverse(all(v));
*max_element(all(v));
*min_element(all(v));
accumulate(all(v), 0LL);
count(all(v), x);
unique(all(v));
next_permutation(all(v));
prev_permutation(all(v));
lower_bound(all(v), x);
upper_bound(all(v), x);
Set / Map
set<int> s;
s.insert(5);
s.count(5); // 0 hoặc 1
s.erase(5);
s.lower_bound(3); // Iterator đầu tiên >= 3
s.upper_bound(3);
*s.begin(); // Phần tử nhỏ nhất
*s.rbegin(); // Phần tử lớn nhất
map<string, int> m;
m["abc"] = 1;
m.count("abc");
for (auto [k, v] : m) cout << k << " " << v << "\n";
Unordered containers
unordered_set<int> us; // O(1) trung bình
unordered_map<int,int> um;
// Chống hack: dùng custom hash
String
s.substr(l, len);
s.find(t);
s.rfind(t);
s.replace(pos, len, t);
s.insert(pos, t);
s.erase(pos, len);
stoi(s); stoll(s); stod(s);
to_string(x);
Pair / Tuple
pair<int,int> p = {1, 2};
p.first; p.second;
auto [a, b] = p;
tuple<int,int,int> t = {1, 2, 3};
auto [x, y, z] = t;
Algorithm
min(a, b);
max(a, b);
min({a, b, c, d});
max({a, b, c, d});
swap(a, b);
__gcd(a, b); // C++14
gcd(a, b); // C++17
lcm(a, b); // C++17
clamp(x, lo, hi); // C++17
iota(all(v), 0); // Gán 0,1,2,...
Xác suất & Kỳ vọng
Kỳ vọng tuyến tính
// E[X + Y] = E[X] + E[Y] — luôn đúng
// E[X * Y] = E[X] * E[Y] — chỉ khi X, Y độc lập
// E[aX + b] = a * E[X] + b
Ví dụ: Số lần tung xúc xắc để ra 6
// E = 1/p = 6
Ví dụ: Kỳ vọng số cặp nghịch thế trong hoán vị ngẫu nhiên
// n * (n-1) / 4
// Vì mỗi cặp (i,j) có xác suất 1/2 đảo ngược
Công thức Bayes
$$P(A|B) = \frac{P(B|A) \cdot P(A)}{P(B)}$$
Ví dụ: Sinh số ngẫu nhiên
mt19937 rng(chrono::steady_clock::now().time_since_epoch().count());
int randInt(int l, int r){
return uniform_int_distribution<int>(l, r)(rng);
}
double randDouble(){
return uniform_real_distribution<double>(0, 1)(rng);
}
Bài toán: Kỳ vọng số phép biến đổi
// dp[n] = E[số bước] để từ n về 0
// dp[n] = 1 + (1/n) * sum_{i=1}^{n} dp[?] (tùy bài)
Checklist khi làm bài
- Đọc kỹ đề — Input/Output, ràng buộc, ví dụ.
- Xác định kiểu dữ liệu — int? long long? string? double?
- Edge cases — 0, số âm, giá trị biên, input rỗng, overflow.
- Tràn số — Nhân 2 số có thể vượt 10^9? Dùng long long.
- Độ phức tạp — Thuật toán có kịp thời gian? Cần tối ưu?
- Tràn bộ nhớ — Mảng có quá lớn? Dùng vector động?
- Test tay — Chạy ví dụ đề bài + edge cases.
- Đọc lại code — Tên biến, dấu chấm phẩy, ngoặc, include.
- Định dạng output — Dấu cách, xuống dòng, độ chính xác số thực.
- Nộp nhiều lần — WA rồi sửa, đừng sợ!
Debug checklist
- In các biến quan trọng ở giữa các bước để theo dõi.
- Kiểm tra mảng có bị out of range không.
- Kiểm tra vòng lặp for có bị off-by-one.
- Kiểm tra các trường hợp input đặc biệt.
- So sánh với thuật toán đơn giản (brute force) trên input nhỏ.
Câu thần chú
Tài nguyên học tập
- Luyện tập: VNOI, SPOJ, Codeforces, AtCoder, HackerRank
- USACO Guide: Roadmap học theo chủ đề từ cơ bản đến nâng cao
- CP-Algorithms: Tài liệu thuật toán chi tiết bằng tiếng Anh
- GeeksforGeeks: Ví dụ code đa dạng
Chiến lược khi thi
- Đọc hết đề trước — Xác định bài dễ, bài khó để phân bổ thời gian.
- Làm bài dễ trước — Ăn điểm chắc trước đã.
- Chia nhỏ bài khó — Ăn subtask nhỏ trước, tối ưu dần.
- Kiểm tra kỹ trước khi nộp — Đừng để WA vì lỗi ngớ ngẩn.
- Quản lý thời gian — Đừng "chết" trên một bài quá lâu.
- Giữ bình tĩnh — Căng thẳng chỉ làm giảm hiệu suất.
Kết thúc
Chúc bạn ôn tập tốt và đạt kết quả cao trong kỳ thi HSG Tin học! 🚀