Thứ Năm, 25 tháng 9, 2014

TÌM HIỂU BÀI TOÀN TÌM ĐƯỜNG ĐI NGẮN NHẤT TRÊN BẢN ĐỒ - [PHẦN 1] GIẢI THUẬT DIJKSTRA

Đối tượng bài viết
Dành cho những bạn mới bắt đầu tìm hiểu về những giải thuật tìm đường, những thắc mắc xoay quanh vấn đề tìm đường đi ngắn nhất.
Loạt bài viết sẽ bao gồm 4 phần:
              Phần 1: Giải thuật Dijkstra.
       Phần 2: Giải thuật Dijkstra với Fibonacci Heap.
       Phần 3: Giải thuật Dijkstra với giải pháp Contraction hierarchies.
       Phần 4: Hướng dẫn hiện thực bài toán tìm đường đi với bản đồ địa lý.

Phạm vi bài viết
Bài viết được tham khảo từ nguồn http://en.wikipedia.org/wiki/Dijkstra's_algorithm, qua đó tôi đã bỏ đi một số nội dung vì xét thấy chưa phù hợp với người vừa bắt đầu tìm hiểu các vấn đề trong bài toán tìm đường đi ngắn nhất.
Bài viết sẽ hướng dẫn demo với C++ tuy nhiên có thể cài đặt tương tự với những ngôn ngữ khác.

Giới thiệu tổng quan
Thuật toán Dijkstra được phát minh bởi nhà khoa học Edsger Dijkstra vào năm 1956 và chính thức công bố vào năm 1959, giúp giải quyết bài toán tìm kiếm đường đi nhắn nhất trên đồ thị không có trọng số âm.

Mục tiêu của thuật toán:
Từ một đỉnh xuất phát của đồ thị, thuật toán sẽ giúp tìm được đường đi ngắn nhất với các đỉnh còn lại, hoặc với 1 đỉnh đích duy nhất. Ví dụ: nếu xem những thành phố là một đỉnh của đồ thị, đường đi lại giữa 2 thành phố là cạnh của đồ thị, Dijkstra sẽ giúp tìm đường đi ngắn nhất từ thành phố A đến thành phố B.

Đánh giá hiệu năng của thuật toán:
STT
Thuật toán
Độ phức tạp
1
Dijkstra với lưu trữ hàng đợi không ưu tiên
O(|V|^2)
2
Dijkstra với Fibonacci heap (sẽ được đề cập ở bài viết sau)
O(|E|+|V|\log|V|) 

Tư tưởng của thuật toán:
Gọi điểm xuất phát là X, điểm đích cần đến là Y, thuất toán Dijkstra sẽ lần lượt thực hiện như sau:
 .        1.Thiết lập giá trị khoảng cách từ X đến với các điểm: giá trị là 0 đối với X->X, là vô cùng X->K (K là một đỉnh nào đó).
 .       2Khởi tạo giá trị cờ (đã ghé thăm) của các đỉnh là false.
         3.Từ đỉnh C hiện tại, xem xét những đỉnh lân cận của C, nếu giá trị của đỉnh lân cận lớn hơn giá trị của C cộng với cạnh nối giữa C và điểm lân cận, ta thay thế giá trị của đỉnh lân cận bằng với giá trị của C cộng với cạnh nối. (Lưu ý: C chính là X trong lần đi đầu tiên).
         4.Nếu đỉnh C đã xem xét hết các lân cận, đánh dấu cờ cho đỉnh C là true, và sẽ không quay lại tính giá trị của C lần nữa.
         5.Nếu đỉnh Y (đỉnh đích) được đánh dấu cờ là true, thì thuật toán kết thúc. Hoặc giá trị nhỏ nhất của các đỉnh chưa được dán cờ là vô cùng thì thuật toán kết thúc, do không có đường đi tồn tại.
         6.Tìm giá trị nhỏ nhất trong các đỉnh chưa được gán cờ, và gán đỉnh đó thành đỉnh hiện tại cần xem xét lân cận, lặp lại bước 3.

Ứng dụng của thuật toán:
Khi hướng đến mục tiêu giải quyết những bài toán về tìm kiếm đường đi trên bản đồ địa lý, chẳng hạn như tìm kiểm đường đi trong thành phố, những khái niệm đỉnh, cạnh của đồ thị được chuyển đổi như sau: những đỉnh của đồ thị mô tả những giao lộ, nút giao thông, cạnh của đồ thị chính là đoạn đường nối giữa 2 nút giao thông. Như vậy việc tìm kiếm đường đi từ 2 giao lộ chính là bài toán tìm kiếm đường đi giữa 2 đỉnh của đồ thị.

Hiện thực thuật toán Dijkstra với ngôn ngữ C++
Danh sách các hàm được dùng:
/* Đọc dữ liệu từ file _IN_FILE lưu thành ma trận kề của đồ thị */
bool readFile();
/* Khởi tạo ma trận kề trước khi đọc file */
void initMatrix();
/* Khởi tạo giá trị chuẩn bị thuật toán Dijktra */
void initDijkstra();
/* Thực hiện thuật toán Dijkstra */
bool doDijkstra();
/* Lấy giá trị nhỏ nhất của khoảng cách từ điểm bắt đầu đến điểm hiện tại, bỏ qua các điểm đã ghé thăm */
int getIdOfMinDistance();
/* In ra đường đi ngắn nhất tìm được */
void printShortestPath();In ra đường đi ngắn nhất tìm được */
Các biến được dùng trong thuật toán:
/* Số đỉnh của đồ thị */
int numOfVertex;
/* Số cạnh của đồ thị */
int numOfEdge;
/* Đỉnh bắt đầu, đỉnh kết thúc */
int sourceId, targetId;
/* Ma trận kề của đồ thị */
int matrix[20][20];
/* Mảng giá trị đường đi ngắn nhất từ đỉnh xuất phát đến đỉnh có id tương ứng */
int distances[20];
/* Mảng đánh dấu đỉnh đã ghé thăm */
bool visited[20];
/* Mảng truy vết đỉnh phía trước đỉnh đã được đánh dấu */
int previous[20];
Đọc vào file dữ liệu với cấu trúc như sau:
·         Hàng đầu tiên: <số đỉnh> <số cạnh> <điểm đầu> <điểm cuối>
·         Danh sách cách cạnh: <đỉnh x> <đỉnh y> <trọng số nối x và y>
bool readFile() {
        ifstream inFile(_IN_FILE);

        // Kiểm tra file mở thành công hay không
        if (inFile.is_open()) {
                // Đọc vào số đỉnh và số cạnh của đồ thị, điểm bắt đầu và điểm kết thúc
                inFile >> numOfVertex >> numOfEdge >> sourceId >> targetId;
                // Khởi tạo ma trận
                initMatrix();
                // Lặp lại số cạnh, đọc vào thông tin của cạnh với tham số như sau: đỉnh x, đỉnh y, cạnh x->y (y->x)
                int x, y, dxy;
                for (int i = 0; i < numOfEdge; i++) {
                        inFile >> x >> y >> dxy;
                        // Gán giá trị cạnh xy yx cho ma trận kề.
                        matrix[x][y] = matrix[y][x] = dxy;
                }
                return true;
        }

        return false;
}
Khởi tạo ma trận kề, và các giá trị cần thiết để bắt đầu thuật toán

void initMatrix() {       
// Khởi tạo ma trận với giá trị vô cùng ở các cạnh
        int value;
        for (int i = 0; i < numOfVertex; i++) {
                for (int j = 0; j < numOfVertex; j++) {
                        // Khởi tạo giá trị vô cùng tại cạnh j, i
                        value = INFINITY;
                        // Giá trị 0 tại giá trị đường đi từ i->i
                        if (i == j) {
                                value = 0;
                        }
                        matrix[i][j] = value;
                }
        }
}

void initDijkstra() {
        // Khởi tạo khoảng cách từ đỉnh bắt đầu đến tất cả các đỉnh còn lại là vô cùng.
        // Khởi tạo mảng đánh dấu đỉnh đã được ghé thăm hay chưa
        for (int i = 0; i < numOfVertex; i++) {
                distances[i] = INFINITY;
                visited[i] = false;
        }
        // Khởi tạo khoảng cách đến đỉnh bắt đầu là 0.
        distances[sourceId] = 0;
        visited[sourceId] = false;
}

Hiện thực thuật toán:
int getIdOfMinDistance() {
        int currentId, minDistance;

        minDistance = INFINITY;
        currentId = 0;

        for (int i = 0; i < numOfVertex; i++) {
                if ((!visited[i]) && (distances[i] < minDistance)) {
                        currentId = i;
                }
        }

        return currentId;
}

bool doDijkstra() {
        int currentId, int dxy;
        while (true) {
                // Tìm giá trị nhỏ nhất cả mảng khoảng cách để tính điểm hiện tại.
                currentId = getIdOfMinDistance();
                // Kiểm tra giá trị của currentId nếu giá trị là vô cùng thì thuật toán kết thúc, không tìm thấy đường đi.
                if (distances[currentId] == INFINITY) {
                        return false;
                }
                else {
                        // Gán đỉnh hiện tại đã ghé thăm
                        visited[currentId] = true;
                        // Nếu đỉnh hiện tại chính là đỉnh đích cần đến thì kết thúc thuật toán
                        if (currentId == targetId) {
                                return true;
                        }
                }
                // Từ điểm hiện tại tiếp tục loang tìm kiếm các đỉnh lân cận

                for (int i = 0; i < numOfVertex; i++) {
                        // Nếu tồn tại cạnh nối giữa currentId và i
                        dxy = matrix[currentId][i];
                        if ((dxy != INFINITY) && (currentId != i)) {
                                // Nếu đỉnh i chưa ghé thăm và khoản cách từ current đến i nhỏ hơn khoảng cách hiện tại của i.
                                if (distances[currentId] + dxy < distances[i]) {
                                        // Thay gía trị mới cho i
                                        distances[i] = distances[currentId] + dxy;
                                        // Đỉnh trước i là current
                                        previous[i] = currentId;
                                }
                        }
                }
        }
}


In ra kết quả tìm được:
void printShortestPath() {
        int currentId = targetId;
        while (true) {
                cout << currentId << " ";
                if (currentId == sourceId) {
                        return;
                }
                currentId = previous[currentId];
        }
}

Kết luận:
Trên thực tế, bài toán tìm đường được gắn với một số tùy chọn khác như: đường 1 chiều, biển báo,… tùy vào tình huống thực tế thuật toán Dijkstra có thể được cài đặt bổ sung.
Bài viết trên đây là góc nhìn cá nhân tôi đối với thuật toán, do đó sẽ còn rất nhiều thiếu sót, mong rằng quý bạn đọc sẽ góp ý thông để tôi dần hoàn thiện hơn. Vui lòng liên hệ levohuutri@gmail.com.
Chúc sức khỏe.

Source code hoàn thiện: https://www.dropbox.com/s/n726juoqyrjnxk5/SourceDijkstra.cpp?dl=0

1 nhận xét: