• ACOJ
  • Trang chủ
  • Problems
  • Submissions
  • Users
  • Contests
  • About
    >
    • Status
Đăng nhập  hoặc  Đăng ký

  • Bài đăng
  • Sự kiện

Tin tức

Giới thiệu Group trên Facebook

đã đăng vào 31 tháng 3 năm 2025, 1:04 p.m. 0

👨‍💻 Giới thiệu cộng đồng ACOJ – Hỏi đáp & Luyện tập lập trình

ACOJ là một nền tảng hỗ trợ học sinh, sinh viên luyện tập và thi đấu các bài lập trình thuật toán bằng C++/Python, hướng đến các kỳ thi như:

🏅 Học sinh giỏi Tin học cấp Tỉnh, Quốc Gia

🏆 Olympic Tin học Sinh viên (OLP), ICPC

💡 Các cuộc thi thuật toán, contest luyện tập định kỳ

Luyện tập với hàng trăm bài tập có chấm điểm tự động

Thảo luận, hỏi đáp và chia sẻ kinh nghiệm cùng cộng đồng trên Facebook

🔗 Group Facebook chính thức: 👉 ACOJ – Hỏi đáp & Luyện tập lập trình

Bao hàm - Bù trừ

đã đăng vào 10 tháng 4 năm 2025, 4:48 p.m. 0

Nguyên lý Bao hàm - Loại trừ (Inclusion-Exclusion Principle)

Phát biểu

Diễn đạt bằng lời:

Để tính số phần tử trong hợp của nhiều tập hợp:

  • Cộng tất cả số phần tử của từng tập hợp riêng lẻ.
  • Trừ đi số phần tử nằm trong từng giao của hai tập.
  • Cộng lại số phần tử nằm trong giao của ba tập.
  • Tiếp tục xen kẽ dấu trừ và cộng với các giao của 4, 5,... tập hợp.

Định nghĩa đầy đủ

Nguyên lý bao hàm - loại trừ là một phương pháp toán học dùng để đếm số lượng phần tử trong hợp của các tập hợp bằng cách cộng, trừ xen kẽ số lượng phần tử trong các giao của những tập hợp này để tránh việc đếm trùng lặp.

Nguyên lý này thường được sử dụng để giải quyết các bài toán đếm liên quan đến điều kiện kết hợp (OR) phức tạp.

Công thức tổng quát

Cho các tập hợp hữu hạn \(A_1, A_2, \dots, A_n\), ta có:

\[ |A_1 \cup A_2 \cup \dots \cup A_n| = \sum_{i=1}^{n} |A_i| - \sum_{1 \le i < j \le n}|A_i \cap A_j| + \sum_{1 \le i < j < k \le n}|A_i \cap A_j \cap A_k| - \dots + (-1)^{n+1}|A_1 \cap A_2 \cap \dots \cap A_n| \]

Ví dụ minh họa

Ví dụ 1

Bài toán: Có bao nhiêu số từ 1 đến 100 chia hết cho 2 hoặc 3 hoặc 5?

Giải: -\(A\): số chia hết cho 2,\(|A| = 50\)

-\(B\): số chia hết cho 3,\(|B| = 33\)

-\(C\): số chia hết cho 5,\(|C| = 20\)

-\(|A \cap B|\): chia hết cho 6,\(|A \cap B| = 16\)

-\(|A \cap C|\): chia hết cho 10,\(|A \cap C| = 10\)

-\(|B \cap C|\): chia hết cho 15,\(|B \cap C| = 6\)

-\(|A \cap B \cap C|\): chia hết cho 30,\(|A \cap B \cap C| = 3\)

Áp dụng nguyên lý: \[ |A \cup B \cup C| = 50 + 33 + 20 - 16 - 10 - 6 + 3 = 74 \]

Ví dụ 2

Bài toán: Có bao nhiêu số từ 1 đến 50 không chia hết cho 2, 3 hoặc 5?

Giải:

  • Tổng số phần tử từ 1 đến 50 là \(50\).
  • Số lượng số chia hết cho 2, 3 hoặc 5 (tương tự ví dụ 1) là \(37\).
  • Vậy số lượng số không chia hết cho 2, 3 hoặc 5 là \(50 - 37 = 13\).

Ứng dụng trong lập trình thi đấu

  • Đếm số lượng phần tử thỏa mãn các điều kiện "hoặc" phức tạp.
  • Tính toán các bài toán liên quan đến chia hết, không chia hết, tìm số nguyên tố trong khoảng.
  • Kết hợp với bitmasking để giải quyết nhanh các bài toán khi số lượng tập hợp nhỏ (\(n \leq 20\)).

Code template C++

Ví dụ 1

#include <bits/stdc++.h>
using namespace std;

typedef long long ll;

// Hàm tính số lượng các số trong khoảng [1..n] chia hết cho ít nhất một số trong tập hợp a[]
ll inclusionExclusion(ll n, vector<ll>& a) {
    ll res = 0;
    int k = a.size();
    for (int mask = 1; mask < (1 << k); ++mask) {
        ll lcm = 1;
        for (int i = 0; i < k; ++i) {
            if (mask & (1 << i)) {
                lcm = lcm / __gcd(lcm, a[i]) * a[i];
                if (lcm > n) break;
            }
        }
        if (__builtin_popcount(mask) % 2 == 1)
            res += n / lcm;
        else
            res -= n / lcm;
    }
    return res;
}

int main() {
    vector<ll> a = {2, 3, 5};
    ll n = 100;
    cout << inclusionExclusion(n, a) << endl; // Kết quả: 74
    return 0;
}

Ví dụ 2

#include <bits/stdc++.h>
using namespace std;

typedef long long ll;

// Hàm đếm số lượng số từ L đến R không chia hết cho bất kỳ số nào trong tập hợp a[]
ll countNotDivisible(ll L, ll R, vector<ll>& a) {
    auto countDivisible = [&](ll x) {
        ll res = 0;
        int k = a.size();
        for (int mask = 1; mask < (1 << k); ++mask) {
            ll lcm = 1;
            for (int i = 0; i < k; ++i) {
                if (mask & (1 << i)) {
                    lcm = lcm / __gcd(lcm, a[i]) * a[i];
                    if (lcm > x) break;
                }
            }
            if (lcm > x) continue;
            if (__builtin_popcount(mask) % 2 == 1)
                res += x / lcm;
            else
                res -= x / lcm;
        }
        return res;
    };

    return (R - L + 1) - (countDivisible(R) - countDivisible(L - 1));
}

int main() {
    vector<ll> a = {2, 3, 5};
    ll L = 1, R = 100;
    cout << countNotDivisible(L, R, a) << endl; // Số lượng số không chia hết cho 2,3,5 trong đoạn [1,100]
    return 0;
}

Mở rộng nâng cao

Công thức cho trường hợp phần bù

Khi muốn đếm số lượng phần tử không thuộc vào bất cứ tập nào, ta có thể sử dụng dạng phần bù của nguyên lý: \[ |\overline{A_1 \cup A_2 \cup \dots \cup A_n}| = |U| - |A_1 \cup A_2 \cup \dots \cup A_n| \]

Các ứng dụng nâng cao
  • Tính số nguyên tố trong đoạn (sử dụng chung với thuật toán sàng Eratosthenes)
  • Giải các bài toán lý thuyết đồ thị liên quan đến tính số lượng cách tô màu, số cách xếp hình.
  • Kết hợp với kỹ thuật khác như quy hoạch động trên bitmask, thuật toán tổ hợp, và xác suất.

Tổ hợp

đã đăng vào 7 tháng 4 năm 2025, 10:08 a.m. 0

Tổng hợp công thức tổ hợp dùng trong lập trình thi đấu

1. Giai thừa và Hoán vị

  • Giai thừa:
    \[n! = n \times (n - 1) \times \dots \times 1 \]

    • Số cách sắp xếp n phần tử phân biệt.
  • Hoán vị:
    \[ P(n, k) = \frac{n!}{(n - k)!} \]

    • Số cách chọn và sắp xếp k phần tử từ n phần tử.

2. Tổ hợp (Combination)

  • \[ C(n, k) = \frac{n!}{k!(n - k)!} \]

    • Số cách chọn k phần tử từ n phần tử.
  • Tính nhanh C(n, k) mod p:
    \[ C(n, k) \equiv \frac{fac[n]}{fac[k] \cdot fac[n-k]} \mod p \]

3. Tổ hợp lặp (Combination with repetition)

  • \[ H(n, k) = C(n + k - 1, k) \]
    • Số cách chọn k phần tử từ n loại, cho phép lặp.

4. Đếm phân hoạch số

  • \[ P(n) \] — số cách phân tích n thành tổng các số nguyên dương.

5. Nguyên lý Dirichlet (Pigeonhole Principle)

  • Nếu có n vật phân vào m hộp và n > m, thì ít nhất một hộp có ≥ 2 vật.

🐦 Định nghĩa cơ bản

Nếu bạn đặt n + 1 vật vào n hộp, thì ít nhất một hộp sẽ chứa ≥ 2 vật.


🔧 Phiên bản mở rộng

1. Phiên bản tổng quát:

Nếu có \(kn + 1\) vật và \(n\) hộp, thì ít nhất một hộp có ≥ \(k + 1\) vật.

2. Phiên bản tồn tại:

Nếu số phần tử vượt quá số nhóm phân biệt được, thì tồn tại ít nhất 2 phần tử có cùng nhóm.


📌 Ứng dụng trong các bài toán

1. Số dư chia (mod)
  • Trong dãy gồm \(n + 1\) số, tồn tại hai số có cùng số dư khi chia cho \(n\).
2. Tổng đoạn con chia hết
  • Cho dãy \(A[1..n]\), tồn tại đoạn con có tổng chia hết cho \(n\).
  • Ý tưởng: Dùng prefix sum và số dư mod n → áp dụng Dirichlet.
3. Tổng chữ số giống nhau
  • Trong \(n + 1\) số từ \(1\) đến \(2n\), tồn tại hai số có tổng chữ số giống nhau.
4. Sinh nhật trùng nhau
  • Có 367 người → tồn tại ít nhất 2 người cùng ngày sinh (năm có 366 ngày).
5. Ứng dụng trong màu sắc, bàn cờ
  • Số lượng vật nhiều hơn số cách phân biệt → có ít nhất hai vật giống nhau về thuộc tính.

🧠 Mẹo áp dụng trong lập trình thi đấu

Tình huống Có thể áp dụng Dirichlet?
Số phần tử > số nhóm ✅
Cần chứng minh tồn tại ✅
Phản chứng rằng không thể phân đủ ✅

🧪 Bài toán mẫu

Bài 1: Tổng đoạn con chia hết cho n
  • Cho mảng \(A[1..n]\), tìm đoạn con có tổng chia hết cho \(n\).
  • Giải: Dùng prefix sum, tìm hai chỉ số có cùng số dư \(\mod n\).
Bài 2: Tất cùng màu
  • Có 10 đôi tất. Rút tối thiểu bao nhiêu chiếc để chắc chắn có 1 đôi cùng màu?
  • Giải: 11 chiếc → chắc chắn có ít nhất 1 đôi (áp dụng Dirichlet).
Bài 3: Sinh nhật trùng
  • Có 367 người → tồn tại ít nhất 2 người sinh cùng ngày.

📌 Tổng kết

  • Dirichlet rất mạnh để chứng minh tồn tại.
  • Thường áp dụng trong bài toán chứng minh ngược, chia nhóm, hoặc đếm dư.

6. Nghịch đảo Fermat

  • \[ a^{-1} \equiv a^{p-2} \mod p \]

7. Công thức Lucas (khi n rất lớn, mod nguyên tố nhỏ)

  • \[ C(n, k) \mod p = C(n/p, k/p) \cdot C(n \bmod p, k \bmod p) \mod p \]

8. Số Catalan

  • \[ C_n = \frac{1}{n+1} \binom{2n}{n} = \frac{(2n)!}{n!(n+1)!} \]

9. Tam giác Pascal

  • \[ C(n, k) = C(n - 1, k - 1) + C(n - 1, k) \]

10. Derangement – Hoán vị không cố định

Derangement là số lượng hoán vị của n phần tử sao cho không phần tử nào ở đúng vị trí ban đầu.

Ví dụ với n = 3:

  • [1,2,3] ❌
  • [2,1,3] ❌
  • [3,1,2] ✅
  • [2,3,1] ✅

Suy ra: !3 = 2.


1. Công thức tính
Đệ quy:

\[ !n = (n - 1)(!(n - 1) + !(n - 2)),\quad !0 = 1,\\ !1 = 0 \]

Công thức đóng:

\[ !n = n! \cdot \left(1 - \frac{1}{1!} + \frac{1}{2!} - \cdots + \frac{(-1)^n}{n!} \right) \]

Gần đúng:

\[ !n \approx \frac{n!}{e} \]


🎩 Bài toán n chiếc mũ (Hat Check Problem)
  • Xác suất không ai nhận đúng mũ:
    \[ P = \frac{!n}{n!} \]
🔁 Ghép cặp sai hoàn toàn
  • Đếm số cách n người tặng quà mà không ai nhận từ chính người mình tặng.
🔐 Mã hóa và mật mã học
  • Tạo ánh xạ không tự trỏ (self-mapping), tăng tính an toàn.
🎲 Xáo trộn an toàn
  • Dùng trong test case, shuffle ngẫu nhiên nhưng tránh vị trí ban đầu.

Mã C++ tính Derangement

const int MAXN = 1e6 + 5;
const int MOD = 1e9 + 7;

long long der[MAXN];

void calc_derangement(int n) {
    der[0] = 1;
    der[1] = 0;
    for (int i = 2; i <= n; ++i) {
        der[i] = (i - 1) * (der[i - 1] + der[i - 2]) % MOD;
    }
}

Tổ hợp giải phương trình x₁ + x₂ + ... + xₘ = n

1. Nghiệm nguyên không âm:
  • \[ C(n + m - 1, m - 1) \]
2. Nghiệm nguyên dương:
  • \[ C(n - 1, m - 1) \]

Dưới đây là hàm tính tổ hợp \(C(n,k) \mod p\) bằng cách tiền xử lý giai thừa và nghịch đảo modular, rất chuẩn cho lập trình thi đấu:

const int MAXN = 1e6 + 5;
const int MOD = 1e9 + 7;

long long fac[MAXN], inv[MAXN];

// Hàm mũ nhanh
long long powmod(long long a, long long b) {
    long long res = 1;
    a %= MOD;
    while (b > 0) {
        if (b & 1) res = res * a % MOD;
        a = a * a % MOD;
        b >>= 1;
    }
    return res;
}

// Tiền xử lý giai thừa và nghịch đảo
void prepare() {
    fac[0] = inv[0] = 1;
    for (int i = 1; i < MAXN; ++i) {
        fac[i] = fac[i - 1] * i % MOD;
        inv[i] = powmod(fac[i], MOD - 2); // nghịch đảo theo Fermat vì MOD là số nguyên tố
    }
}

// Tính C(n, k) mod MOD
long long C(int n, int k) {
    if (k < 0 || k > n) return 0;
    return fac[n] * inv[k] % MOD * inv[n - k] % MOD;
}

Các bài toán tương tự và biến thể

1. Nghiệm có giới hạn trên: 0 ≤ xᵢ ≤ bᵢ
  • Dùng bao hàm hoặc DP.
2. Chia kẹo cho trẻ:
  • Không ràng buộc: \(C(n + m - 1, m - 1)\)
  • Mỗi đứa \( \ge 1\) viên: \(C(n - 1, m - 1)\)
3. Đếm số dãy có tổng = n với các phần tử thuộc tập cho trước
  • Dùng DP kiểu coin change.
4. Đếm số phân tích n thành tổng các số dương
  • Không phân biệt thứ tự: dùng DP.
  • Phân biệt thứ tự: như coin change.
5. Ràng buộc \(x_i \ge a_i\)
  • Đặt \(x_i = y_i + a_i \Rightarrow \sum y_i = n - \sum a_i\)
6. Chia n phần tử vào m nhóm (không rỗng)
  • Dùng Stirling số loại 2: \(S(n, m)\)
7. Sắp xếp n phần tử vào m hộp không rỗng (phân biệt)
  • \(m! \cdot S(n, m)\)
8. Phân phối có giới hạn trên
  • Dùng bao lơn hoặc DP nâng cao.

contest-olympic-30-4-2025

đã đăng vào 5 tháng 4 năm 2025, 2:34 p.m. 0

https://tritrang.dev/contest/25olp304

Lý thuyết đồ thị

đã đăng vào 1 tháng 4 năm 2025, 12:33 a.m. 0

🌐 Cheatsheet Chi Tiết: Lý Thuyết Đồ Thị & Cây (Graph & Tree Theory)


🧠 Khái Niệm Cơ Bản

Thuật ngữ Mô tả
Đỉnh (vertex) Một nút trong đồ thị hoặc cây
Cạnh (edge) Kết nối giữa hai đỉnh
Bậc (degree) Số cạnh nối vào một đỉnh (in/out degree với đồ thị có hướng)
Đường đi (path) Chuỗi đỉnh nối nhau bằng cạnh
Chu trình (cycle) Đường đi bắt đầu và kết thúc tại cùng một đỉnh
Cây (tree) Đồ thị vô hướng, liên thông, không chu trình
DAG Đồ thị có hướng không có chu trình (Directed Acyclic Graph)

📌 Ứng Dụng Đồ Thị & Cây

  • Tìm đường đi ngắn nhất: giao thông, mạng máy tính (Dijkstra, BFS)
  • Tối ưu mạng lưới: xây dựng cây khung nhỏ nhất (Kruskal, Prim)
  • Phân tích cấu trúc: SCC, Biconnected, Cầu/Khớp (Tarjan, Kosaraju)
  • Quản lý phân cấp: tổ chức, biểu đồ thừa kế → dùng cây
  • Truy vấn tổ tiên, phân đoạn cây: LCA, HLD, Euler Tour

🎯 Biểu Diễn Đồ Thị

Danh sách kề (Adjacency List) — Thường dùng
vector<vector<int>> adj(n + 1);
adj[u].push_back(v);
Ma trận kề (Adjacency Matrix) — Chỉ dùng khi đồ thị dày
vector<vector<int>> g(n + 1, vector<int>(n + 1, 0));
g[u][v] = 1;
DSU — Dùng trong Kruskal, Union-Find
vector<int> parent(n + 1);
iota(parent.begin(), parent.end(), 0);

int find(int u) {
    return u == parent[u] ? u : parent[u] = find(parent[u]);
}

void unite(int u, int v) {
    parent[find(u)] = find(v);
}

🔄 Duyệt Đồ Thị

DFS (Depth-First Search)
void dfs(int u) {
    visited[u] = true;
    for (int v : adj[u])
        if (!visited[v]) dfs(v);
}
BFS (Breadth-First Search)
void bfs(int start) {
    queue<int> q;
    q.push(start);
    visited[start] = true;
    while (!q.empty()) {
        int u = q.front(); q.pop();
        for (int v : adj[u])
            if (!visited[v]) {
                visited[v] = true;
                q.push(v);
            }
    }
}

🌲 DFS Trên Cây — Truy Vết, Kích Thước, Chiều Sâu

vector<int> parent(n + 1), depth(n + 1), size(n + 1);

void dfs_tree(int u, int p) {
    parent[u] = p;
    size[u] = 1;
    depth[u] = depth[p] + 1;
    for (int v : adj[u]) {
        if (v == p) continue;
        dfs_tree(v, u);
        size[u] += size[v];
    }
}

📉 Tìm Tổ Tiên Chung Gần Nhất (LCA - Binary Lifting)

const int LOG = 20;
int up[MAXN][LOG], tin[MAXN], tout[MAXN], timer;

void dfs_lca(int u, int p) {
    tin[u] = ++timer;
    up[u][0] = p;
    for (int i = 1; i < LOG; ++i)
        up[u][i] = up[up[u][i - 1]][i - 1];
    for (int v : adj[u])
        if (v != p) dfs_lca(v, u);
    tout[u] = ++timer;
}

bool is_ancestor(int u, int v) {
    return tin[u] <= tin[v] && tout[v] <= tout[u];
}

int lca(int u, int v) {
    if (is_ancestor(u, v)) return u;
    if (is_ancestor(v, u)) return v;
    for (int i = LOG - 1; i >= 0; --i)
        if (!is_ancestor(up[u][i], v))
            u = up[u][i];
    return up[u][0];
}

📚 Khi Nào Dùng Kỹ Thuật Nào?

Nhu cầu Thuật toán / Kỹ thuật
Duyệt đồ thị DFS / BFS
Đếm thành phần liên thông DFS
Tìm đường ngắn nhất BFS (đơn vị), Dijkstra
Tìm tổ tiên chung (LCA) Binary Lifting / RMQ
Cây khung nhỏ nhất Kruskal / Prim
Tìm chu trình DFS + back edge
Phân tích cấu trúc Tarjan, SCC, Bridge, AP
Truy vấn trên đường đi hoặc cây con Euler Tour + Segment Tree
Cập nhật động trên cây HLD, Link Cut Tree (nâng cao)

📈 Độ Phức Tạp Tổng Hợp

Thuật toán Thời gian Không gian
DFS / BFS O(V + E) O(V + E)
Kruskal (với DSU) O(E log E) O(V)
Dijkstra (PQ) O((V + E) log V) O(V + E)
Tarjan (SCC, cầu, khớp) O(V + E) O(V + E)
LCA (binary lifting) O(log N) O(N log N)

📚 Tham Khảo

  • CP-Algorithms - Graph Theory
  • CSES Graph & Tree Algorithms
  • AtCoder Educational DP Contest (Tree DP)
  • VNOI / SPOJ / Codeforces Tag "graphs", "trees"

Thành phần liên thông mạnh

đã đăng vào 1 tháng 4 năm 2025, 12:18 a.m. 0

🔗 Cheatsheet: Strongly Connected Components (SCC)

📘 Giới thiệu

Một SCC (thành phần liên thông mạnh) trong đồ thị có hướng là tập hợp các đỉnh sao cho mỗi đỉnh đều có đường đi tới mọi đỉnh còn lại trong tập.


🧠 Ứng dụng

  • Tối ưu hoá biểu đồ phụ thuộc.
  • Rút gọn đồ thị (SCC DAG).
  • Phát hiện chu trình trong đồ thị có hướng.
  • Giải bài toán 2-SAT (sử dụng Kosaraju/Tarjan).
  • Đếm số phần liên thông mạnh → mô hình hóa logic.

⏱️ Độ phức tạp

  • Tarjan: O(V + E)
  • Kosaraju: O(V + E)

✅ Ưu điểm

  • Phân tích cấu trúc đồ thị có hướng hiệu quả.
  • Cài đặt đơn giản, trực quan.
  • Hỗ trợ rút gọn đồ thị để làm các bài toán cao hơn.

❌ Nhược điểm

  • Chỉ áp dụng được cho đồ thị có hướng.
  • Nếu cần tìm SCC lớn nhất / có điều kiện, cần xử lý thêm.

🚀 C++ Code (Tarjan’s Algorithm)

#include <bits/stdc++.h>
using namespace std;

const int MAXN = 1e5 + 5;
vector<int> adj[MAXN];
int tin[MAXN], low[MAXN], timer = 0;
bool on_stack[MAXN];
stack<int> st;
vector<vector<int>> sccs;

void dfs(int u) {
    tin[u] = low[u] = ++timer;
    st.push(u);
    on_stack[u] = true;

    for (int v : adj[u]) {
        if (!tin[v]) {
            dfs(v);
            low[u] = min(low[u], low[v]);
        } else if (on_stack[v]) {
            low[u] = min(low[u], tin[v]);
        }
    }

    if (low[u] == tin[u]) {
        vector<int> scc;
        int v;
        do {
            v = st.top(); st.pop();
            on_stack[v] = false;
            scc.push_back(v);
        } while (v != u);
        sccs.push_back(scc);
    }
}

void tarjan_scc(int n) {
    fill(tin, tin + n + 1, 0);
    fill(low, low + n + 1, 0);
    fill(on_stack, on_stack + n + 1, false);
    timer = 0;
    while (!st.empty()) st.pop();
    sccs.clear();

    for (int i = 1; i <= n; ++i)
        if (!tin[i]) dfs(i);
}

🧭 Khi nào dùng?

Nhu cầu Thuật toán phù hợp
Tìm các SCC trong đồ thị có hướng Tarjan hoặc Kosaraju
Xây DAG từ SCC Tarjan + rút gọn đồ thị
Bài toán 2-SAT Kosaraju
Đếm chu trình hoặc rút gọn phụ thuộc Tarjan/Kosaraju

🔧 Tối ưu khi dùng

  • Với Kosaraju, cần đồ thị đảo chiều (transposed) → chiếm thêm bộ nhớ.
  • Với Tarjan, chỉ cần 1 DFS → hiệu quả cho đồ thị lớn.
  • Khi cần DAG SCC: gán id mỗi SCC và rút gọn đồ thị theo id.
  • Lưu id[u]: id của SCC mà đỉnh u thuộc về.

📚 Tham khảo

  • CP-Algorithms - Strongly Connected Components
  • CSES Problem Set: Flight Routes Check / Giant Pizza
  • Codeforces 371C, 427C, AtCoder ABC231 Ex

Cầu & Khớp

đã đăng vào 1 tháng 4 năm 2025, 12:15 a.m. 0

🕸️ Cheatsheet: Cầu & Khớp (Bridge & Articulation Point)

📘 Giới thiệu

Trong lý thuyết đồ thị, khớp (articulation point) là một đỉnh mà nếu bị loại bỏ sẽ làm tăng số thành phần liên thông của đồ thị.

Cầu (bridge) là một cạnh mà nếu bị loại bỏ sẽ làm đồ thị bị chia tách.


🔧 Ứng dụng

  • Thiết kế mạng không có điểm lỗi đơn (single point of failure).
  • Tìm điểm yếu trong hệ thống liên kết.
  • Phân tích mạng máy tính, mạng xã hội.
  • Chia đồ thị thành biconnected components.
  • Phân tích cấu trúc liên thông mạnh/yếu.

⏱️ Độ phức tạp

  • Thời gian: O(V + E) với DFS.
  • Không gian: O(V + E)

✅ Ưu điểm

  • Cài đặt hiệu quả với DFS.
  • Tìm tất cả cầu & khớp trong một lượt duyệt.
  • Ứng dụng rộng trong thực tiễn & thuật toán chia đồ thị.

❌ Nhược điểm

  • Không áp dụng cho đồ thị có hướng (phải dùng biến thể khác).
  • Cần xử lý kỹ khi có cạnh song song (multiedge) hoặc đồ thị không liên thông.

📌 C++ Code (DFS cho cầu và khớp)

#include <bits/stdc++.h>
using namespace std;

const int MAXN = 1e5 + 5;
vector<int> adj[MAXN];
int tin[MAXN], low[MAXN], timer;
bool is_articulation[MAXN];
vector<pair<int, int>> bridges;

void dfs(int u, int parent) {
    tin[u] = low[u] = ++timer;
    int children = 0;

    for (int v : adj[u]) {
        if (v == parent) continue;

        if (tin[v]) {
            low[u] = min(low[u], tin[v]);
        } else {
            dfs(v, u);
            low[u] = min(low[u], low[v]);
            if (low[v] > tin[u])
                bridges.emplace_back(u, v);
            if (low[v] >= tin[u] && parent != -1)
                is_articulation[u] = true;
            ++children;
        }
    }

    if (parent == -1 && children > 1)
        is_articulation[u] = true;
}

void find_bridges_and_cutpoints(int n) {
    timer = 0;
    fill(tin, tin + n + 1, 0);
    fill(low, low + n + 1, 0);
    fill(is_articulation, is_articulation + n + 1, false);
    bridges.clear();

    for (int i = 1; i <= n; ++i)
        if (!tin[i])
            dfs(i, -1);
}

📌 Tối ưu khi dùng

  • Duyệt đồ thị không liên thông: nhớ chạy DFS cho từng thành phần.
  • Có thể lưu biconnected components trong stack để tái sử dụng.
  • Xử lý trùng cạnh: dùng set<pair<int, int>> nếu cần loại trừ cạnh song song.

🧭 Khi nào dùng?

Nhu cầu Dùng thuật toán nào
Tìm điểm yếu trong đồ thị vô hướng DFS khớp + cầu
Tìm chia đồ thị thành Biconnected Components Biến thể Tarjan
Đồ thị có hướng Phải dùng SCC (Kosaraju/ Tarjan)

📚 Tham khảo

  • CP-Algorithms - Bridges
  • CP-Algorithms - Articulation Points
  • Bài tập: CSES Graph Algorithms / SPOJ SUBMERGE / VNOI QBRECT

Sắp Xếp Tôpô

đã đăng vào 1 tháng 4 năm 2025, 12:11 a.m. 0

🧮 Cheatsheet: Sắp Xếp Tôpô (Topological Sort)

📘 Giới thiệu

Sắp xếp tôpô (topological sort) là một kỹ thuật duyệt đồ thị có hướng (DAG) sao cho nếu tồn tại cạnh từ u → v thì u sẽ xuất hiện trước v trong thứ tự sắp xếp.


🧠 Ứng dụng

  • Sắp xếp thứ tự thực hiện công việc có phụ thuộc.
  • Lập lịch bài học, xây dựng module hệ thống.
  • Phân tích biểu thức, trình biên dịch.
  • Bài toán kiểm tra chu trình trong đồ thị có hướng.

🔁 Code C++ (BFS - Kahn’s Algorithm)

#include <bits/stdc++.h>
using namespace std;

vector<int> topo_sort(int n, vector<vector<int>>& adj) {
    vector<int> in_degree(n + 1, 0);
    for (int u = 1; u <= n; ++u)
        for (int v : adj[u])
            in_degree[v]++;

    queue<int> q;
    for (int i = 1; i <= n; ++i)
        if (in_degree[i] == 0)
            q.push(i);

    vector<int> topo;
    while (!q.empty()) {
        int u = q.front(); q.pop();
        topo.push_back(u);
        for (int v : adj[u]) {
            if (--in_degree[v] == 0)
                q.push(v);
        }
    }

    if (topo.size() < n)
        throw runtime_error("Graph has a cycle");

    return topo;
}

🔁 Code C++ (DFS)

void dfs(int u, vector<vector<int>>& adj, vector<bool>& visited, stack<int>& st) {
    visited[u] = true;
    for (int v : adj[u])
        if (!visited[v])
            dfs(v, adj, visited, st);
    st.push(u);
}

vector<int> topo_sort_dfs(int n, vector<vector<int>>& adj) {
    vector<bool> visited(n + 1, false);
    stack<int> st;
    for (int i = 1; i <= n; ++i)
        if (!visited[i])
            dfs(i, adj, visited, st);

    vector<int> topo;
    while (!st.empty()) {
        topo.push_back(st.top());
        st.pop();
    }
    return topo;
}

📊 So sánh BFS và DFS

Tiêu chí BFS (Kahn's) DFS-Based
Phát hiện chu trình Có (qua số lượng đỉnh) Không trực tiếp
Dễ debug ✅ ❌
Dễ cài đặt ✅ ✅
Hiệu suất Ngang nhau Ngang nhau

⏱️ Độ phức tạp

  • Thời gian: O(V + E)
  • Không gian: O(V + E)

✅ Ưu điểm

  • Hiệu quả với đồ thị DAG.
  • Cài đặt đơn giản.
  • Có thể dùng phát hiện chu trình (Kahn’s Algorithm).

❌ Nhược điểm

  • Chỉ dùng được với đồ thị có hướng không chu trình (DAG).
  • Với đồ thị có chu trình sẽ không tồn tại thứ tự topo.

🔧 Tối ưu

  • Dùng priority_queue thay vì queue để tìm topo theo thứ tự từ điển nhỏ nhất.
  • Gộp visited + in_degree thành 1 mảng nếu tiết kiệm bộ nhớ.

🤔 Khi nào dùng?

Tình huống Có nên dùng?
Đồ thị có hướng, không chu trình (DAG) ✅
Bài toán lập lịch, sắp xếp có phụ thuộc ✅
Đồ thị có chu trình ❌ (không áp dụng)
Tìm chu trình trong đồ thị Dùng biến thể của Kahn ✅

📚 Tham khảo

  • CP-Algorithms - Topological Sort
  • Atcoder, Codeforces, CSES Graph Section

Tìm đường đi ngắn nhất

đã đăng vào 1 tháng 4 năm 2025, 12:07 a.m. 0

🛣️ Cheatsheet Các Thuật Toán Tìm Đường Đi Ngắn Nhất

📘 Giới thiệu

Ba thuật toán tìm đường đi ngắn nhất phổ biến nhất trong đồ thị là:

  • Dijkstra: Tìm đường đi ngắn nhất từ 1 đỉnh đến các đỉnh còn lại, không có cạnh âm.
  • Bellman-Ford: Xử lý được cạnh âm, phát hiện chu trình âm.
  • Floyd-Warshall: Tìm đường đi ngắn nhất giữa mọi cặp đỉnh.

📌 So sánh nhanh

Thuật toán Cạnh âm Chu trình âm Đồ thị dày Đồ thị thưa Độ phức tạp
Dijkstra ❌ ❌ ❌ ✅ O((V + E) log V)
Bellman-Ford ✅ ✅ ✅ ✅ O(V * E)
Floyd-Warshall ✅ ✅ ✅ ❌ O(V³)

1. 🚀 Dijkstra (với hàng đợi ưu tiên)

#include <bits/stdc++.h>
using namespace std;
#define pii pair<int, int>
const int INF = 1e9;

vector<int> dijkstra(int start, int n, vector<vector<pii>>& adj) {
    vector<int> dist(n + 1, INF);
    priority_queue<pii, vector<pii>, greater<>> pq;
    dist[start] = 0;
    pq.push({0, start});

    while (!pq.empty()) {
        auto [du, u] = pq.top(); pq.pop();
        if (du > dist[u]) continue;
        for (auto [v, w] : adj[u]) {
            if (dist[v] > dist[u] + w) {
                dist[v] = dist[u] + w;
                pq.push({dist[v], v});
            }
        }
    }
    return dist;
}
✅ Ưu điểm:
  • Rất nhanh với đồ thị không có cạnh âm.
  • Có thể dùng set, priority_queue hoặc pairing_heap.
❌ Nhược điểm:
  • Không hoạt động đúng với cạnh âm.

2. 🐌 Bellman-Ford

vector<int> bellman_ford(int start, int n, vector<tuple<int, int, int>>& edges) {
    const int INF = 1e9;
    vector<int> dist(n + 1, INF);
    dist[start] = 0;

    for (int i = 1; i < n; ++i) {
        for (auto [u, v, w] : edges) {
            if (dist[u] != INF && dist[v] > dist[u] + w) {
                dist[v] = dist[u] + w;
            }
        }
    }

    // Kiểm tra chu trình âm
    for (auto [u, v, w] : edges) {
        if (dist[u] != INF && dist[v] > dist[u] + w)
            throw runtime_error("Negative cycle detected");
    }

    return dist;
}
✅ Ưu điểm:
  • Hoạt động với cạnh âm.
  • Phát hiện chu trình âm.
❌ Nhược điểm:
  • Chậm với đồ thị lớn.

3. 🧠 Floyd-Warshall

void floyd_warshall(int n, vector<vector<int>>& dist) {
    for (int k = 1; k <= n; ++k)
        for (int i = 1; i <= n; ++i)
            for (int j = 1; j <= n; ++j)
                if (dist[i][k] < 1e9 && dist[k][j] < 1e9)
                    dist[i][j] = min(dist[i][j], dist[i][k] + dist[k][j]);
}
✅ Ưu điểm:
  • Tìm đường đi ngắn nhất giữa mọi cặp đỉnh.
  • Dễ cài đặt.
❌ Nhược điểm:
  • Độ phức tạp O(V³) → chậm với đồ thị lớn.

🧪 Khi nào dùng thuật toán nào?

Tình huống Thuật toán gợi ý
Đường đi ngắn nhất từ 1 đỉnh, không cạnh âm Dijkstra
Đường đi ngắn nhất từ 1 đỉnh, có cạnh âm Bellman-Ford
Tìm đường ngắn nhất giữa mọi cặp đỉnh Floyd-Warshall
Cần phát hiện chu trình âm Bellman-Ford / FW

🔧 Tối ưu khi dùng

  • Với Dijkstra:

    • Sử dụng priority_queue hoặc set để tăng tốc độ.
    • Khi dùng cho lưới (grid), có thể thay bằng BFS nếu trọng số = 1.
  • Với Bellman-Ford:

    • Dừng sớm nếu không có cập nhật sau 1 lượt lặp.
    • Dùng để detect chu trình âm trong Competitive Programming.
  • Với Floyd-Warshall:

    • Dùng ma trận dist[i][j] và next[i][j] để reconstruct đường đi.
    • Có thể dùng với đồ thị nhỏ (V ≤ 500).

📚 Tài liệu tham khảo

  • CP-Algorithms - Dijkstra
  • CP-Algorithms - Bellman-Ford
  • CP-Algorithms - Floyd-Warshall

Cheatsheet C++

đã đăng vào 31 tháng 3 năm 2025, 11:38 p.m. 0

Hướng dẫn sử dụng mt19937 và mt19937_64 trong C++

📌 Mục tiêu

  • Giải thích cách sử dụng bộ sinh số ngẫu nhiên chất lượng cao trong C++
  • Phân biệt mt19937 (32-bit) và mt19937_64 (64-bit)
  • Hướng dẫn sinh số ngẫu nhiên, số trong khoảng, số thực, mảng ngẫu nhiên

🧠 Tổng quan

mt19937 và mt19937_64 là gì?
  • Là bộ sinh số ngẫu nhiên dựa trên thuật toán Mersenne Twister.
  • mt19937: sinh số nguyên 32-bit (unsigned int).
  • mt19937_64: sinh số nguyên 64-bit (unsigned long long).
  • Chu kỳ rất dài, phân phối đồng đều, dùng nhiều trong lập trình thi đấu và mô phỏng.

🔧 Code mẫu cơ bản

#include <bits/stdc++.h>
using namespace std;

mt19937 rng32(time(0));         // Sinh số ngẫu nhiên 32-bit
mt19937_64 rng64(time(0));      // Sinh số ngẫu nhiên 64-bit

int main() {
    cout << "random 32 (bit): " << rng32() << endl;
    cout << "random 64 (bit): " << rng64() << endl;
}

📘 C++ Hash Map Cheatsheet: map, unordered_map, gp_hash_table

1. std::map

  • Dạng: Cây đỏ-đen (Red-Black Tree)
  • Tự động sắp xếp theo key
  • Độ phức tạp: O(log n) cho các thao tác chèn, tìm kiếm, xóa
#include <bits/stdc++.h>
using namespace std;

unordered_map<int, int> um;

int main() {
    map<int, int> mp;
    mp[1] = 100;
    if (mp.count(1)) {
        cout << mp[1];  // Output: 100
    }
}

✅ Ưu điểm:

  • Sắp xếp theo key
  • Ổn định

❌ Nhược điểm:

  • Chậm hơn unordered_map hoặc gp_hash_table với dữ liệu ngẫu nhiên

2. std::unordered_map

  • Dạng: Hash Table chuẩn
  • Không sắp xếp theo key
  • Độ phức tạp trung bình: O(1) (xấu nhất O(n))

🛠 Tip: Tăng hiệu năng bằng reserve() khi biết trước số phần tử:

#include <bits/stdc++.h>
using namespace std;

unordered_map<int, int> um;

int main() {
    um.reserve(1000000);
    um[3] = 3;
    cout << um[3];
}

3. __gnu_pbds::gp_hash_table

  • Cấu trúc dữ liệu thay thế unordered_map với hiệu suất cao hơn
  • Cần include đặc biệt:
#include <bits/stdc++.h>
#include <ext/pb_ds/assoc_container.hpp>
using namespace __gnu_pbds;
using namespace std;

int main() {
    gp_hash_table<int, int> m;
    m[3] = 1;
    cout << m[3];
}

✅ Ưu điểm:

  • Rất nhanh cho dữ liệu ngẫu nhiên
  • Ít bị đụng độ hash

❌ Nhược điểm:

  • Không tương thích với Clang
  • Không có trong chuẩn STL, chỉ dùng trên GCC (như Codeforces)

4. So sánh hiệu suất

Dữ liệu std::map unordered_map gp_hash_table
Tìm kiếm log(n) Trung bình O(1) Nhanh hơn O(1)
Bộ nhớ Cao Thấp - TB Tối ưu tốt
Dữ liệu ngẫu nhiên Chậm TB - có thể bị tệ Rất tốt
Hỗ trợ Clang ✔️ ✔️ ❌

5. Một số tối ưu khi dùng unordered_map

unordered_map<int, int> um;
um.reserve(1 << 20);              // Dự phòng bộ nhớ
um.max_load_factor(0.25);        // Giảm load factor để tránh đụng độ hash

6. Khi nào dùng cái nào?

Trường hợp Cấu trúc phù hợp
Cần sắp xếp key std::map
Tìm kiếm nhanh, không cần sắp xếp unordered_map
Dữ liệu ngẫu nhiên lớn, cần tốc độ gp_hash_table (nếu dùng GCC)

7. Bài tập áp dụng

  • Bedao Grand Contest 10 - PERFECT
  • Atcoder ABC250E - Prefix Equality
  • Hackerrank - Number Game on a Tree
  • Codeforces 1175F - The Number of Subpermutations
  • Codeforces 1418G - Three Occurrences

📌 Ghi chú

  • Với test ngẫu nhiên lớn, gp_hash_table thường vượt trội về thời gian và bộ nhớ.
  • Nếu không dùng được gp_hash_table, bạn có thể thử tùy biến unordered_map bằng cách tự viết hàm hash.
struct custom_hash {
    size_t operator()(uint64_t x) const {
        return x ^ (x >> 16);
    }
};
unordered_map<uint64_t, int, custom_hash> safe_map;

📘 C++ Cheatsheet: vector 1 chiều & 2 chiều

1. vector 1 chiều

🔹 Khai báo
vector<int> a;                // Vector rỗng
vector<int> b(5);             // Vector 5 phần tử, giá trị mặc định = 0
vector<int> c(5, -1);         // Vector 5 phần tử, mỗi phần tử = -1
🔹 Truy cập phần tử
a[0], a.at(0);               // Truy cập phần tử đầu tiên
a.front();                   // Phần tử đầu
a.back();                    // Phần tử cuối
🔹 Thêm / Xóa phần tử
a.push_back(10);             // Thêm vào cuối
a.pop_back();                // Xóa phần tử cuối
a.insert(a.begin() + 2, 100);// Chèn 100 tại vị trí thứ 2
a.erase(a.begin() + 1);      // Xóa phần tử tại vị trí thứ 1
🔹 Duyệt
for (int x : a) cout << x;
for (int i = 0; i < a.size(); ++i) cout << a[i];
🔹 Resize & Clear
a.resize(10);                // Thay đổi kích thước thành 10 phần tử
a.clear();                   // Xóa toàn bộ phần tử
🔹 Gán
a = vector<int>(5, 7);       // Gán lại vector a thành 5 phần tử = 7
a.assign(3, 100);            // Gán 3 phần tử bằng 100

2. vector 2 chiều

🔹 Khai báo
int n = 3, m = 4;
vector<vector<int>> mat(n, vector<int>(m));          // ma trận n x m, mặc định 0
vector<vector<int>> mat2(n, vector<int>(m, -1));     // ma trận n x m, mỗi phần tử = -1
🔹 Truy cập & Duyệt
mat[i][j];                    // Truy cập phần tử hàng i, cột j

for (int i = 0; i < n; ++i)
    for (int j = 0; j < m; ++j)
        cout << mat[i][j];
🔹 Resize sau khai báo
vector<vector<int>> mat;
mat.resize(n, vector<int>(m, 0));   // Resize thành n x m, tất cả = 0
🔹 Gán lại toàn bộ
mat.assign(n, vector<int>(m, 1));  // Gán lại ma trận n x m toàn bộ = 1

3. Một số mẹo

  • Duyệt nhanh:

    for (auto &row : mat)
      for (auto x : row)
          cout << x << " ";
    
  • Xoay chiều ma trận:

    vector<vector<int>> rotate90(const vector<vector<int>>& mat) {
      int n = mat.size(), m = mat[0].size();
      vector<vector<int>> res(m, vector<int>(n));
      for (int i = 0; i < n; ++i)
          for (int j = 0; j < m; ++j)
              res[j][n - 1 - i] = mat[i][j];
      return res;
    }
    

🔚 Tổng kết

Tác vụ Cú pháp
Khởi tạo vector<int> v(n, val);
Resize v.resize(new_size);
Gán toàn bộ v.assign(n, val);
Vector 2D vector<vector<int>> mat(n, vector<int>(m));
Xóa v.clear();
Truy cập v[i], mat[i][j]
Duyệt for (int x : v) hoặc for (auto x : row)

Cuộc thi đang diễn ra

BCC/SCC, DAG
Kết thúc trong 20 ngày 04:53:49.
BCC/SCC, DAG (Nâng cao)
Kết thúc trong 20 ngày 04:53:49.
Toán tổ hợp/dp
Kết thúc trong 20 ngày 04:53:49.
Tuyển sinh 10 - THPT Chuyên Lý Tự Trọng - TP.Cần Thơ 2025
Kết thúc trong 7 ngày 04:53:49.

Dòng bình luận

  • nhan0123456 → Hội thao học sinh (Olympic 30/4 K10- 2025)
  • Kngan → HSG THCS - Cần Thơ 2025
  • triile → HSG9 - Ninh Bình (2023)
  • quachlynhanai → HSG9 - Ninh Bình (2023)
  • triile → HSG9 - Ninh Bình (2023) - Dãy con
  • triile → HSG9 - Đăk Lắk (2023) - Bài 1
  • quachlynhanai → HSG9 - Đăk Lắk (2023) - Bài 1
  • quachlynhanai → HSG9 - Đồng Tháp (2023) - Bài 1
  • htl → HSG9 - An Giang (2023) - Bài 1
  • quachlynhanai → HSG9 - An Giang (2023) - Bài 1
RSS / Atom

Đề bài mới

  • Bài 2. Dãy số (THT Hà Nội 2025 - Sơ khảo)
  • Problem A. MEXGRAPH (Chuyên KHTN 2025)
  • Problem B. TREE (Chuyên KHTN 2025)
  • Problem C. AMAZINGMIGHTYYYYYYYYYYYYYYYYY!!!!!!! (Chuyên KHTN 2025)
  • Problem E. COPRIME (Chuyên KHTN 2025)
  • THT B - Đoạn con tăng nghiêm ngặt (Ninh Kiều - Cần Thơ)
  • Bài 5. Trồng cây (Chọn ĐTQG - Nam Định 2022)
RSS / Atom

dựa trên nền tảng DMOJ |