Computer >> Hướng Dẫn Máy Tính >  >> Lập Trình >> C++

Tìm tất cả cung tiến (Forward Edges) trong đồ thị C++

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)

  1. 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).
  2. Khi xét cạnh (u, v):
    • Nếu v chư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.

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.