29
Cho một đồ thị có hướng gồm n đỉnh mã số từ 1..n với các cung (u, v) có hướng đi từ đỉnh u đến đỉnh v và có chiều dài thể hiện đường đi nối từ đỉnh u đến đỉnh v. Viết chương trình tìm mọi đường đi ngắn nhất từ một đỉnh s cho trước tới các đỉnh còn lại của đồ thị.
Dữ liệu vào được ghi trong một tệp văn bản tên DIJ.INP có cấu trúc như sau:
- Dòng đầu ghi hai số tự nhiên n và s cách nhau bởi dấu cách, trong đó n là số lượng đỉnh của đồ thị, s là số hiệu của đỉnh xuất phát.
- Từ dòng thứ hai ghi lần lượt độ dài đường đi từ đỉnh i đến các đỉnh 1, 2,..., n; i = 1..n. Giá trị 0 cho biết không có cung nối hai đỉnh tương ứng. Với mọi đỉnh i = 1..n, cung (i, i) được xem là không tồn tại và ghi chiều dài là 0. Các số cùng dòng cách nhau qua dấu cách. Dạng dữ liệu cho như vậy được gọi là ma trận kề của đồ thị.
Thí dụ sau đây cho biết đồ thị có bảy đỉnh, cần tìm các đường đi ngắn nhất từ đỉnh 2 tới các đỉnh còn lại của đồ thị. Cung (2, 1) có chiều dài 4,...
DIJ.INP 7 2 0 0 0 0 0 0 0 4 0 1 0 0 0 5 0 0 0 0 0 0 1 0 0 0 0 0 2 0 0 0 0 3 0 0 0 1 0 0 0 0 0 5 0 0 0 1 0 0 0
Dữ liệu ra được ghi trong tệp văn bản DIJ.OUT gồm n dòng. Thông tin về mỗi đường đi ngắn nhất từ đỉnh s đến các đỉnh còn lại được ghi trên 1 dòng. Số đầu tiên của dòng là chiều dài đường đi. Nếu không tồn tại đường đi thì ghi giá trị 0. Tiếp đến, trong trường hợp có đường đi từ đỉnh s đến đỉnh i thì ghi dãy đỉnh xuất hiện lần lượt trên đường đi, đỉnh đầu tiên, dĩ nhiên là s, đỉnh cuối cùng là i. Đường đi từ đỉnh i tới chính đỉnh đó được coi là không tồn tại, i = 1..n. Thí dụ trên cho ta kết quả
DIJ.OUT 2 1 6 7 3 4 5 1 5 1 3 2 1 5 1 4
30
- Đường ngắn nhất từ đỉnh 2 đến đỉnh 1 có chiều dài 4, cách đi: 2
1.
- Đường ngắn nhất từ đỉnh 2 đến đỉnh 2: không có (thực ra, theo lẽ thường là có đường chiều dài 0).
- Đường ngắn nhất từ đỉnh 2 đến đỉnh 3 có chiều dài 1, cách đi: 2
3.
- Đường ngắn nhất từ đỉnh 2 đến đỉnh 4 có chiều dài 3, cách đi: 2 3
7 4.
- Đường ngắn nhất từ đỉnh 2 đến đỉnh 5: không có.
- Đường ngắn nhất từ đỉnh 2 đến đỉnh 6 có chiều dài 5, cách đi: 23746. - Đường ngắn nhất từ đỉnh 2 đến đỉnh 7 có chiều dài 2, cách đi: 237. 6.2.23.2.Thuật giải
Thuật giải quy hoạch động được trình bày dưới đây mang tên Dijkstra, một nhà tin học lỗi lạc người Hà Lan. Bản chất của thuật toán là sửa đỉnh, chính xác ra là sửa trọng số của mỗi đỉnh.
Theo sơ đồ giải các bài toán quy hoạch động trước hết ta xây dựng hệ thức cho bài toán.
Gọi p(i) là độ dài đường ngắn nhất từ đỉnh s đến đỉnh i, 1 i n. Ta thấy, hàm p(i) phải thoả các tính chất sau:
- p(s) = 0: đường ngắn nhất từ đỉnh xuất phát s đến chính đỉnh đó có chiều dài 0.
- Với i s, muốn đến được đỉnh i ta phải đến được một trong các đỉnh sát trước đỉnh i. Nếu j là một đỉnh sát trước đỉnh i, theo điều kiện của đầu bài ta phải có
a[j,i ] > 0
Trong đó a[j, i] chính là chiều dài cung (j i).
Trong số các đỉnh j sát trước đỉnh i ta cần chọn đỉnh nào?
Kí hiệu path(x, y) là đường đi ngắn nhất qua các đỉnh, xuất phát từ đỉnh từ x và kết thúc tại đỉnh y
x. Khi đó đường từ s đến i sẽ được chia làm hai đoạn, đường từ s đến j và cung (j i):
path(s,i) = path(s,j)+ path(j,i)
trong đó path(j, i) chỉ gồm một cung:
path(j,i) = (j i) 4 2 1 0 1 2 3 3 2 3 7 4 0 5 2 3 7 4 6 2 2 3 7
31
Do p(i) và p(j) phải là ngắn nhất, tức là phải đạt các trị min, ta suy ra điều kiện để chọn đỉnh j sát trước đỉnh i là tổng chiều dài đường từ s đến j và chiều dài cung (j i) là ngắn nhất. Ta thu được hệ thức sau:
p(i) = min {p(j)+a[j,i ] | a[j,i ] > 0, j = 1..n }
Để ý rằng điều kiện a[j, i] > 0 cho biết j là đỉnh sát trước đỉnh i.
Điều tài tình là Dijkstra đã cung cấp thuật toán tính đồng thời mọi đường đi ngắn nhất từ đỉnh s đến các đỉnh còn lại của đồ thị. Thuật toán đó như sau.
Thuật toán thực hiện n lần lặp, mỗi lần lặp ta chọn và xử lí 1 đỉnh của đồ thị. Tại lần lặp thứ k ta khảo sát phần của đồ thị gồm k đỉnh với các cung liên quan đến k đỉnh được chọn trong phần đồ thị đó. Ta gọi phần này là đồ thị con thu được tại bước xử lý thứ k của đồ thị ban đầu và kí hiệu là G(k). Với đồ thị này ta hoàn tất bài giải tìm mọi đường đi ngắn nhất từ đỉnh xuất phát s đến mọi đỉnh còn lại của G(k). Chiều dài thu được ta gán cho mỗi đỉnh i như một trọng số p[i]. Ngoài ra, để chuẩn bị cho bước tiếp theo ta đánh giá lại trọng số cho mọi đỉnh kề sau của các đỉnh trong G(k).
Khởi trị: Gán trọng số p[i] = cho mọi đỉnh, trừ đỉnh xuất phát s, gán trị p[s] = 0.
Ý nghĩa của thao tác này là khi mới đứng ở đỉnh xuất phát s của đồ thị con G(0), ta coi như chưa thăm mảnh nào của đồ thị nên ta chưa có thông tin về đường đi từ s đến các đỉnh còn lại của đồ thị ban đầu. Nói cách khác ta coi như chưa có đường đi từ s đến các đỉnh khác s và do đó, độ dài đường đi từ s đến các đỉnh đó là .
Giá trị được chọn trong chương trình là:
MAXWORD = 65535.
Tại bước lặp thứ k ta thực hiện các thao tác sau:
- Trong số các đỉnh chưa xử lí, tìm đỉnh i có trọng số min.
- Với mỗi đỉnh j chưa xử lí và kề sau với đỉnh i, ta chỉnh lại trọng số p[j] của đỉnh đó theo tiêu chuẩn sau:
Nếu p[i] + a[i, j] < p[j] thì gán cho p[j] giá trị mới:
p[j]=p[i]+a[i,j]
Ý nghĩa của thao tác này là: nếu độ dài đường đi path(s, j) trong đồ thị con G(k - 1) không qua đỉnh i mà lớn hơn độ dài đường đi mới path(s, j) có qua đỉnh i thì cập nhật lại theo đường mới đó.
32
- Sau khi cập nhật ta cần lưu lại vết cập nhật đó bằng lệnh gán before[i] = j với ý nghĩa là, đường ngắn nhất từ đỉnh s tới đỉnh j cần đi qua đỉnh i.
- Đánh dấu đỉnh i là đã xử lí.
Như vậy, tại mỗi bước lặp ta chỉ xử lí đúng một đỉnh i có trọng số min và đánh dấu duy nhất đỉnh đó.
(*--- Thuat toan Dijkstra ---*) Void Dijkstra() { int i,k,j,n; for(k=0; k<n; k++) {
i := Min; { tim dinh i co trong so p[i] -> min } d[i] := 1; {danh dau dinh i la da xu li } for(i=0; i<n; i++)
if (d[j] = 0 ) {dinh chua tham } if (a[i,j] > 0 ) {co duong di i -> j }
if ([i] + a[i,j] < p[j] ) { // sua dinh p[j] := p[i] + a[i,j]; before[j] := i; } }
33
}
Thuật toán chứa hai vòng for lồng nhau do đó có độ phức tạp là n2.
Sau khi hoàn thành thuật toán Dijkstra ta cần gọi thủ tục Ket (kết) để ghi lại kết quả theo yêu cầu của đầu bài như sau.
Với mỗi đỉnh i = 1..n ta cần ghi vào tệp output chiều dài đường đi từ s đến i bao gồm giá trị p[i] và các đỉnh nằm trên đường đó.
Chú ý rằng nếu p[i] nhận giá trị khởi đầu tức là MAXWORD = 65535 thì tức là không có đường đi từ s đến i. 6.2.3 3.3. Chương trình minh họa using System; using System.IO; using System.Collections; namespace SangTaoT1 { /*--- * Thuat toan Dijkstra
* Tim moi duong ngan nhat tu mot dinh * den moi dinh con lai
* ---*/ class Dijkstra
{
const string fn = "Dij.inp"; const string gn = "Dij.out";
34
static int n = 0; // so dinh
static int s = 0; // dinh xuat phat // c[i,j] ma tran ke cho biet
// do dai cung (i,j) static int[,] c;
static int[] d; // danh dau dinh static int[] t; // tro truoc static int[] p; // trong so dinh static void Main()
{
Run();
Console.ReadLine(); } // Main
static void Run() {
Doc(); Show(); Dij(); Ghi(); Test();
Console.WriteLine("\n Fini"); Console.ReadLine();
}
// Kiem tra lai tep output static void Test() tự viết
35
static void Ghi() {
StreamWriter g = File.CreateText(gn); for (int i = 1; i <= n; ++i)
if (i == s || p[i] == int.MaxValue) g.WriteLine(0); else { g.Write(p[i] + " "); int u = InvPath(i); for (int j = u; j > 0; --j) g.Write(d[j] + " "); g.WriteLine(); } g.Close(); }
// Lan nguoc duong di // tu dinh v den dinh s // ghi tam vao mang d static int InvPath(int v) {
36 do { d[++i] = v; v = t[v]; } while (v != 0); return i; }
static void Dij() {
for (int i = 0; i <= n; ++i) { //d: danh dau dinh, t: tro truoc d[i] = t[i] = 0;
p[i] = int.MaxValue; // Trong so }
p[s] = 0;// s: dinh xuat phat for (int i = 1; i < n; ++i) {
int u = Minp();// u: dinh trong so min d[u] = 1; // danh dau dinh da xet // sua lai nhan dinh
for (int v = 1; v <= n; ++v) if (d[v] == 0) // dinh chua xet
37
if (c[u, v] > 0) // co cung(u,v) if (p[u] + c[u, v] < p[v]) { // sua lai nhan dinh v p[v] = p[u] + c[u, v]; // chon cach di tu u -> v t[v] = u; } } }
// Tim trong so cac dinh chua // xu li mot dinh j co p min static int Minp()
{
int jmin = 0;
for (int i = 1; i <= n; ++i) if (d[i] == 0) // dinh i chua xet if (p[i] < p[jmin]) jmin = i; return jmin;
}
static void Doc() {
38
ReadAllText(fn)).Split( new char[] {'\0','\n','\t','\r',' '},
StringSplitOptions.RemoveEmptyEntries), new Converter<String, int>(int.Parse)); n = a[0]; // so dinh
s = a[1]; // dinh xuat phat c = new int[n + 1, n + 1]; d = new int[n + 1]; t = new int[n + 1]; p = new int[n + 1]; int k = 2;
for (int i = 1; i <= n; ++i) for (int j = 1; j <= n; ++j) c[i, j] = a[k++]; }
// Hien thi du lieu static void Show() tự viết // hien thi mang
static void Print(int[] a, int n) tự viết }
39
CHƯƠNG 7: Phần V: KẾT LUẬN
Trên đây là 3 phương pháp giải toán tin học rất phổ biến, rất hay và được ứng dụng rất rộng rãi trong nhiều bài toán khác nhau. Tùy theo yêu cầu và tính chất mỗi bài toán ta sẽ có sự lựa chọn thích hợp.
Đây chỉ là 3 phương pháp trong rất nhiều phương pháp giải toán trong tin học và đôi khi chúng ta phải kết hợp các phương pháp lại với nhau để có thể tìm lời giải hoặc tối ưu lời giải.
Hi vọng bài luận này có thể giúp ích một phần nào đó cho các bạn trên con đường khám phá và chinh phục tri thức. Vì khả năng bản thân có hạn nên bài này còn đơn giản, chưa sâu sắc hay chưa thỏa mãn hết nhu cầu người đọc.Trong bài viết chắc chắn khó tránh khỏi sai sót, mọi ý kiến mong quí Thầy cô và các bạn đóng góp về địa chỉ: tronglt88@gmail.com để bài viết ngày càng tốt hơn.
Chân thành cảm ơn quí Thầy cô và các bạn!
CHƯƠNG 8: Phần VI: TÀI LIỆU THAM KHẢO
[1] Phương pháp nghiên cứu khoa học trong tin học,GS.TSKH. Hoàng Kiếm. [2] Giáo trình thuật toán thuật giải, GS.TSKH. Hoàng Kiếm.
[3] Thiết kế và đánh giá thuật giải, Trần Tuấn Minh.
[4] Bách khoa toàn thư mở Wikipedia bàn về các thuật toán. [5] Introduction to Algorithms_ 2nd Ed - Thomas H. Cormen