Tác giả:
Reviewer:
Trong đồ thị có hướng, xét các cung được thăm và không được thăm bởi , ta có loại cung sau:
Trong đồ thị vô hướng:
Một số mảng quan trọng trong cây DFS :
Nhận xét : Các đỉnh có thứ tự thăm nằm trong khoảng từ đến chính là các đỉnh nằm trong cây con gốc trong cây .
Cách tính mảng low[], num[], tail[] :
int timeDfs = 0; // Thứ tự duyệt DFS
void dfs(int u, int pre) {
num[u] = low[u] = ++timeDfs;
for (int v : g[u]){
if (v == pre) continue;
if (!num[v]) {
dfs(v, u);
low[u] = min(low[u], low[v]);
}
else low[u] = min(low[u], num[v]);
}
tail[u] = timeDfs;
}
Ví dụ minh họa :
Mô tả quá trình :
Trong đồ thị vô hướng, một đỉnh được gọi là đỉnh khớp nếu như loại bỏ đỉnh này và các cạnh liên thuộc với nó ra khỏi đồ thị thì số thành phần liên thông của đồ thị tăng lên.
Trong đồ thị vô hướng, một cạnh được gọi là cạnh cầu nếu như loại bỏ cạnh này ra khỏi đồ thị thì số thành phần liên thông của đồ thị tăng lên.
GRAPH_ - Tìm khớp và cầu (Cơ bản)
Xét đơn đồ thị vô hướng có đỉnh và cạnh. Người ta định nghĩa một đỉnh gọi là khớp nếu như xoá đỉnh đó sẽ làm tăng số thành phần liên thông của đồ thị. Tương tự như vậy, một cạnh được gọi là cầu nếu xoá cạnh đó sẽ làm tăng số thành phần liên thông của đồ thị.
Vấn đề đặt ra là cần phải đếm tất cả các khớp và cầu của đồ thị .
Input
Output
Example
Input
10 12
1 10
10 2
10 3
2 4
4 5
5 2
3 6
6 7
7 3
7 8
8 9
9 7
Output
4 3
Note
Xét cây con gốc trong cây của có là cha trực tiếp của . Gọi tập hợp các đỉnh thuộc cây con gốc là , tập hợp các đỉnh không thuộc cây con gốc là . Khi xoá đi cạnh thì giữa đỉnh bất kì thuộc cùng tập hợp vẫn có thể đến với nhau bằng các cạnh nét liền. Một đỉnh thuộc với một đỉnh thuộc muốn đi đến với nhau bằng các cạnh nét liền thì đều phải thông qua cạnh .
Giả sử không có cạnh nét đứt nào nối giữa đỉnh thuộc với đỉnh thuộc thì khi xoá cạnh , sẽ tách ra thành vùng liên thông và . Ngược lại nếu tồn tại cạnh nét đứt nối giữa đỉnh thuộc và đỉnh thuộc đồ thị vẫn liên thông . Do đó ta chỉ cần xét xem có tồn tại cạnh nét đứt nối giữa và hay không để kết luận có phải cầu không?
Ta có từ có thể đi đến một đỉnh nào đó có bằng cách đi theo các cung của cây và đi qua không quá cạnh nét đứt và có thứ tự thăm sớm nhất khi . Nếu nằm trong thì phải là tổ tiên của cũng đồng nghĩa với việc hay (vì đồ thị không có cung chéo), nghĩa là tồn tại cạnh nét đứt nối giữa đỉnh thuộc với đỉnh thuộc (vì nếu chỉ đi bằng các cung của cây thì không thể tới một tổ tiên của nó).
Do đó nếu chắc chắn đỉnh thuộc cây con gốc hay thuộc tập hợp khi đó không tồn tại cạnh nét đứt nối giữa đỉnh thuộc với đỉnh thuộc . Tuy nhiên, ta dễ dàng nhận thấy vì đỉnh luôn tới được chính nó.
Ta sẽ xét riêng từng thành phần liên thông của đồ thị. Xét vùng liên thông như sau:
Xét cây con gốc trong cây của , nếu mọi nhánh con của đều có cung ngược lên tới tổ tiên của (, với là tất cả các con trực tiếp của trên cây ) thì đỉnh không thể là đỉnh khớp. Bởi trong đồ thị ban đầu, nếu ta loại bỏ đỉnh đi thì từ mỗi đỉnh bất kỳ thuộc nhánh con vẫn có thể đi lên một tổ tiên của , rồi đi sang nhánh con khác hoặc đi sang tất cả những đỉnh còn lại của cây nên số thành phần liên thông của đồ thị không thay đổi.
Nếu không phải là đỉnh gốc của cây , và tồn tại ít nhất một nhánh con trong cây con gốc không có cung ngược lên một tổ tiên của (, với là một con trực tiếp bất kì của trên cây ) thì đỉnh là đỉnh khớp. Bởi khi đó, tất cả những cung xuất phát từ nhánh con đó chỉ có thể đi tới những đỉnh thuộc cây con gốc mà thôi, trên đồ thị ban đầu, không tồn tại cạnh nối từ những đỉnh thuộc nhánh con đó tới một tổ tiên của . Vậy nên từ một đỉnh bất kì thuộc nhánh con đó muốn đi lên một tổ tiên của thì bắt buộc phải đi qua nên việc loại bỏ đỉnh ra khỏi đồ thị sẽ làm tăng số thành phần liên thông của đồ thị.
Nếu là đỉnh gốc của cây , thì là đỉnh khớp khi và chỉ khi có ít nhất nhánh con. Vì đồ thị không có cung chéo nên khi có nhánh con thì đường đi giữa hai đỉnh thuộc hai nhánh con đó bắt buộc phải đi qua . Việc loại bỏ đỉnh ra khỏi đồ thị sẽ làm tăng số thành phần liên thông của đồ thị.
Kết luận: Đỉnh là đỉnh khớp khi:
Hoặc
Cấu trúc dữ liệu:
maxN = 10010
timeDfs
- Thứ tự bridge
- Số lượng cạnh cầulow[], num[]
joint[]
- Đánh dấu đỉnh khớpg[]
- Danh sách cạnh kề của mỗi đỉnh#include <bits/stdc++.h>
using namespace std;
const int maxN = 10010;
int n, m;
bool joint[maxN];
int timeDfs = 0, bridge = 0;
int low[maxN], num[maxN];
vector <int> g[maxN];
void dfs(int u, int pre) {
int child = 0; // Số lượng con trực tiếp của đỉnh u trong cây DFS
num[u] = low[u] = ++timeDfs;
for (int v : g[u]) {
if (v == pre) continue;
if (!num[v]) {
dfs(v, u);
low[u] = min(low[u], low[v]);
if (low[v] == num[v]) bridge++;
child++;
if (u == pre) { // Nếu u là đỉnh gốc của cây DFS
if (child > 1) joint[u] = true;
}
else if (low[v] >= num[u]) joint[u] = true;
}
else low[u] = min(low[u], num[v]);
}
}
int main() {
cin >> n >> m;
for (int i = 1; i <= m; i++) {
int u, v;
cin >> u >> v;
g[u].push_back(v);
g[v].push_back(u);
}
for (int i = 1; i <= n; i++)
if (!num[i]) dfs(i, i);
int cntJoint = 0;
for (int i = 1; i <= n; i++) cntJoint += joint[i];
cout << cntJoint << ' ' << bridge;
}
Để truy bắt tội phạm, cảnh sát xây dựng một hệ thống máy tính mới. Bản đồ khu vực bao gồm thành phố và đường nối chiều. Các thành phố được đánh số từ đến .
Cảnh sát muốn bắt các tội phạm di chuyển từ thành phố này đến thành phố khác. Các điều tra viên, theo dõi bản đồ, phải xác định vị trí thiết lập trạm gác. Hệ thống máy tính mới phải trả lời được loại truy vấn sau:
Input
Dữ liệu được cho sao cho ban đầu luôn có cách di chuyển giữa hai thành phố bất kỳ.
Output
Example
Input
13 15
1 2
2 3
3 5
2 4
4 6
2 6
1 4
1 7
7 8
7 9
7 10
8 11
8 12
9 12
12 13
5
1 5 13 1 2
1 6 2 1 4
1 13 6 7 8
2 13 6 7
2 13 6 8
Output
yes
yes
yes
no
yes
Note
Dễ thấy rằng, nếu đường nối giữa thành phố không phải là cạnh cầu thì việc loại bỏ nó đi sẽ không ảnh hưởng đến tính liên thông giữa thành phố và .
Ngược lại, nếu đường nối giữa thành phố là cạnh cầu thì ta phải kiểm tra xem thành phố và có thuộc cùng một thành phần liên thông sau khi loại bỏ cạnh hay không?
Mỗi khi loại bỏ một cạnh cầu của đồ thị vô hướng thì số thành phần liên thông của đồ thị sẽ tăng thêm . Nghĩa là khi ta loại bỏ cạnh cầu (với là con trực tiếp của ) thì đồ thị sẽ chia ra làm thành phần liên thông:
Ví dụ minh họa: Loại bỏ cạnh cầu (với đỉnh là con trực tiếp của đỉnh )
Bây giờ, ta có thể xác định vị trí của đỉnh có nằm trong cây con gốc hay không.
Nhắc lại: Nếu thì đỉnh nằm trong cây con gốc của cây .
Dễ thấy rằng, nếu thành phố không phải là đỉnh khớp thì việc loại bỏ nó đi sẽ không ảnh hưởng đến tính liên thông giữa thành phố và .
Ngược lại, nếu thành phố là đỉnh khớp thì ta phải kiểm tra xem thành phố và có thuộc cùng một thành phần liên thông sau khi loại bỏ đỉnh và các cạnh liên thuộc với nó đi hay không?
Vì đồ thị không có cung chéo nên khi loại bỏ đỉnh khớp ra khỏi đồ thị thì số thành phần liên thông của đồ thị tăng thêm một lượng bằng số lượng đỉnh là con trực tiếp của trong cây sao cho không tồn tại cung ngược (cạnh nét đứt) từ một đỉnh thuộc cây con gốc trong cây nối lên tổ tiên của (đồng nghĩa với việc ). Nghĩa là khi ta loại bỏ đỉnh khớp ra khỏi đồ thị thì đồ thị sẽ chia ra làm các thành phần liên thông:
Ví dụ minh họa: Loại bỏ đỉnh khớp . Đỉnh và là các con trực tiếp của đỉnh trong cây . Nhưng chỉ có cây con gốc là tách riêng ra thành thành phần liên thông riêng biệt. Còn cây con gốc thì có cung ngược nối lên đỉnh (tổ tiên của đỉnh ) nên số lượng thành phần liên thông của cả đồ thị chỉ tăng thêm .
Với mỗi đỉnh là con trực tiếp của trong cây và , ta kiểm tra xem nếu chỉ có đúng duy nhất trong đỉnh nằm trong cây con gốc thì thành phố và không thuộc cùng một thành phần liên thông sau khi loại bỏ đỉnh và các cạnh liên thuộc với đỉnh đi.
Ngược lại, với là các con trực tiếp của trong cây và , nếu cả đỉnh và cùng nằm trong cây con gốc hoặc cả đỉnh và đều không nằm trong bất cứ cây con gốc nào cả (đồng nghĩa với việc cả đỉnh sẽ cùng nằm trong thành phần liên thông còn lại) thì thành phố và đều thuộc cùng một thành phần liên thông sau khi loại bỏ đỉnh và các cạnh liên thuộc với đỉnh đi.
Tuy nhiên theo thuật toán trên thì với mỗi truy vấn ta sẽ phải duyệt hết tất cả các con trực tiếp của đỉnh C nên khi xử lí các truy vấn sẽ mất độ phức tạp là O(Q⋅ bậc của C). Trong trường hợp tệ nhất thì đỉnh C có thể lên đến N - 1 con trực tiếp (100000 - 1) với số lượng truy vấn Q = 300000, khiến cho thuật toán trên sẽ bị quá thời gian. Bây giờ ta cần phải cải tiến thuật toán :
Thay vì duyệt hết tất cả các con trực tiếp của để xác định được tổ tiên của , tổ tiên của . Ta có thể sử dụng để tìm ra tổ tiên của đỉnh (hoặc ) là con trực tiếp của đỉnh nếu (hoặc ) nằm trong cây con gốc .
Bạn có thể tìm hiểu thêm về Sparse Table và ứng dụng của nó tại đây.
Gọi đỉnh là tổ tiên của đỉnh và là con trực tiếp của đỉnh .
Gọi đỉnh là tổ tiên của đỉnh và là con trực tiếp của đỉnh .
và thuộc cùng một thành phần liên thông sau khi loại bỏ đỉnh và các cạnh liên thuộc với đỉnh đi khi thỏa mãn một trong số các điều kiện sau:
Ngược lại, nếu không thỏa mãn tất cả các điều kiện trên thì và không thuộc cùng một thành phần liên thông sau khi loại bỏ đỉnh và các cạnh liên thuộc với đỉnh đi.
Lúc này, độ phức tạp để xử lí các truy vấn sẽ là .
Cấu trúc dữ liệu:
maxN = 100010
timeDfs
- Thứ tự low[], num[], tail[]
depth[]
- Lưu chiều sâu của mỗi đỉnh trong cây p[][]
- Mảng ứng dụng với là tổ tiên thứ của trong cây joint[]
- Đánh dấu đỉnh khớpg[]
- Danh sách cạnh kề của mỗi đỉnh#include <bits/stdc++.h>
using namespace std;
const int maxN = 100010;
int n, m, q;
int timeDfs = 0;
int low[maxN], num[maxN], tail[maxN];
int depth[maxN], p[maxN][20];
bool joint[maxN];
vector <int> g[maxN];
/* Tính mảng p */
void calP() {
p[1][0] = 1;
for (int j = 1; j <= 19; j++)
for (int i = 1; i <= n; i++)
p[i][j] = p[p[i][j - 1]][j - 1];
}
/* Tìm tổ tiên của đỉnh u là con trực tiếp của đỉnh par */
int findParent(int u, int par) {
for (int i = 19; i >= 0; i--)
if (depth[p[u][i]] > depth[par]) u = p[u][i];
return u;
}
/* Tìm khớp cầu */
void dfs(int u, int pre) {
int child = 0;
num[u] = low[u] = ++timeDfs;
for (int v : g[u]){
if (v