- Ứng dụng trong bản đồ số: Các ứng dụng bản đồ và định vị sử dụng thuật toán tìm đường đi ngắn nhất để chỉ đường và cung cấp các lộ trình tối ưu cho người dùng.. - Thuật toán Dijkstra:
Trang 1TRƯỜNG ĐẠI HỌC BÁCH KHOA
KHOA CÔNG NGHỆ THÔNG TIN
ĐỒ ÁN LẬP TRÌNH TÍNH TOÁN
ĐỀ TÀI 107: BÀI TOÁN TRÊN ĐỒ THỊ
Người hướng dẫn: PGS TS NGUYỄN VĂN HIỆU
Sinh viên thực hiện:
Dương Minh Hữu LỚP: 23T_Nhat2 MÃ SV: 102230073
Đà Nẵng, 03/2024
Trang 26 TÀI LIỆU THAM KHẢO 24
7 PHỤ LỤC 25
Trang 31
MỞ ĐẦU
Thuật toán tìm đường đi ngắn nhất nhằm mục đích xác định con đường hoặc tuyến đường ngắn nhất giữa hai điểm trong một không gian hoặc mạng lưới Các thuật toán này được sử dụng rộng rãi trong nhiều lĩnh vực, bao gồm công nghệ, giao thông vận tải, logistics, viễn thông và nhiều ứng dụng khác Một số mục đích cụ thể của thuật toán tìm đường đi ngắn nhất bao gồm:
- Tối ưu hóa đường đi: Xác định đường đi hiệu quả nhất giữa hai điểm, giúp
tiết kiệm thời gian, chi phí hoặc tài nguyên
- Lập kế hoạch và điều phối: Trong ngành logistics và vận tải, thuật toán tìm
đường đi ngắn nhất giúp lập kế hoạch vận chuyển hiệu quả, tối ưu hóa việc giao hàng và phân phối
- Ứng dụng trong bản đồ số: Các ứng dụng bản đồ và định vị sử dụng thuật
toán tìm đường đi ngắn nhất để chỉ đường và cung cấp các lộ trình tối ưu cho người dùng
- Mạng máy tính và viễn thông: Thuật toán tìm đường đi ngắn nhất giúp định
tuyến dữ liệu hiệu quả trong mạng máy tính và hệ thống viễn thông, đảm bảo thông tin được truyền qua các kênh tốt nhất
- Quản lý giao thông: Các cơ quan giao thông sử dụng thuật toán này để tối
ưu hóa luồng giao thông, giảm tắc nghẽn và nâng cao an toàn
- Hỗ trợ ra quyết định: Thuật toán này cũng có thể được sử dụng để hỗ trợ
trong việc ra quyết định trong nhiều lĩnh vực khác nhau, nơi cần lựa chọn phương
án tốt nhất giữa nhiều lựa chọn
Trang 4- Thuật toán Dijkstra: Đây là một trong những thuật toán phổ biến nhất để
giải bài toán tìm đường đi ngắn nhất trên đồ thị Thuật toán Dijkstra thường được sử dụng để tìm đường đi ngắn nhất từ một đỉnh đến tất cả các đỉnh còn lại trên đồ thị, hoặc từ một đỉnh đến một đỉnh cụ thể
- Thuật toán Floyd-Warshall: Thuật toán này được sử dụng để tìm tất cả các
đường đi ngắn nhất giữa tất cả các cặp đỉnh trên đồ thị, không chỉ tìm một đường đi ngắn nhất giữa hai đỉnh như Dijkstra hay Bellman-Ford
2.2 Cơ sở lý thuyết
Thuật toán Dijkstra tìm đường đi ngắn nhất từ một đỉnh nguồn đến tất cả các đỉnh khác trong một đồ thị có trọng số không âm Cơ sở lý thuyết của thuật toán này dựa trên:
• Greedy Algorithm (Thuật toán tham lam): Dijkstra hoạt động theo
nguyên tắc tham lam, nghĩa là nó chọn đường đi tốt nhất (ngắn nhất) có sẵn tại mỗi bước, dựa trên các thông tin đã biết
• Relaxation (Giãn cạnh): Khái niệm giãn cạnh (relaxation) là khi thuật
toán cố gắng tìm đường đi ngắn hơn đến một đỉnh thông qua một đỉnh trung gian Nếu nó tìm thấy một đường đi ngắn hơn, nó sẽ cập nhật giá trị khoảng cách đến đỉnh đó
• Min-Heap (Đống tối thiểu): Để chọn cạnh hoặc đỉnh với trọng số nhỏ
nhất một cách hiệu quả, Dijkstra thường sử dụng cấu trúc dữ liệu heap (thường là đống tối thiểu) Heap giúp tăng hiệu suất khi truy cập và cập nhật các giá trị trong quá trình tìm kiếm
• Thuật toán Floyd-Warshall tìm đường đi ngắn nhất giữa tất cả các cặp đỉnh trong một đồ thị có trọng số (có thể có trọng số âm, nhưng không có chu trình âm) Cơ sở lý thuyết của thuật toán này dựa trên:
Trang 5• Dynamic Programming (Lập trình động): Floyd-Warshall là một thuật
toán dựa trên lập trình động, nghĩa là nó giải quyết bài toán lớn hơn bằng cách chia nó thành các bài toán con nhỏ hơn và sử dụng kết quả của các bài toán con
để giải quyết bài toán lớn hơn
• Transitive Closure (Tính chất chuyển tiếp): Thuật toán dựa trên ý
tưởng rằng nếu bạn có đường đi ngắn nhất từ đỉnh i đến đỉnh k, và từ đỉnh k đến đỉnh j, thì bạn có thể tìm đường đi ngắn nhất từ i đến j bằng cách nối chúng lại Floyd-Warshall sử dụng nguyên lý này để liên tục cập nhật ma trận khoảng cách giữa các cặp đỉnh
• Triple Nested Loops (Vòng lặp lồng nhau ba lần): Thuật toán này thực
hiện ba vòng lặp lồng nhau để kiểm tra tất cả các cặp đỉnh i, j, và đỉnh trung gian
k Nếu đường đi qua đỉnh trung gian k ngắn hơn đường đi trực tiếp từ i đến j, nó
sẽ cập nhật ma trận khoảng cách
3 TỔ CHỨC CẤU TRÚC DỮ LIỆU VÀ THUẬT TOÁN
3.1 Phát biểu bài toán
+ Một đồ thị có hướng hoặc vô hướng với các cạnh có thể có trọng số âm
(nhưng không có chu trình âm) Đồ thị thường được biểu diễn dưới dạng ma trận kề
+ Ma trận kề ban đầu có thể sử dụng inf (vô cùng) cho các cặp đỉnh không kết nối trực tiếp, và 0 cho các đường từ một đỉnh đến chính nó
3.2 Cấu trúc dữ liệu
Thuật toán Dijkstra thường sử dụng các cấu trúc dữ liệu sau:
+ Danh sách kề (Adjacency List): Một danh sách kề lưu trữ các cạnh của đồ
thị theo cách mà mỗi đỉnh có một danh sách các cạnh liên kết với nó Mỗi cạnh
Trang 6thường được biểu diễn dưới dạng một cặp (đỉnh, trọng số) Danh sách kề giúp tìm nhanh các cạnh xuất phát từ một đỉnh cụ thể
+ Đống tối thiểu (Min-Heap) hoặc Queue ưu tiên (Priority Queue): Để tìm
đỉnh có trọng số nhỏ nhất nhanh chóng, Dijkstra thường sử dụng đống tối thiểu hoặc queue ưu tiên Khi thuật toán cần chọn đỉnh với khoảng cách ngắn nhất để xử lý tiếp,
nó lấy đỉnh từ đống tối thiểu
+ Mảng khoảng cách (Distance Array): Mảng này lưu trữ khoảng cách ngắn
nhất hiện tại từ đỉnh nguồn đến mỗi đỉnh khác Khi thuật toán tìm thấy đường đi ngắn hơn, mảng này được cập nhật
+ Mảng đỉnh trước (Predecessor Array): Mảng này lưu trữ đỉnh trước đó trên
đường đi ngắn nhất từ đỉnh nguồn Điều này giúp tái tạo đường đi ngắn nhất sau khi thuật toán hoàn thành
Floyd-Warshall sử dụng các cấu trúc dữ liệu sau:
+ Ma trận khoảng cách (Distance Matrix): Đây là ma trận n x n (n là số đỉnh
trong đồ thị), với dist[i][j] là khoảng cách ngắn nhất từ đỉnh i đến j Ma trận này được khởi tạo với trọng số cạnh hoặc vô cực (inf) nếu không có cạnh trực tiếp, và 0 cho các đường từ một đỉnh đến chính nó
+ Ma trận đỉnh trung gian (Predecessor Matrix): Đây là một ma trận n x n,
cho phép theo dõi đỉnh trung gian trên đường đi ngắn nhất giữa các cặp đỉnh Khi tìm đường đi ngắn nhất giữa hai đỉnh, thuật toán sử dụng ma trận này để tái tạo đường đi
+ Vòng lặp lồng nhau ba lần: Mặc dù không phải là cấu trúc dữ liệu, thuật
toán Floyd-Warshall sử dụng ba vòng lặp lồng nhau để cập nhật ma trận khoảng cách dựa trên đỉnh trung gian k
3.3 Thuật toán
3.3.1 Dijkstra
1 Khởi tạo các biến:
1 int u,minp;
• ‘u’ là biến lưu chỉ số của đỉnh đang được xét trong quá trình tìm kiếm
• ‘minp’ là biến lưu giá trị khoảng cách nhỏ nhất tạm thời
Trang 72 Khởi tạo khoảng cách và trạng thái cho các đỉnh:
• ‘d[v]’ là mảng lưu khoảng cách ngắn nhất từ đỉnh nguồn ‘s’ đến đỉnh ‘v’ Ban
đầu, khoảng cách từ ‘s’ đến ‘v’ được khởi tạo bằng giá trị trong ma trận trọng số
‘matran[s][v]’
• ‘truoc[v]’ là mảng lưu đỉnh trước của đỉnh ‘v’ trên đường đi ngắn nhất từ ‘s’
đến ‘v’ Ban đầu, tất cả các đỉnh đều có đỉnh trước là ‘s’
• ‘chuaxet[v]’ là mảng đánh dấu trạng thái của đỉnh ‘v’ Ban đầu, tất cả các đỉnh đều chưa được xét (FALSE)
3 Khởi tạo đỉnh nguồn:
• truoc[s] = 0 để biểu thị rằng ‘s’ không có đỉnh trước (điểm bắt đầu)
4 Vòng lặp chính của thuật toán Dijkstra:
Trang 8• Vòng lặp tiếp tục cho đến khi đỉnh đích ‘t’ đã được xét (chuaxet[t] là TRUE)
• Trong mỗi vòng lặp, tìm đỉnh ‘u’ chưa được xét có khoảng cách nhỏ nhất
(minp) từ đỉnh nguồn
• Đánh dấu đỉnh ‘u’ đã được xét (chuaxet[u] = TRUE)
• Nếu đỉnh đích ‘t’ chưa được xét, cập nhật khoảng cách của các đỉnh kề với u:
• Nếu đường đi qua ‘u’ đến ‘v’ ngắn hơn đường đi hiện tại (d[u] +
matran[u][v] < d[v]), cập nhật khoảng cách mới (d[v] = d[u] + matran[u][v]) và cập nhật đỉnh trước (truoc[v] = u)
Trước tiên chúng ta phải hiểu rõ nội dung của phương pháp Dijkstra:
(Trích từ tài liệu TS Nguyễn Văn Hiệu, Bài tham khảo Đồ án lập trình tính toán – tìm đường đi ngắn nhất với Dijkstra, Khoa CNTT ĐHBK – ĐHĐN)
Thuật toán Dijkstra
Cho G=(X,E) là một đồ thị có trọng không âm gồm n đỉnh Thuật toán Dijkstra được dùng để tìm đường đi ngắn nhất từ đỉnh i đến j cho trước
Gọi L là ma trận trọng lượng (với qui ước Lhk = +∞ nếu không có cạnh nối từ đỉnh
h đến đỉnh k) Ta sử dụng thêm hai mảng để lưu vết của quá trình tìm đường đi:
- Length[…] : lưu độ dài từ đỉnh đầu i đến các đỉnh trong đồ thị
- Last[…] : lưu đỉnh liền trước nó trên đường đi
Trang 9Chú ý: Khi thuật toán dừng, nếu Length[j] = +∞ thì không tồn tại đường đi từ i đến j,
nếu ngược lại thì Length[j] là độ dài đường đi ngắn nhất
Trang 10Bước 4: Các đỉnh còn lại đều có Length[] cực đại nên ta cập nhật lại như sau
Trang 11Last[1] 2 = −1
3 Length[3]2 = 5
Bước 4 (lần 2): tính độ dài từ đỉnh 0 vừa xét ở trên đến các đỉnh còn lại trong T.
Ðỉnh số 3 có chi phí mới là 2+3=5 nhỏ hơn chi phí cũ (6), vì vậy ta cập nhật lại đỉnh này.
Trang 133 Length[3] 4 = 5
2 Vòng lặp chính của thuật toán Floyd – Warshall:
1 void floydWarshall(int n) {
Trang 14• Cập nhật a[i][j] qua đỉnh k:
• Nếu a[i][k] + a[k][j] < a[i][j] với a[i][k] và a[k][j] tồn tại đường đi, thì a[i][j] = a[i][k] + a[k][j] (được hiểu là nếu qua đỉnh k đường đi ngắn hơn thì chọn đường đi qua k)
• Và cập nhật next1[i][j]
• Trong trường hợp ngược lại thì sẽ không cập nhật qua điểm k “next1[i][j]” lưu trữ đỉnh kế tiếp trên đường đi ngắn nhất từ đỉnh i tới đỉnh j:
• Dùng để biểu diễn ma trận khoảng cách trong thuật toán
• Dùng để kết luận đơn giản hơn
Trước tiên chúng ta phải hiểu rõ nội dung của phương pháp Floyd - Warshall:
Bước 1 Xác định ma trận trọng số D và ma trận đường đi P
Bước 2 Cập nhật ma trận trọng số và ma trận đường đi qua từng đỉnh k
Trang 15Bước 3 Viết ma trận trọng số sau khi cập nhật và kết luận thông qua ma trận đường đi
Ví dụ Floyd – Warshall
Trang 16Cập nhật qua đình A: Ma trận khoảng cách:
Cập nhật qua đỉnh B: Ma trận khoảng cách:
Trang 17Cập nhật qua đỉnh C: Ma trận khoảng cách:
Cập nhật qua đỉnh D: Ma trận khoảng cách:
Trang 18Cập nhật qua đỉnh E: Ma trận khoảng cách:
Cập nhật qua đỉnh F: Ma trận khoảng cách:
Từ ma trận khoảng cách sau khi cập nhật qua tất cả cách đỉnh, ta kết luận:
- Không có đường đi từ A tới A
- Đường đi từ A tới B: A B
- Đường đi từ A tới C: A C
- Đường đi từ A tới D: A B D
- Đường đi từ A tới E: A B E
- Đường đi từ A tới F: A B E F
- Không có đường đi từ B tới A
- Không có đường đi từ B tới B
Trang 19- Không có đường đi từ B tới C
- Đường đi từ B tới D: B D
- Đường đi từ B tới E: B E
- Đường đi từ B tới F: B E F
- Không có đường đi từ C tới A
- Không có đường đi từ C tới B
- Không có đường đi từ C tới C
- Đường đi từ C tới D: C D
- Đường đi từ C tới E: C E
- Đường đi từ C tới F: C E F
- Không có đường đi từ D tới A
- Không có đường đi từ D tới B
- Không có đường đi từ D tới C
- Không có đường đi từ D tới D
- Đường đi từ D tới E: D E
- Đường đi từ D tới F: D F
- Không có đường đi từ E tới A
- Không có đường đi từ E tới B
- Không có đường đi từ E tới C
- Không có đường đi từ E tới D
- Không có đường đi từ E tới E
- Đường đi từ E tới F: E F
- Không có đường đi từ F tới A
- Không có đường đi từ F tới B
- Không có đường đi từ F tới C
- Không có đường đi từ F tới D
- Không có đường đi từ F tới E
- Không có đường đi từ F tới F
Trang 20thống, mặc dù nó cũng được dùng cho việc viết các ứng dụng Ngoài ra, C cũng
thường được dùng làm phương tiện giảng dạy trong khoa học máy tính
Trang 21Hình 2 Kết quả thực thi chương trình
Hình 3 Kết quả nhận được ở file xuatfiledijkstra.txt
Hình 4 Đồ thị của bài toán
Trang 22Chương trình Floyd – Warshall
Hình 5 Đồ thị bài toán
Hình 6 Nhập số đỉnh và ma trận trọng số của đồ thị trên (999 được hiểu là vô cùng)
Hình 7 Kết quả thực thi chương trình
Kết quả trên được hiểu với A=1,B=2,C=3,D=4,E=5,F=6
Trang 23Nhận xét đánh giá : Từ đồ thị ta có thể thấy, chương trình đã giải quyết hoàn toàn
chính xác kết quả
4.4 So sánh hai thuật toán
1 Thuật toán Dijkstra
Đặc điểm chính:
đỉnh khác trong đồ thị (đơn nguồn)
• Với cấu trúc dữ liệu hàng đợi ưu tiên (priority queue) và đống (heap), độ phức tạp là 𝑂((𝑉+𝐸)log𝑉), trong đó 𝑉 là số đỉnh và 𝐸 là số cạnh
đỉnh có đường đi ngắn nhất hiện tại chưa được xử lý và cập nhật khoảng cách đến các đỉnh lân cận
• Không xử lý được đồ thị có cạnh trọng số âm
• Chỉ tìm được đường đi ngắn nhất từ một đỉnh nguồn, không phải giữa mọi cặp đỉnh
2 Thuật toán Floyd-Warshall
Đặc điểm chính:
thị
âm (negative weight cycle)
• Độ phức tạp thời gian: 𝑂(𝑉3) , trong đó 𝑉 là số đỉnh
cập nhật dần các khoảng cách ngắn nhất giữa tất cả các cặp đỉnh bằng cách xét từng đỉnh trung gian
Trang 24Ưu điểm:
• Tìm đường đi ngắn nhất giữa mọi cặp đỉnh
• Xử lý được đồ thị có cạnh trọng số âm (miễn là không có chu trình âm)
Nhược điểm:
• Độ phức tạp thời gian cao 𝑂(𝑉3) , không hiệu quả với đồ thị lớn
• Sử dụng nhiều bộ nhớ vì cần lưu ma trận khoảng cách giữa tất cả các cặp đỉnh
So sánh chi tiết
Khi nào nên sử dụng thuật toán nào?
có cạnh trọng số âm Thích hợp cho các đồ thị thưa với nhiều đỉnh và ít cạnh
đồ thị có thể có trọng số âm (không chu trình âm) Thích hợp cho các đồ thị nhỏ hoặc khi cần tính khoảng cách giữa mọi cặp đỉnh
Tóm lại: Cả hai thuật toán Dijkstra và Floyd-Warshall đều có những ứng
dụng và ưu điểm riêng tùy vào yêu cầu cụ thể của bài toán và đặc điểm của đồ thị Việc lựa chọn thuật toán phù hợp sẽ giúp tối ưu hóa hiệu suất và tài
Trang 25đòi hỏi chúng ta phải có một lượng kiến thức nhất định để thực hiệnĐây là một đề tài
có thể nói là khá hay
5.2 Hướng phát triển
Nhóm nhận thấy phần code của nhóm còn khá dài, chưa được tối ưu một cách
tốt nhất để giảm độ dài cho chương trình cũng như giảm độ phức tạp của thuật toán do
đó nhóm sẽ tiếp tục tìm hiểu và nghiên cứu để làm sao có thể cải thiện được thuật toán
và giải quyết được vấn đề đã nêu ở trên Sử dụng các ngôn ngữ bậc cao hơn để có thể
sử dụng luôn cấu trúc dữ liệu có trong các thư viện, ít mất thời gian thiết lập và bớt
phức tạp hơn
===========================Thanks=============================
Trang 26TÀI LIỆU THAM KHẢO
[1] Nguyễn Văn Hiệu, Giáo trình Toán Rời Rạc, Đại học Bách Khoa - Đại học
Đà Nẵng
[2] Đặng Thiên Bình, Cấu trúc dữ liệu, Đại học Bách Khoa - Đại học Đà Nẵng [3] Nguyễn Thị Lệ Quyên, Giáo trình Kĩ thuật lập trình, Đại học Bách Khoa - Đại học Đà Nẵng
Trang 2727 printf( "Do dai duong di ngan nhat tu %d den %d la: %d\n" ,s,t,d[t]);
28 fprintf(f, "Do dai duong di ngan nhat tu %d den %d la: %d\n" ,s,t,d[t]);
29 printf( "Duong di ngan nhat tu %d den %d la:\n" ,s,t);
30 fprintf(f, "Duong di ngan nhat tu %d den %d la:\n" ,s,t);
Trang 289 void khoiTaoMaTran(int n);
10 void floydWarshall(int n);
11 void inMaTranKhoangCach(int n);
12 void inDuongDi(int n);
27 void khoiTaoMaTran(int n) {
28 printf( "Nhap ma tran trong so cua do thi (nhap %d cho vo cung): \n" , oo);
29 for (int i = 1; i <= n; i++) {
Trang 2943 for (int i = 1; i <= n; i++) {
54 void inMaTranKhoangCach(int n) {
55 FILE* fpOut = fopen( "floydwarshall.txt" , "w" );
56 printf( "Ma tran khoang cach:\n" );
57 fprintf(fpOut, "Ma tran khoang cach:\n" );
58 for (int i = 1; i <= n; i++) {
70 void inDuongDi(int n) {
71 FILE* fpOut = fopen( "floydwarshall.txt" , "a" );
79 printf( "Khong có duong di tu %d toi %d\n" , i, j);
80 fprintf(fpOut, "Khong co duong di tu %d toi %d\n" , i, j);
81 continue;
82 }
83 if (i != j) {
84 printf( "Duong di tu %d toi %d: " , i, j);
85 fprintf(fpOut, "Duong di tu %d toi %d: " , i, j);