Thuật toán Johnson cho đồ thị thưa

<p style="text-align: justify;">Thuật to&aacute;n Johnson l&agrave; một thuật to&aacute;n t&igrave;m đường đi ngắn nhất, được sử dụng để giải quyết tất b&agrave;i to&aacute;n về việc t&igrave;m c&aacute;c cặp cạnh c&oacute; đường đi ngắn nhất. B&agrave;i to&aacute;n t&igrave;m tất cả c&aacute;c cặp đường đi ngắn nhất nhận đầu v&agrave;o l&agrave; một đồ thị c&oacute; c&aacute;c đỉnh v&agrave; c&aacute;c cạnh, v&agrave; n&oacute; đưa ra đường đi ngắn nhất giữa mọi cặp đỉnh trong đồ thị đ&oacute;. Thuật to&aacute;n Johnson rất giống với thuật to&aacute;n Floyd-Warshall, tuy nhi&ecirc;n, Floyd-Warshall hiệu quả nhất đối với đồ thị đầy (nhiều cặp cạnh), trong khi thuật to&aacute;n Johnson hiệu quả nhất đối với đồ thị thưa (&iacute;t c&aacute;c cạnh).</p> <h2 id="ftoc-heading-1" class="ftwp-heading" style="text-align: justify;">1. Kh&aacute;i niệm</h2> <p style="text-align: justify;">L&yacute; do m&agrave; thuật to&aacute;n Johnson tốt hơn cho đồ thị thưa l&agrave; độ phức tạp về thời gian của n&oacute; phụ thuộc v&agrave;o số cạnh trong đồ thị, trong khi của Floyd-Warshall th&igrave; kh&ocirc;ng. Thuật to&aacute;n của Johnson chạy trong khoảng thời gian l&agrave; $O(V^2.log(V) + |V|.|E|)$. V&igrave; vậy, nếu số lượng cạnh nhỏ (tức l&agrave; đồ thị thưa), n&oacute; sẽ chạy nhanh hơn $O(V^3)$&nbsp;so với thuật to&aacute;n Floyd-Warshall.</p> <p style="text-align: justify;">Thuật to&aacute;n Johnson sử dụng thuật to&aacute;n Bellman-Ford l&agrave;m thuật to&aacute;n con để đ&aacute;nh lại trọng số cho đồ thị đầu v&agrave;o nhằm loại bỏ c&aacute;c cạnh c&oacute; trọng số &acirc;m v&agrave; ph&aacute;t hiện c&aacute;c chu kỳ v&ograve;ng lặp &acirc;m. Với đồ thị mới đ&atilde; được thay đổi n&agrave;y, n&oacute; sẽ sử dụng thuật to&aacute;n đường đi ngắn nhất của Dijkstra để t&iacute;nh đường đi ngắn nhất giữa tất cả c&aacute;c cặp đỉnh. Đầu ra của thuật to&aacute;n sau đ&oacute; l&agrave; tập hợp c&aacute;c đường đi ngắn nhất trong đồ thị ban đầu.</p> <p style="text-align: justify;">Thuật to&aacute;n của Johnson hoạt động hiệu quả cũng như nhiều thuật t&igrave;m to&aacute;n đường đi ngắn nhất cho đồ thị đầu v&agrave;o, G. Đồ thị n&agrave;y c&oacute; một tập c&aacute;c đỉnh, V, &aacute;nh xạ tới một tập c&aacute;c cạnh, E. Mỗi cạnh, $(u, v)$, c&oacute; một h&agrave;m trọng số $w = \text{distance(u, v)}$. Thuật to&aacute;n của Johnson hoạt động tr&ecirc;n đồ thị c&oacute; hướng, c&oacute; trọng số. N&oacute; cho ph&eacute;p c&aacute;c cạnh c&oacute; trọng số &acirc;m, nhưng kh&ocirc;ng thể c&oacute; chu kỳ trọng số &acirc;m (v&igrave; khi đ&oacute; sẽ kh&ocirc;ng tồn tại đường đi ngắn nhất cho c&aacute;c đỉnh bởi chu kỳ đ&oacute;).</p> <h2 id="ftoc-heading-2" class="ftwp-heading" style="text-align: justify;">2. C&aacute;ch thức hoạt động của thuật to&aacute;n Johnson</h2> <p style="text-align: justify;">Thuật to&aacute;n Johnson bao gồm ba bước sau:</p> <ol style="text-align: justify;"> <li>Một đỉnh mới được th&ecirc;m v&agrave;o đồ thị v&agrave; n&oacute; được nối bằng c&aacute;c cạnh c&oacute; trọng số bằng kh&ocirc;ng với tất cả c&aacute;c đỉnh kh&aacute;c trong đồ thị.</li> <li>Tất cả c&aacute;c cạnh đều đi qua qu&aacute; tr&igrave;nh đ&aacute;nh lại trọng số để loại bỏ c&aacute;c cạnh trọng số &acirc;m.</li> <li>Đỉnh được th&ecirc;m v&agrave;o từ bước 1 bị loại bỏ v&agrave; thuật to&aacute;n Dijkstra được chạy tr&ecirc;n mọi n&uacute;t trong biểu đồ.</li> </ol> <p style="text-align: justify;">Ba bước n&agrave;y c&oacute; thể được biểu diễn trong h&igrave;nh b&ecirc;n dưới. H&igrave;nh (a) cho thấy bước 1. H&igrave;nh (b) cho thấy bước 2. H&igrave;nh (c) &ndash; (g) cho thấy bước 3, thuật to&aacute;n Dijkstra đang được chạy tr&ecirc;n mỗi đỉnh trong số 5 đỉnh của đồ thị.</p> <p><img style="width: 100%;" src="../../../public_files/e206ea8e-7b06-48cf-9516-993ed78b5fde" alt="thuat-toan-johnson" /></p> <p style="text-align: justify;">Đoạn giả m&atilde; của giải thuật được triển khai như sau:</p> <ul style="text-align: justify;"> <li>H&agrave;m Johnson(G) <ul> <li>1.</li> <li>Tạo G` trong đ&oacute; G`.V = G.V &cup; {s}, <ul> <li>G`.E = G.E &cup; ((s, u) với u trong G.V, v&agrave;</li> <li>Trọng số(s, u) = 0 với u trong G.V</li> </ul> </li> <li>2.</li> <li>Nếu Bellman-Ford(s) == false <ul> <li>Trả về v&agrave; đưa ra th&ocirc;ng b&aacute;o</li> </ul> </li> <li>Nếu kh&ocirc;ng: <ul> <li>Với mỗi đỉnh v trong G`.V: <ul> <li>h(v) = Khoảng c&aacute;ch(s, v) được t&iacute;nh bởi thuật to&aacute;n Bellman-Ford</li> </ul> </li> <li>Với mỗi cạnh (u, v) trong G`.E: <ul> <li>Trọng số`(u, v) = Trọng số(u, v) + h(u) &ndash; h(v)</li> </ul> </li> </ul> </li> </ul> </li> <li>3. <ul> <li>D = ma trận mới bao gồm c&aacute;c khoảng c&aacute;ch được khởi tạo với gi&aacute; trị v&ocirc; cực</li> <li>Với mỗi đỉnh u trong G.V: <ul> <li>Thực hiện Dijkstra(G, Trọng số`, u) để t&igrave;m khoảng c&aacute;ch`(u, v) cho tất cả c&aacute;c v trong G.v</li> <li>Với mỗi đỉnh v trong G.V: <ul> <li>D_(u, v) = khoảng c&aacute;ch`(u, v) + h(v) &ndash; h(u)</li> </ul> </li> </ul> </li> <li>Trả về D</li> </ul> </li> </ul> <p style="text-align: justify;">Cụ thể về c&aacute;c bước của thuật to&aacute;n được m&ocirc; tả như sau:</p> <ul> <li style="text-align: justify;">Bước 1: Th&ecirc;m một đỉnh cơ sở <ul> <li style="text-align: justify;">Phần đầu ti&ecirc;n của thuật to&aacute;n kh&aacute; đơn giản. Th&ecirc;m một đỉnh mới, s, v&agrave;o đồ thị v&agrave; kết nối n&oacute; với tất cả c&aacute;c đỉnh kh&aacute;c c&oacute; cạnh trọng số bằng 0.</li> </ul> </li> </ul> <p><img style="width: 100%;" src="../../../public_files/5956380a-44eb-4656-abf4-7a5e278e3e36" alt="thuat-toan-johnson-2" /></p> <ul> <li style="text-align: justify;">Bước 2: Đ&aacute;nh lại trọng số cho c&aacute;c cạnh <ul> <li>Đ&aacute;nh lại trọng số c&oacute; lẽ l&agrave; phần mới lạ nhất của thuật to&aacute;n n&agrave;y. Kh&aacute;i niệm cơ bản l&agrave;: nếu tất cả c&aacute;c cạnh trong đồ thị đều kh&ocirc;ng &acirc;m, th&igrave; việc thực hiện thuật to&aacute;n Dijkstra tr&ecirc;n mọi đỉnh l&agrave; c&aacute;ch nhanh nhất để giải b&agrave;i to&aacute;n đường đi ngắn nhất của tất cả c&aacute;c cặp. Tuy nhi&ecirc;n, nếu một số cạnh c&oacute; trọng số &acirc;m, ch&uacute;ng ta c&oacute; thể đ&aacute;nh trọng số lại cho ch&uacute;ng để giữ nguy&ecirc;n định nghĩa sau.</li> <li>Đ&aacute;nh lại trọng số l&agrave; một qu&aacute; tr&igrave;nh m&agrave; trọng số của cạnh được thay đổi để thỏa m&atilde;n hai thuộc t&iacute;nh sau:</li> <li>Đối với tất cả c&aacute;c cặp đỉnh u, v trong đồ thị, nếu một đường đi n&agrave;o đ&oacute; l&agrave; đường đi ngắn nhất giữa c&aacute;c đỉnh đ&oacute; trước khi đ&aacute;nh lại trọng số th&igrave; n&oacute; cũng phải l&agrave; đường đi ngắn nhất giữa c&aacute;c đỉnh đ&oacute; sau đ&aacute;nh lại trọng số.</li> <li>Đối với tất cả c&aacute;c cạnh, (u, v), trong đồ thị, trọng số (u, v) phải kh&ocirc;ng &acirc;m.</li> <li>Bước n&agrave;y sử dụng thuật to&aacute;n Bellman-Ford để gi&uacute;p đ&aacute;nh lại trọng số cho c&aacute;c cạnh. Bằng c&aacute;ch sử dụng Bellman-Ford trong bước n&agrave;y, thuật to&aacute;n của Johnson cũng c&oacute; thể ph&aacute;t hiện c&aacute;c chu kỳ trọng lượng &acirc;m, một thuộc t&iacute;nh của Bellman-Ford.</li> </ul> </li> <li style="text-align: justify;">Bước 3: T&igrave;m tất cả c&aacute;c cặp đường đi ngắn nhất <ul> <li style="text-align: justify;">Cuối c&ugrave;ng, thuật to&aacute;n Dijkstra được chạy tr&ecirc;n tất cả c&aacute;c đỉnh để t&igrave;m đường đi ngắn nhất. Điều n&agrave;y c&oacute; thể thực hiện được v&igrave; c&aacute;c trọng số đ&atilde; được chuyển đổi th&agrave;nh c&aacute;c trọng số kh&ocirc;ng &acirc;m. Tuy nhi&ecirc;n, điều quan trọng l&agrave; phải chuyển đổi c&aacute;c trọng số đường đi n&agrave;y trở lại th&agrave;nh c&aacute;c trọng số đường đi ban đầu để trọng số đường đi ch&iacute;nh x&aacute;c được trả về khi kết th&uacute;c thuật to&aacute;n. Điều n&agrave;y được thực hiện ở cuối thuật to&aacute;n bằng c&aacute;ch đảo ngược qu&aacute; tr&igrave;nh đ&aacute;nh lại trọng số một c&aacute;ch đơn giản. Th&ocirc;ng thường, một cấu tr&uacute;c dữ liệu như một ma trận được trả về. Tại mọi &ocirc; i, j của ma trận l&agrave; đường đi ngắn nhất từ đỉnh i đến đỉnh j.</li> </ul> </li> </ul> <h2 id="ftoc-heading-3" class="ftwp-heading" style="text-align: justify;">3. Độ phức tạp của thuật to&aacute;n</h2> <p style="text-align: justify;">Bước đầu ti&ecirc;n của thuật to&aacute;n chạy trong thời gian $O(V)$ v&igrave; n&oacute; cần tạo một cạnh mới cho tất cả c&aacute;c đỉnh trong đồ thị. Bước thứ hai của thuật to&aacute;n, Bellman-Ford, chạy trong thời gian $O(V E)$, Bellman-Ford cũng vậy. Qu&aacute; tr&igrave;nh x&aacute;c định lại trọng số của đồ thị chạy trong c&ugrave;ng một thời gian tương tự.</p> <p style="text-align: justify;">Bước cuối c&ugrave;ng của thuật to&aacute;n l&agrave; thuật to&aacute;n Dijkstra chạy tr&ecirc;n tất cả c&aacute;c đỉnh V. Sử dụng một đống Fibonacci, mỗi lần lặp lại n&agrave;y mất $O(Vlog(V) + E)$ thời gian để ho&agrave;n th&agrave;nh với tổng độ phức tạp l&agrave; $O(V^2log(V) + VE)$</p> <p style="text-align: justify;">Tr&ecirc;n đ&acirc;y l&agrave; kh&aacute;i niệm v&agrave; v&iacute; dụ cơ bản về thuật to&aacute;n Johnson. Hy vọng mọi người c&oacute; thể &aacute;p dụng v&agrave;o trong chương tr&igrave;nh của m&igrave;nh. Nếu mọi người c&oacute; đ&oacute;ng g&oacute;p g&igrave; th&ecirc;m th&igrave; c&oacute; thể để c&aacute;c b&igrave;nh luận b&ecirc;n dưới b&agrave;i viết n&agrave;y. Mọi người h&atilde;y tiếp tục theo d&otilde;i c&aacute;c b&agrave;i tiếp theo v&agrave; cập nhật c&aacute;c b&agrave;i mới nhất tr&ecirc;n&nbsp;<a href="../../../">tek4</a>&nbsp;nh&eacute;!</p>