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

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 và độ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:
- 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:
- Bắt đầu một quá trình BFS từ đỉnh đó. Tô màu đỉnh bắt đầu bằng màu 0.
- Thêm đỉnh này vào hàng đợi (queue).
- 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ủau
:- Nếu
v
chưa được tô màu: Tô màuv
bằng màu đối diện với màu củau
(nếuu
màu 0 thìv
màu 1, nếuu
màu 1 thìv
màu 0). Sau đó, thêmv
vào hàng đợi. - Nếu
v
đã được tô màu và màu củav
lại giống màu củau
: Đồ thị không phải là đồ thị hai phía. Dừng lại và trả vềfalse
.
- Nếu
- Lấy một đỉnh
- 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.
- 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 đỉnhi
. 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àu0
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ạiu
, chúng ta kiểm tra màu củav
:- 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ủau
(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
- 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.
- Một hàng đợi
- 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ọiisBipartiteBFS
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àmcheckBipartite
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:
- 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
. - 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:
- Bắt đầu một quá trình DFS từ đỉnh đó. Tô màu đỉnh bắt đầu bằng màu 0.
- Hàm DFS đệ quy
DFS(u, current_color)
:- Tô màu đỉnh
u
bằngcurrent_color
. - Với mỗi đỉnh
v
là hàng xóm củau
:- Nếu
v
chưa được tô màu: Gọi đệ quyDFS(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 và màu củav
lại giống màu củau
: Phát hiện mâu thuẫn. Trả vềfalse
.
- Nếu
- 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
.
- Tô màu đỉnh
- 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ó). - 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
adj
vàcolor
tương tự như trong ví dụ BFS. - Hàm
isBipartiteDFS(u, c, adj, color)
thực hiện đệ quy DFS từ đỉnhu
, cố gắng tô màu nó bằng màuc
.- Đỉnh
u
được tô màuc
. - Vòng lặp
for (int v : adj[u])
duyệt qua các hàng xóm củau
. - Nếu
v
chưa được tô màu (color[v] == -1
), hàm gọi đệ quyisBipartiteDFS
chov
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
.
- Đỉnh
- 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ọiisBipartiteDFS
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.
Comments