C++ cho THPT HSG

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;
}
Trên hệ thống không hỗ trợ 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);
Không trộn cin/cout với scanf/printf khi đã tắt sync — thứ tự nhập xuất sẽ sai.

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ểuSizeGiới hạnFormat
char1 byte-128 → 127%c
bool1 byte0/1%d
short2 byte±3.2 × 10^4%hd
int4 byte±2.1 × 10^9%d
unsigned int4 byte0 → 4.3 × 10^9%u
long4/8 bytetùy hệ thống%ld
long long8 byte±9.2 × 10^18%lld
unsigned long long8 byte0 → 1.8 × 10^19%llu
__int12816 byte±1.7 × 10^38custom
float4 byte~7 chữ số%f
double8 byte~15 chữ số%lf
long double16 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;
}
Nhân 2 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ệuTênn=10n=10^3n=10^5n=10^6n=10^7
O(1)Hằng số11111
O(log n)Logarit310172023
O(√n)Căn33231610003162
O(n)Tuyến tính1010^310^510^610^7
O(n log n)Log-tuyến3010^41.7M20M230M
O(n²)Bình phương10010^610^10----
O(n³)Lập phương100010^9------
O(2ⁿ)Hàm mũ1024--------
O(n!)Giai thừa3.6M--------

Chọn thuật toán theo ràng buộc

Quy tắc tính

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()));
1 giây ≈ 10^8 phép tính đơn giản trên máy chấm thông thường.

Đệ 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

Đệ quy sâu quá 10^5 có thể bị stack overflow. Chuyển sang vòng lặp hoặc tăng stack size bằng compiler flag.

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();
    }
}
Số trạng thái của backtracking thường là O(2^n) hoặc O(n!). Với n > 20 thường không kịp.

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.

Greedy không phải lúc nào cũng đúng. Phải chứng minh hoặc kiểm tra kỹ trước khi áp dụng.

Đổ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 để 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;
}
Chia để trị hiệu quả khi bài toán có thể chia thành các bài con độc lập. Nếu bài con giao nhau nhiều, dùng quy hoạch động.

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;
}
Cộng, trừ, nhân số lớn đều O(n) với n là số chữ số. Nhân 2 số lớn O(n·m).

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;
}
Độ phức tạp O(log min(a,b)) — cực nhanh kể cả với số 10^18.

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;
}
Chỉ chia modulo được khi 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;
}
CRT dùng để giải hệ phương trình đồng dư — thường xuất hiện trong các bài toán số học khó.

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

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

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 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";
}
Mo's Algorithm hiệu quả khi bài toán có thể add/remove phần tử ở 2 đầu đoạn trong O(1).

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 << " ";
}
Độ phức tạp O((V + E) log V). Với E ~ V² (đồ thị dày) thì chậm hơn Floyd.

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;
}
Độ phức tạp O(V·E). Chậm hơn Dijkstra nhưng xử lý được cạnh âm.

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];
            }
        }
O(V³). Với n = 500 thì 1.25 × 10^8 phép tính — vừa 1 giây.

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

  1. Đọc kỹ đề — Input/Output, ràng buộc, ví dụ.
  2. Xác định kiểu dữ liệu — int? long long? string? double?
  3. Edge cases — 0, số âm, giá trị biên, input rỗng, overflow.
  4. Tràn số — Nhân 2 số có thể vượt 10^9? Dùng long long.
  5. Độ phức tạp — Thuật toán có kịp thời gian? Cần tối ưu?
  6. Tràn bộ nhớ — Mảng có quá lớn? Dùng vector động?
  7. Test tay — Chạy ví dụ đề bài + edge cases.
  8. Đọc lại code — Tên biến, dấu chấm phẩy, ngoặc, include.
  9. Định dạng output — Dấu cách, xuống dòng, độ chính xác số thực.
  10. Nộp nhiều lần — WA rồi sửa, đừng sợ!

Debug checklist

Câu thần chú

"Đọc đề 3 lần, code 1 lần, debug 10 lần."

Tài nguyên học tập

Chiến lược khi thi

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! 🚀