Bài 19.3. Đồ thị hai phía và kiểm tra

Trong thế giới rộng lớn và đầy màu sắc của Lý thuyết đồ thị, chúng ta gặp gỡ vô vàn cấu trúc khác nhau, mỗi loại lại mang trong mình những đặc tính riêng biệt và ứng dụng độc đáo. Một trong những cấu trúc đồ thị đặc biệt và quan trọng bậc nhất chính là đồ thị hai phía, hay còn được biết đến với cái tên đồ thị lưỡng phân (bipartite graph).

Đồ thị Hai Phía là Gì?

Hãy tưởng tượng bạn có một nhóm người và một nhóm công việc, và bạn muốn biểu diễn mối quan hệ "có thể làm" giữa người và công việc đó. Mọi mối quan hệ (cạnh) đều nối một người với một công việc, không có cạnh nào nối hai người với nhau, cũng không có cạnh nào nối hai công việc với nhau. Đây chính là một ví dụ trực quan về đồ thị hai phía.

Một cách hình thức hơn:

Một đồ thị $G = (V, E)$ được gọi là đồ thị hai phía nếu tập hợp đỉnh $V$ của nó có thể được phân hoạch (chia thành các tập con không giao nhau và gộp lại được toàn bộ tập gốc) thành hai tập hợp con không rỗngđộc lập $U$ và $W$ (tức là $U \cup W = V$ và $U \cap W = \emptyset$), sao cho mọi cạnh trong tập hợp cạnh $E$ đều nối một đỉnh từ tập $U$ với một đỉnh từ tập $W$.

Điều quan trọng cần nhấn mạnh ở đây là: không có cạnh nào nối hai đỉnh cùng thuộc tập $U$, và tương tự, không có cạnh nào nối hai đỉnh cùng thuộc tập $W$.

Các tập hợp $U$ và $W$ này thường được gọi là tập hai phía (bipartition).

Ví dụ đơn giản:

  • Một đường đi (path) bất kỳ là đồ thị hai phía.
  • Một cây (tree) bất kỳ là đồ thị hai phía.
  • Một chu trình (cycle) có độ dài chẵn là đồ thị hai phía.
  • Một chu trình có độ dài lẻ không phải là đồ thị hai phía.
  • Một đồ thị đầy đủ hai phía $K_{m,n}$ là đồ thị gồm $m$ đỉnh trong tập $U$ và $n$ đỉnh trong tập $W$, với mọi đỉnh trong $U$ đều nối với mọi đỉnh trong $W$. Đây hiển nhiên là đồ thị hai phía.

Tại Sao Đồ thị Hai Phía Quan Trọng?

Đồ thị hai phía xuất hiện tự nhiên trong nhiều bài toán thực tế và lý thuyết, ví dụ như:

  • Bài toán ghép cặp (Matching): Tìm cách ghép cặp các phần tử từ hai tập khác nhau (ví dụ: ghép sinh viên với đề tài, công nhân với máy móc) sao cho tối ưu một tiêu chí nào đó.
  • Bài toán tô màu đồ thị (Graph Coloring): Một đồ thị hai phía luôn có thể tô màu bằng 2 màu. Ngược lại, nếu một đồ thị có thể tô màu bằng 2 màu, nó là đồ thị hai phía. Điều này liên quan đến sắc số của đồ thị ($\chi(G) \leq 2$).
  • Lập kế hoạch/Lịch trình: Nhiều bài toán phân công nhiệm vụ có thể mô hình hóa bằng đồ thị hai phía.

Nhận biết một đồ thị có phải là đồ thị hai phía hay không là bước đầu tiên và quan trọng để áp dụng các giải thuật đặc thù cho loại đồ thị này, thường hiệu quả hơn so với giải thuật tổng quát trên đồ thị bất kỳ.

Làm thế nào để Kiểm tra Đồ thị Hai Phía?

Câu hỏi đặt ra là: cho một đồ thị bất kỳ, làm thế nào để biết nó có phải là đồ thị hai phía hay không?

Ý tưởng cốt lõi là chúng ta sẽ cố gắng tô màu các đỉnh của đồ thị bằng hai màu (ví dụ: màu 0 và màu 1) sao cho hai đỉnh bất kỳ được nối bởi một cạnh luôn có màu khác nhau. Nếu chúng ta có thể thực hiện được việc tô màu này cho toàn bộ đồ thị (hoặc tất cả các thành phần liên thông của nó) mà không gặp mâu thuẫn, thì đồ thị đó là đồ thị hai phía. Ngược lại, nếu trong quá trình tô màu, chúng ta gặp một cạnh nối hai đỉnh đã được tô màu mà lại có cùng màu, thì đồ thị đó không phải là đồ thị hai phía.

Quá trình tô màu này có thể được thực hiện hiệu quả bằng cách sử dụng các giải thuật duyệt đồ thị quen thuộc: Duyệt theo chiều rộng (BFS) hoặc Duyệt theo chiều sâu (DFS).

Chúng ta sẽ sử dụng một mảng/vector để lưu màu của từng đỉnh. Khởi tạo tất cả các đỉnh là "chưa tô màu" (ví dụ, gán giá trị -1).

Phương pháp 1: Sử dụng BFS

Giải thuật BFS để kiểm tra tính hai phía hoạt động như sau:

  1. Duyệt qua tất cả các đỉnh của đồ thị. Nếu gặp một đỉnh chưa được tô màu:
  2. Bắt đầu một quá trình BFS từ đỉnh đó. Tô màu đỉnh bắt đầu bằng màu 0.
  3. Thêm đỉnh này vào hàng đợi (queue).
  4. Trong khi hàng đợi chưa rỗng:
    • Lấy một đỉnh u từ đầu hàng đợi.
    • Với mỗi đỉnh v là hàng xóm của u:
      • Nếu v chưa được tô màu: Tô màu v bằng màu đối diện với màu của u (nếu u màu 0 thì v màu 1, nếu u màu 1 thì v màu 0). Sau đó, thêm v vào hàng đợi.
      • Nếu v đã được tô màu màu của v lại giống màu của u: Đồ thị không phải là đồ thị hai phía. Dừng lại và trả về false.
  5. Nếu quá trình BFS kết thúc mà không gặp mâu thuẫn nào về màu, nghĩa là thành phần liên thông hiện tại là hai phía. Tiếp tục với các đỉnh chưa tô màu khác (nếu có) để xử lý các thành phần liên thông khác.
  6. Nếu tất cả các thành phần liên thông đều được xử lý mà không gặp mâu thuẫn, đồ thị là đồ thị hai phía. Trả về true.

Đây là mã nguồn C++ minh họa giải thuật kiểm tra đồ thị hai phía bằng BFS:

#include <iostream>
#include <vector>
#include <queue>
#include <numeric> // For std::fill

// Hàm kiểm tra tính hai phía sử dụng BFS cho một thành phần liên thông
bool isBipartiteBFS(int start_node, int V, const std::vector<std::vector<int>>& adj, std::vector<int>& color) {
    std::queue<int> q;
    q.push(start_node);
    color[start_node] = 0; // Bắt đầu tô màu đỉnh xuất phát là màu 0

    while (!q.empty()) {
        int u = q.front();
        q.pop();

        // Duyệt qua tất cả hàng xóm của đỉnh u
        for (int v : adj[u]) {
            // Nếu đỉnh v chưa được tô màu (-1)
            if (color[v] == -1) {
                // Tô màu v khác màu với u
                color[v] = 1 - color[u];
                q.push(v); // Thêm v vào hàng đợi để duyệt tiếp
            }
            // Nếu đỉnh v đã được tô màu và có CÙNG màu với u
            else if (color[v] == color[u]) {
                // Phát hiện mâu thuẫn, đồ thị không phải hai phía
                return false;
            }
        }
    }
    // Nếu duyệt xong thành phần liên thông mà không có mâu thuẫn
    return true;
}

// Hàm chính kiểm tra đồ thị hai phía (xử lý cả đồ thị không liên thông)
bool checkBipartite(int V, const std::vector<std::vector<int>>& adj) {
    // Mảng color để lưu màu của các đỉnh. -1: chưa tô màu, 0: màu 0, 1: màu 1
    std::vector<int> color(V, -1);

    // Duyệt qua tất cả các đỉnh để xử lý các thành phần liên thông
    for (int i = 0; i < V; ++i) {
        // Nếu đỉnh i chưa được tô màu, bắt đầu BFS từ đỉnh này
        if (color[i] == -1) {
            if (!isBipartiteBFS(i, V, adj, color)) {
                // Nếu một thành phần liên thông bất kỳ không phải hai phía
                return false;
            }
        }
    }

    // Nếu tất cả các thành phần liên thông đều là hai phía
    return true;
}

int main() {
    // Ví dụ 1: Đồ thị hai phía (chu trình C4)
    // 0 -- 1
    // |    |
    // 3 -- 2
    int V1 = 4;
    std::vector<std::vector<int>> adj1(V1);
    adj1[0].push_back(1); adj1[1].push_back(0);
    adj1[1].push_back(2); adj1[2].push_back(1);
    adj1[2].push_back(3); adj1[3].push_back(2);
    adj1[3].push_back(0); adj1[0].push_back(3);

    std::cout << "Example 1 (C4): " << (checkBipartite(V1, adj1) ? "Is Bipartite" : "Not Bipartite") << std::endl; // Expected: Is Bipartite

    // Ví dụ 2: Đồ thị không hai phía (chu trình C3)
    // 0 -- 1
    // |  /
    // | /
    // 2
    int V2 = 3;
    std::vector<std::vector<int>> adj2(V2);
    adj2[0].push_back(1); adj2[1].push_back(0);
    adj2[1].push_back(2); adj2[2].push_back(1);
    adj2[2].push_back(0); adj2[0].push_back(2);

    std::cout << "Example 2 (C3): " << (checkBipartite(V2, adj2) ? "Is Bipartite" : "Not Bipartite") << std::endl; // Expected: Not Bipartite

    // Ví dụ 3: Đồ thị không liên thông, cả hai thành phần đều là hai phía
    // 0 -- 1      2 -- 3
    // |    |      |    |
    // 4 -- 5      6 -- 7
    int V3 = 8;
    std::vector<std::vector<int>> adj3(V3);
    // Component 1 (C4)
    adj3[0].push_back(1); adj3[1].push_back(0);
    adj3[1].push_back(5); adj3[5].push_back(1);
    adj3[5].push_back(4); adj3[4].push_back(5);
    adj3[4].push_back(0); adj3[0].push_back(4);
    // Component 2 (C4)
    adj3[2].push_back(3); adj3[3].push_back(2);
    adj3[3].push_back(7); adj3[7].push_back(3);
    adj3[7].push_back(6); adj3[6].push_back(7);
    adj3[6].push_back(2); adj3[2].push_back(6);

    std::cout << "Example 3 (Disconnected): " << (checkBipartite(V3, adj3) ? "Is Bipartite" : "Not Bipartite") << std::endl; // Expected: Is Bipartite

    // Ví dụ 4: Đồ thị không liên thông, một thành phần là hai phía, một không
    // 0 -- 1      2 -- 3
    // |    |      |  /
    // 4 -- 5      4 -- (Note: vertex 4 is shared, this is invalid graph construction for typical adjacency list)
    // Let's create a proper example: C4 and C3 separated
    // 0 -- 1     5 -- 6
    // |    |     |  /
    // 3 -- 2     7
    int V4 = 8;
    std::vector<std::vector<int>> adj4(V4);
    // Component 1 (C4)
    adj4[0].push_back(1); adj4[1].push_back(0);
    adj4[1].push_back(2); adj4[2].push_back(1);
    adj4[2].push_back(3); adj4[3].push_back(2);
    adj4[3].push_back(0); adj4[0].push_back(3);
    // Component 2 (C3) starting from vertex 5
    adj4[5].push_back(6); adj4[6].push_back(5);
    adj4[6].push_back(7); adj4[7].push_back(6);
    adj4[7].push_back(5); adj4[5].push_back(7);


    std::cout << "Example 4 (Disconnected, C4 + C3): " << (checkBipartite(V4, adj4) ? "Is Bipartite" : "Not Bipartite") << std::endl; // Expected: Not Bipartite

    return 0;
}

Giải thích mã nguồn BFS:

  • Chúng ta sử dụng std::vector<std::vector<int>> adj để biểu diễn đồ thị bằng danh sách kề. adj[i] chứa danh sách các đỉnh kề với đỉnh i.
  • std::vector<int> color được dùng để lưu màu của các đỉnh. Ban đầu, tất cả các đỉnh được gán giá trị -1 để biểu thị "chưa tô màu".
  • Hàm isBipartiteBFS(start_node, V, adj, color) thực hiện BFS trên một thành phần liên thông bắt đầu từ start_node.
    • Một hàng đợi std::queue<int> q được sử dụng cho BFS.
    • Đỉnh xuất phát start_node được tô màu 0 và đưa vào hàng đợi.
    • Vòng lặp while (!q.empty()) xử lý các đỉnh trong hàng đợi.
    • Đối với mỗi hàng xóm v của đỉnh hiện tại u, chúng ta kiểm tra màu của v:
      • Nếu v chưa tô màu (color[v] == -1), chúng ta gán màu cho nó khác với màu của u (1 - color[u]) và thêm nó vào hàng đợi.
      • Nếu v đã tô màu và color[v] == color[u], chúng ta tìm thấy một cạnh nối hai đỉnh cùng màu, nên đồ thị không phải hai phía và hàm trả về false.
    • Nếu vòng lặp kết thúc mà không trả về false, thành phần liên thông đó là hai phía.
  • Hàm checkBipartite(V, adj) xử lý toàn bộ đồ thị, bao gồm cả trường hợp không liên thông. Nó lặp qua tất cả các đỉnh. Nếu một đỉnh chưa được tô màu, nó gọi isBipartiteBFS từ đỉnh đó. Nếu bất kỳ thành phần liên thông nào không phải hai phía, hàm checkBipartite trả về false ngay lập tức. Nếu tất cả các thành phần đều là hai phía, hàm trả về true.

Độ phức tạp thời gian của giải thuật BFS này là $O(V+E)$, vì nó thăm mỗi đỉnh và mỗi cạnh đúng một lần trong quá trình duyệt. Độ phức tạp không gian là $O(V)$ để lưu trữ màu và hàng đợi.

Phương pháp 2: Sử dụng DFS

Giải thuật DFS để kiểm tra tính hai phía cũng dựa trên ý tưởng tô màu tương tự BFS, nhưng sử dụng đệ quy thay vì hàng đợi:

  1. Sử dụng một mảng/vector color như trong phương pháp BFS, khởi tạo tất cả là -1.
  2. Duyệt qua tất cả các đỉnh của đồ thị. Nếu gặp một đỉnh chưa được tô màu:
  3. Bắt đầu một quá trình DFS từ đỉnh đó. Tô màu đỉnh bắt đầu bằng màu 0.
  4. Hàm DFS đệ quy DFS(u, current_color):
    • Tô màu đỉnh u bằng current_color.
    • Với mỗi đỉnh v là hàng xóm của u:
      • Nếu v chưa được tô màu: Gọi đệ quy DFS(v, 1 - current_color). Nếu lời gọi đệ quy này trả về false (phát hiện mâu thuẫn ở sâu hơn), hàm hiện tại cũng trả về false.
      • Nếu v đã được tô màu màu của v lại giống màu của u: Phát hiện mâu thuẫn. Trả về false.
    • Nếu duyệt qua tất cả hàng xóm mà không gặp mâu thuẫn và các lời gọi đệ quy đều trả về true, hàm trả về true.
  5. Nếu quá trình DFS từ một đỉnh kết thúc mà không trả về false, thành phần liên thông đó là hai phía. Tiếp tục với các đỉnh chưa tô màu khác (nếu có).
  6. Nếu tất cả các thành phần liên thông đều được xử lý mà không gặp mâu thuẫn, đồ thị là đồ thị hai phía. Trả về true.

Đây là mã nguồn C++ minh họa giải thuật kiểm tra đồ thị hai phía bằng DFS:

#include <iostream>
#include <vector>
#include <numeric> // For std::fill

// Hàm kiểm tra tính hai phía sử dụng DFS cho một thành phần liên thông
// u: đỉnh hiện tại
// c: màu cần tô cho đỉnh u (0 hoặc 1)
bool isBipartiteDFS(int u, int c, const std::vector<std::vector<int>>& adj, std::vector<int>& color) {
    color[u] = c; // Tô màu đỉnh u

    // Duyệt qua tất cả hàng xóm của đỉnh u
    for (int v : adj[u]) {
        // Nếu đỉnh v chưa được tô màu (-1)
        if (color[v] == -1) {
            // Gọi đệ quy DFS cho v với màu đối diện
            if (!isBipartiteDFS(v, 1 - c, adj, color)) {
                // Nếu lời gọi đệ quy phát hiện mâu thuẫn
                return false;
            }
        }
        // Nếu đỉnh v đã được tô màu và có CÙNG màu với u
        else if (color[v] == color[u]) {
            // Phát hiện mâu thuẫn, đồ thị không phải hai phía
            return false;
        }
    }

    // Nếu duyệt xong các hàng xóm mà không có mâu thuẫn
    return true;
}

// Hàm chính kiểm tra đồ thị hai phía (xử lý cả đồ thị không liên thông)
bool checkBipartiteDFS(int V, const std::vector<std::vector<int>>& adj) {
    // Mảng color để lưu màu của các đỉnh. -1: chưa tô màu, 0: màu 0, 1: màu 1
    std::vector<int> color(V, -1);

    // Duyệt qua tất cả các đỉnh để xử lý các thành phần liên thông
    for (int i = 0; i < V; ++i) {
        // Nếu đỉnh i chưa được tô màu, bắt đầu DFS từ đỉnh này
        if (color[i] == -1) {
            // Bắt đầu DFS với màu 0 cho đỉnh i
            if (!isBipartiteDFS(i, 0, adj, color)) {
                // Nếu một thành phần liên thông bất kỳ không phải hai phía
                return false;
            }
        }
    }

    // Nếu tất cả các thành phần liên thông đều là hai phía
    return true;
}

int main() {
    // Ví dụ 1: Đồ thị hai phía (chu trình C4)
    // 0 -- 1
    // |    |
    // 3 -- 2
    int V1 = 4;
    std::vector<std::vector<int>> adj1(V1);
    adj1[0].push_back(1); adj1[1].push_back(0);
    adj1[1].push_back(2); adj1[2].push_back(1);
    adj1[2].push_back(3); adj1[3].push_back(2);
    adj1[3].push_back(0); adj1[0].push_back(3);

    std::cout << "Example 1 (C4) using DFS: " << (checkBipartiteDFS(V1, adj1) ? "Is Bipartite" : "Not Bipartite") << std::endl; // Expected: Is Bipartite

    // Ví dụ 2: Đồ thị không hai phía (chu trình C3)
    // 0 -- 1
    // |  /
    // | /
    // 2
    int V2 = 3;
    std::vector<std::vector<int>> adj2(V2);
    adj2[0].push_back(1); adj2[1].push_back(0);
    adj2[1].push_back(2); adj2[2].push_back(1);
    adj2[2].push_back(0); adj2[0].push_back(2);

    std::cout << "Example 2 (C3) using DFS: " << (checkBipartiteDFS(V2, adj2) ? "Is Bipartite" : "Not Bipartite") << std::endl; // Expected: Not Bipartite

    // Ví dụ 3: Đồ thị không liên thông, cả hai thành phần đều là hai phía
    // 0 -- 1      2 -- 3
    // |    |      |    |
    // 4 -- 5      6 -- 7
    int V3 = 8;
    std::vector<std::vector<int>> adj3(V3);
    // Component 1 (C4)
    adj3[0].push_back(1); adj3[1].push_back(0);
    adj3[1].push_back(5); adj3[5].push_back(1);
    adj3[5].push_back(4); adj3[4].push_back(5);
    adj3[4].push_back(0); adj3[0].push_back(4);
    // Component 2 (C4)
    adj3[2].push_back(3); adj3[3].push_back(2);
    adj3[3].push_back(7); adj3[7].push_back(3);
    adj3[7].push_back(6); adj3[6].push_back(7);
    adj3[6].push_back(2); adj3[2].push_back(6);

    std::cout << "Example 3 (Disconnected) using DFS: " << (checkBipartiteDFS(V3, adj3) ? "Is Bipartite" : "Not Bipartite") << std::endl; // Expected: Is Bipartite

    // Ví dụ 4: Đồ thị không liên thông, một thành phần là hai phía, một không
    // 0 -- 1     5 -- 6
    // |    |     |  /
    // 3 -- 2     7
    int V4 = 8;
    std::vector<std::vector<int>> adj4(V4);
    // Component 1 (C4)
    adj4[0].push_back(1); adj4[1].push_back(0);
    adj4[1].push_back(2); adj4[2].push_back(1);
    adj4[2].push_back(3); adj4[3].push_back(2);
    adj4[3].push_back(0); adj4[0].push_back(3);
    // Component 2 (C3) starting from vertex 5
    adj4[5].push_back(6); adj4[6].push_back(5);
    adj4[6].push_back(7); adj4[7].push_back(6);
    adj4[7].push_back(5); adj4[5].push_back(7);

    std::cout << "Example 4 (Disconnected, C4 + C3) using DFS: " << (checkBipartiteDFS(V4, adj4) ? "Is Bipartite" : "Not Bipartite") << std::endl; // Expected: Not Bipartite


    return 0;
}

Giải thích mã nguồn DFS:

  • Cấu trúc dữ liệu adjcolor tương tự như trong ví dụ BFS.
  • Hàm isBipartiteDFS(u, c, adj, color) thực hiện đệ quy DFS từ đỉnh u, cố gắng tô màu nó bằng màu c.
    • Đỉnh u được tô màu c.
    • Vòng lặp for (int v : adj[u]) duyệt qua các hàng xóm của u.
    • Nếu v chưa được tô màu (color[v] == -1), hàm gọi đệ quy isBipartiteDFS cho v với màu đối diện (1 - c). Nếu lời gọi đệ quy này trả về false, nghĩa là có mâu thuẫn ở nhánh đó, nên hàm hiện tại cũng trả về false.
    • Nếu v đã được tô màu và color[v] == color[u], phát hiện mâu thuẫn, trả về false.
    • Nếu vòng lặp kết thúc mà không trả về false (tất cả các hàng xóm đã được xử lý mà không có mâu thuẫn), hàm trả về true.
  • Hàm checkBipartiteDFS(V, adj) cũng xử lý đồ thị không liên thông bằng cách lặp qua tất cả các đỉnh và gọi isBipartiteDFS nếu đỉnh đó chưa được tô màu.

Bài tập ví dụ:

Hòn đảo cô đơn

Trong một chuyến du lịch khám phá các hòn đảo, FullHouse Dev đã tình cờ gặp một bài toán thú vị về các cây cầu một chiều. Với tinh thần khám phá, nhóm quyết định giải quyết bài toán này để tìm ra hòn đảo cuối cùng mà họ sẽ dừng chân.

Bài toán

Có nhiều hòn đảo được kết nối bởi các cây cầu một chiều. Nếu một cây cầu nối từ đảo \(i\) đến đảo \(j\), bạn chỉ có thể di chuyển từ đảo \(i\) sang đảo \(j\) mà không thể quay lại. Khi đứng ở đảo \(i\), bạn sẽ ngẫu nhiên chọn một trong các đảo có thể đi đến trực tiếp từ đảo \(i\) thông qua cầu một chiều. Bạn sẽ bị kẹt trên một đảo nếu không thể di chuyển tiếp. Đảm bảo rằng sau khi rời khỏi một đảo, bạn không thể quay lại đảo đó.

Hãy tìm hòn đảo mà bạn có khả năng cao nhất sẽ bị kẹt lại. Hai đảo được coi là có xác suất bằng nhau nếu chênh lệch tuyệt đối của xác suất kẹt lại trên chúng là \(10^{-6}\).

INPUT FORMAT:
  • Dòng đầu tiên chứa ba số nguyên \(n\) (số lượng đảo), \(m\) (số lượng cầu một chiều), và \(s\) (chỉ số của đảo xuất phát)
  • \(m\) dòng tiếp theo: Mỗi dòng chứa hai số nguyên \(u\) và \(v\) biểu thị một cây cầu một chiều từ đảo \(u\) đến đảo \(v\)
OUTPUT FORMAT:
  • In ra chỉ số của đảo mà bạn có khả năng cao nhất sẽ bị kẹt lại. Nếu có nhiều đảo thỏa mãn, in ra các chỉ số theo thứ tự tăng dần (các số cách nhau bởi dấu cách trên một dòng).
Ràng buộc:
  • \(1 \leq n \leq 100\)
  • \(1 \leq m \leq n(n-1)\)
  • \(1 \leq s \leq n\)
Ví dụ
INPUT
5 7 1
1 2
1 3
1 4
1 5
2 4
2 5
3 4
OUTPUT
4
Giải thích

Có hai đảo mà bạn có thể bị kẹt lại - đảo 4 và đảo 5, trong đó đảo 4 có xác suất cao hơn.

// Hướng dẫn giải bài "Hòn đảo cô đơn"

// 1. Biểu diễn đồ thị:
//    - Sử dụng danh sách kề (adjacency list) để lưu trữ các cây cầu một chiều.
//      Ví dụ: vector<vector<int>> adj; adj[u] chứa danh sách các đảo v có cầu u -> v.
//    - Cần lưu trữ bậc ra (out-degree) của mỗi đảo, vì xác suất di chuyển từ đảo u đến mỗi đảo lân cận v là 1 / out_degree[u].
//      Ví dụ: vector<int> out_degree; out_degree[u] là số lượng đảo có thể đi trực tiếp từ u.

// 2. Tính toán xác suất:
//    - Bài toán yêu cầu tìm xác suất bị kẹt lại tại mỗi đảo (có bậc ra bằng 0).
//    - Xác suất này là tổng xác suất của tất cả các con đường từ đảo xuất phát S đến đảo đó.
//    - Vì có thể có chu trình trong đồ thị (mặc dù một lần đi không quay lại đảo đã thăm), xác suất dòng chảy có thể phức tạp. Tuy nhiên, quy tắc "không thể di chuyển tiếp" khi bậc ra bằng 0 đảm bảo rằng mọi luồng xác suất cuối cùng đều hội tụ về các đảo có bậc ra 0.
//    - Ta có thể mô phỏng quá trình lan truyền xác suất theo từng bước (từng "đợt" di chuyển).
//    - Khởi tạo: Đảo S có xác suất 1.0 để bắt đầu tại đó. Các đảo khác có xác suất 0.0.
//    - Lặp lại nhiều lần (số bước đủ lớn để xác suất hội tụ):
//        - Tại mỗi bước, xác suất hiện có tại một đảo u sẽ được phân tán đều cho các đảo lân cận nếu out_degree[u] > 0.
//        - Nếu out_degree[u] == 0, xác suất hiện có tại u sẽ "bị kẹt" tại u và không được phân tán tiếp. Xác suất này cộng dồn vào tổng xác suất bị kẹt tại u.
//        - Sử dụng hai mảng xác suất: `current_prob` (xác suất đến đảo ở bước hiện tại) và `next_prob` (xác suất đến đảo ở bước tiếp theo).
//        - Sử dụng một mảng `total_stuck_prob` để cộng dồn xác suất bị kẹt tại các đảo có out_degree == 0.

// 3. Chi tiết thuật toán lan truyền xác suất:
//    - Khởi tạo `current_prob[1...N]` = 0.0, `current_prob[S]` = 1.0.
//    - Khởi tạo `total_stuck_prob[1...N]` = 0.0.
//    - Lặp lại khoảng 1000 đến 2000 lần (số lần lặp đủ lớn cho N=100, đảm bảo xác suất nhỏ hội tụ hoặc trở nên không đáng kể):
//        - Khởi tạo `next_prob[1...N]` = 0.0.
//        - Duyệt qua tất cả các đảo u từ 1 đến N:
//            - Nếu `current_prob[u]` nhỏ (ví dụ: < 1e-15), bỏ qua để tránh lan truyền nhiễu số thực.
//            - Nếu `out_degree[u] == 0`:
//                - Cộng `current_prob[u]` vào `total_stuck_prob[u]`.
//            - Ngược lại (`out_degree[u] > 0`):
//                - Tính xác suất truyền cho mỗi đảo lân cận: `p_transfer = current_prob[u] / out_degree[u]`.
//                - Duyệt qua tất cả các đảo lân cận v của u (adj[u]):
//                    - Cộng `p_transfer` vào `next_prob[v]`.
//        - Gán `current_prob = next_prob` cho lần lặp tiếp theo.

// 4. Tìm kết quả:
//    - Sau khi kết thúc các bước lặp, mảng `total_stuck_prob` sẽ chứa xác suất (đã hội tụ) bị kẹt tại mỗi đảo có out_degree == 0. Các đảo có out_degree > 0 sẽ có `total_stuck_prob` bằng 0.
//    - Tìm xác suất tối đa `max_prob` trong mảng `total_stuck_prob` (chỉ xét các đảo có out_degree == 0).
//    - Thu thập tất cả các chỉ số đảo i mà `out_degree[i] == 0` và `total_stuck_prob[i]` rất gần với `max_prob` (sử dụng sai số 10^-6: `fabs(total_stuck_prob[i] - max_prob) < 1e-6`).
//    - Sắp xếp các chỉ số đảo đã thu thập theo thứ tự tăng dần.
//    - In ra các chỉ số đảo đó, cách nhau bởi dấu cách.

// 5. Lưu ý:
//    - Sử dụng kiểu dữ liệu `double` cho xác suất.
//    - Cần cẩn thận với chỉ số đảo (1-based vs 0-based). Nếu dùng 0-based trong code, nhớ điều chỉnh khi đọc S và in kết quả.
//    - Số lần lặp đủ lớn là quan trọng để xác suất hội tụ, đặc biệt trong các đồ thị có chu trình. 1000-2000 thường đủ cho N=100.

Làm thêm nhiều bài tập miễn phí tại đây

Comments

There are no comments at the moment.