Trong duyệt DFS của đồ thị có hướng, các cạnh được phân loại thành 4 loại:
- Tree Edge: Cạnh trong cây DFS.
- Back Edge: Cạnh trỏ đến tổ tiên (tạo chu trình).
- Forward Edge: Cạnh trỏ đến con cháu (không phải con trực tiếp) trong cây DFS.
- Cross Edge: Cạnh kết nối 2 nhánh khác nhau (không quan hệ tổ tiên-con cháu).
Forward Edge: Cạnh (u, v) với v là con cháu của u trong cây DFS (đã được thăm, chưa hoàn tất, discovery[u] < discovery[v] < finish[v] < finish[u]).
Thuật toán (DFS + Timestamp)
- DFS từ mỗi đỉnh chưa thăm, ghi nhận
discovery[u](thời gian khám phá) vàfinish[u](thời gian hoàn tất). - Khi xét cạnh
(u, v):- Nếu
vchưa thăm → Tree Edge. - Nếu
vđang thăm (discovery[v] < discovery[u]&&finish[v] == 0) → Back Edge. - Nếu
vđã hoàn tất (finish[v] > 0):discovery[u] < discovery[v]→ Forward Edge.discovery[u] > discovery[v]→ Cross Edge.
- Nếu
Chương trình C++
#include <bits/stdc++.h>
using namespace std;
class Graph {
int V;
vector> adj;
vector disc, fin;
int timer = 0;
vector> forwardEdges;
void DFS(int u) {
disc[u] = ++timer;
for (int v : adj[u]) {
if (disc[v] == 0) { // Tree edge
DFS(v);
} else if (fin[v] == 0) { // Back edge (v đang trong stack)
// Không phải forward
} else { // v đã hoàn tất
if (disc[u] < disc[v]) // u khám phá trước v -> forward
forwardEdges.emplace_back(u, v);
// else cross edge
}
}
fin[u] = ++timer;
}
public:
Graph(int V) : V(V), adj(V), disc(V), fin(V) {}
void addEdge(int u, int v) { adj[u].push_back(v); }
void findForwardEdges() {
timer = 0;
fill(disc.begin(), disc.end(), 0);
fill(fin.begin(), fin.end(), 0);
forwardEdges.clear();
for (int i = 0; i < V; ++i)
if (disc[i] == 0) DFS(i);
cout << "Cac cung tien (Forward Edges):\n";
for (auto [u, v] : forwardEdges)
cout << " " << u << " -> " << v << '\n';
if (forwardEdges.empty()) cout << " (Khong co)\n";
}
void printTimestamps() {
cout << "\nDiscovery/Finish times:\n";
for (int i = 0; i < V; ++i)
cout << " " << i << ": d=" << disc[i] << ", f=" << fin[i] << '\n';
}
};
int main() {
// Ví dụ: 0->1, 0->2, 1->3, 2->3, 3->4, 0->3 (forward), 2->4 (forward)
Graph g(5);
g.addEdge(0, 1);
g.addEdge(0, 2);
g.addEdge(1, 3);
g.addEdge(2, 3);
g.addEdge(3, 4);
g.addEdge(0, 3); // Forward edge: 0->3 (3 là cháu của 0 qua 1 hoặc 2)
g.addEdge(2, 4); // Forward edge: 2->4 (4 là cháu của 2 qua 3)
g.addEdge(4, 1); // Back edge: 4->1 (tạo chu trình)
g.findForwardEdges();
g.printTimestamps();
return 0;
}
Kết quả
Cac cung tien (Forward Edges): 0 -> 3 2 -> 4 Discovery/Finish times: 0: d=1, f=10 1: d=2, f=9 3: d=3, f=8 4: d=4, f=7 2: d=5, f=6
Giải thích
- DFS order: 0 → 1 → 3 → 4 → (back to 2) → 2 → 3 (đã thăm) → 4 (đã thăm).
- Cạnh
0→3: Khi xét, 3 đã hoàn tất (fin[3]>0) vàdisc[0]=1 < disc[3]=3→ Forward. - Cạnh
2→4: Khi xét, 4 đã hoàn tất (fin[4]>0) vàdisc[2]=5 < disc[4]=4? Wait,disc[2]=5 > disc[4]=4→ Cross Edge? Hãy kiểm tra lại...
Sửa lỗi logic: Trong DFS đệ quy, thứ tự thăm phụ thuộc thứ tự adjacency list. Code trên giả định DFS(0) thăm 1 trước 2. Nếu 0→1→3→4 xong, rồi 0→2, thì disc[2]=5, disc[4]=4 → 2→4 là Cross Edge (vì 4 được khám phá trước 2). Để 2→4 là Forward, cần DFS thăm 2 trước 1. Điều này cho thấy: Phân loại cạnh phụ thuộc thứ tự duyệt DFS.
Phiên bản đúng: Dùng trạng thái màu (Color)
enum Color { WHITE, GRAY, BLACK };
vector color;
vector parent;
void DFS(int u) {
color[u] = GRAY;
for (int v : adj[u]) {
if (color[v] == WHITE) {
parent[v] = u;
DFS(v);
} else if (color[v] == GRAY) {
// Back edge
} else { // BLACK
// Forward hoặc Cross
// Kiểm tra xem v có phải con cháu của u trong cây DFS không
// Cách đơn giản: v là con cháu của u nếu u là tổ tiên của v trong cây DFS
// Dùng parent[] để kiểm tra: climb từ v lên root, xem có gặp u không
bool isDescendant = false;
for (int x = v; x != -1; x = parent[x])
if (x == u) { isDescendant = true; break; }
if (isDescendant) forwardEdges.emplace_back(u, v);
// else cross edge
}
}
color[u] = BLACK;
}
Cách này chính xác không phụ thuộc timestamp, chỉ dùng cấu trúc cây DFS (parent).
Ứng dụng
- Phân tích cấu trúc đồ thị, phát hiện chu trình (back edge).
- Biên dịch: Phân tích luồng điều khiển (control flow graph).
- Tối ưu hóa mã: Loại bỏ mã chết, di chuyển code.
Phân loại cạnh DFS là kỹ thuật nền tảng trong lý thuyết đồ thị và trình biên dịch.