Hiểu đúng và viết chuẩn thuật toán tìm đường đi ngắn nhất (SPFA & Dijkstra)

Nhiều lập trình viên gặp khó khăn với các thuật toán tìm đường đi ngắn nhất. Bài viết này sẽ phân tích chi tiết hai thuật toán phổ biến: SPFA và Dijkstra.

Thuật toán SPFA (Shortest Path Faster Algorithm)

Nguyên lý hoạt động

  • Khởi tạo khoảng cách tại đỉnh nguồn bằng 0, các đỉnh khác bằng vô cùng
  • Đưa đỉnh nguồn vào hàng đợi và đánh dấu đang xử lý (vis = true)
  • Lặp: lấy đỉnh đầu hàng đợi, bỏ đánh dấu, thử nới lỏng các cạnh kề
  • Nếu nới lỏng thành công: kiểm tra chu trình âm thông qua số lần vào hàng đợi (cnt)
  • Nếu không có chu trình âm: đánh dấu và đưa đỉnh đó vào hàng đợi
  • Dừng khi phát hiện chu trình âm hoặc hàng đợi rỗng

Mã nguồn tham khảo

Phiên bản 1: Có cạnh trùng (cnt đếm số lần vào hàng đợi)

struct Edge {
    int to, weight;
};

int dist[MAXN];
bool inQueue[MAXN];
int queuedCount[MAXN];

bool spfa(vector<Edge> graph[], int source, int n) {
    queue<int> q;
    memset(dist, 0x3f, (n + 1) * sizeof(int));
    memset(queuedCount, 0, (n + 1) * sizeof(int));
    memset(inQueue, 0, (n + 1) * sizeof(bool));
    
    q.push(source);
    dist[source] = 0;
    inQueue[source] = true;
    queuedCount[source]++;
    
    while (!q.empty()) {
        int u = q.front();
        q.pop();
        inQueue[u] = false;
        
        for (auto edge : graph[u]) {
            int v = edge.to;
            int w = edge.weight;
            
            if (dist[v] > dist[u] + w) {
                dist[v] = dist[u] + w;
                
                if (!inQueue[v]) {
                    queuedCount[v]++;
                    if (queuedCount[v] >= n) return false;
                    inQueue[v] = true;
                    q.push(v);
                }
            }
        }
    }
    return true;
}

Phiên bản 2: Không có cạnh trùng (cnt đếm số lần nới lỏng)

struct Edge {
    int to, weight;
};
vector<Edge> graph[MAXN];
int dist[MAXN], relaxCount[MAXN];
bool inQueue[MAXN];
int n;

queue<int> q;
bool spfa(int source) {
    memset(dist, 0x3f, (n + 1) * sizeof(int));
    q.push(source);
    inQueue[source] = true;
    dist[source] = 0;
    
    while (!q.empty()) {
        int u = q.front();
        q.pop();
        inQueue[u] = false;
        
        for (auto e : graph[u]) {
            int v = e.to, w = e.weight;
            if (dist[v] > dist[u] + w) {
                dist[v] = dist[u] + w;
                relaxCount[v] = relaxCount[u] + 1;
                if (relaxCount[v] >= n) return false;
                if (!inQueue[v]) {
                    inQueue[v] = true;
                    q.push(v);
                }
            }
        }
    }
    return true;
}

Lưu ý quan trọng

  • Ý nghĩa của biến đếm (cnt/relaxCount) phụ thuộc vào việc có cạnh trùng hay không
  • Không quên memset các mảng trước khi sử dụng
  • Đánh dấu vis khi: đưa vào hàng đợi lần đầu, lấy ra khỏi hàng đợi, và đưa vào lại hàng đợi
  • Kiểm tra kiểu dữ liệu của trọng số cạnh (int, long long,...)

Thuật toán Dijkstra

Nguyên lý hoạt động

  • Khởi tạo khoảng cách tại đỉnh nguồn bằng 0
  • Đưa đỉnh nguồn vào hàng đợi ưu tiên (min-heap)
  • Lặp: lấy đỉnh có dist nhỏ nhất từ hàng đợi
  • Nếu đỉnh đã được xác nhận đường đi ngắn nhất (vis = true) thì bỏ qua
  • Nếu chưa: đánh dấu đã xác nhận, nới lỏng các cạnh kề
  • Với mỗi cạnh nới lỏng thành công, đưa đỉnh đích vào hàng đợi

Mã nguồn tham khảo

struct Edge {
    int to, weight;
};

struct Node {
    int distance, vertex;
    bool operator>(const Node& other) const {
        return distance > other.distance;
    }
};

vector<Edge> adjacency[MAXN];
int dist[MAXN], visited[MAXN];
priority_queue<Node, vector<Node>, greater<Node>> pq;

void dijkstra(int n, int source) {
    memset(dist, 0x3f, (n + 1) * sizeof(int));
    memset(visited, 0, (n + 1) * sizeof(int));
    dist[source] = 0;
    pq.push({0, source});
    
    while (!pq.empty()) {
        int u = pq.top().vertex;
        pq.pop();
        if (visited[u]) continue;
        visited[u] = 1;
        
        for (auto edge : adjacency[u]) {
            int v = edge.to, w = edge.weight;
            if (dist[v] > dist[u] + w) {
                dist[v] = dist[u] + w;
                pq.push({dist[v], v});
            }
        }
    }
}

Lưu ý quan trọng

  • Cần hai cấu trúc dữ liệu: Edge cho danh sách kề, Node cho hàng đợi ưu tiên
  • Không đánh dấu visited cho đỉnh nguồn hoặc bất kỳ đỉnh nào trước khi xử lý
  • Phải lưu cấu trúc Node trong hàng đợi ưu tiên, không chỉ lưu đỉnh
  • Kiểm tra visited sau khi lấy đỉnh từ hàng đợi, trước khi xử lý

Thẻ: SPFA Dijkstra đồ thị tìm đường đi ngắn nhất C++

Đăng vào ngày 18 tháng 6 lúc 05:07