Bài tập lập trình tìm đường đi ngắn nháyt năm 2024
Was this document helpful? Show
Was this document helpful? 3 2 9 6 Bài Tập Tìm đường đi ngắn nhất từ đỉnh S đến đỉnh Z bằng thuật toán Dijkstra Bảng Kết Quả k Đỉnh S Đỉnh 1 Đỉnh 2 Đỉnh 3 Đỉnh 4 Đỉnh Z k là số thứ tự, bắt đầu từ 0 Mỗi đỉnh là tiêu đề mỗi cột, thứ tự không quan trọng. Bảng Kết Quả k Đỉnh S Đỉnh 1 Đỉnh 2 Đỉnh 3 Đỉnh 4 Đỉnh Z 0 (0, S)* ( , S) Ở bước khởi tạo, ứng với k = 0, Đỉnh xuất phát có độ dài đường đi từ đỉnh S đến S là 0, nên ta viết (0, S). Đỉnh 1 có độ dài đường đi ngắn nhất từ đỉnh 5 đến 1 là , S) . Đánh dấu * vào đỉnh có đường đi từ đỉnh S đến nó là ngắn nhất. Bảng Kết Quả k Đỉnh S Đỉnh 1 Đỉnh 2 Đỉnh 3 Đỉnh 4 Đỉnh Z 0 (0, S)* ( , S) 1 - (2, S)* (9, S) (7, S) (13, S) ( , S) Tại k = 1, tỉnh lại độ dài đường đi ngắn nhất từ S đến các đỉnh kề với đỉnh S L(1) = L(s) + w(s, 1)= 0 + 2 = 2 ⇒ (2, S)
Đây là bài viết số 2 trong series bài viết về Bài toán đường đi ngắn nhất trên đồ thị. Để theo dõi lại phần 1 của series, các bạn hãy nhấn vào đây. I. Tổng quan1. Phát biểu bài toánTa đã biết về thuật toán Floyd - Warshall trong bài toán tìm đường đi ngắn nhất giữa mọi cặp đỉnh trước đây. Thuật toán hoạt động rất hiệu quả trong các bài toán cần xác định nhanh đường đi ngắn nhất giữa hai đỉnh bất kì, tuy nhiên nhược điểm của nó là độ phức tạp khá lớn lên tới O(n3),O(n^3), vì vậy chỉ có thể sử dụng trên các đồ thị có số đỉnh vừa phải. Trên thực tế, không phải lúc nào ta cũng cần tìm ra đường đi ngắn nhất giữa mọi cặp đỉnh. Bài toán đường đi ngắn nhất xuất phát từ một đỉnh (single-source shortest path) là một bài toán rất thông dụng như vậy, bài toán của giải thuật Floyd chỉ là dạng biến đổi của nó mà thôi. Bài toán này được phát biểu như sau: Cho đơn đồ thị có hướng, có trọng số G=(V,E,w),G = (V, E, w), các đỉnh được đánh số từ 11 tới nn. Độ dài của đường đi ngắn nhất giữa hai đỉnh ss và tt được kí hiệu là d(s,t),d(s, t), và nếu như không tồn tại đường đi từ đỉnh ss tới một đỉnh tt nào đó thì ta sẽ coi d(s,t)=∞d(s, t) = \infty. Bài toán trên thực tế sẽ phải quy về việc tìm đường đi ngắn nhất từ đỉnh ss tới tất cả các đỉnh còn lại. Trong bài viết này, tôi sẽ giới thiệu tới các bạn một số thuật toán thông dụng để giải bài toán này, đó là Dijkstra và Ford Bellman. Ta quy ước các giải thuật sẽ được cài đặt trên đơn đồ thị có hướng. Trong trường hợp đồ thị là vô hướng ta vẫn quy được về đồ thị có hướng bằng cách coi mỗi cạnh là hai cung ngược chiều nhau. 2. Cấu trúc bài toán con tối ưuCác thuật toán tìm đường đi ngắn nhất mà chúng ta sẽ khảo sát trong bài viết này đều dựa trên một đặc tính chung: Mỗi đoạn đường trên đường đi ngắn nhất phải là một đường đi ngắn nhất. Ta có định lý sau: Định lý 1.1: Cho đồ thị có trọng số G=(V,E,w),G = (V, E, w), gọi P={v1,v2,…,vk}P = \{v_1, v_2, \dots, v_k\} là một đường đi ngắn nhất từ v1v_1 tới vkv_k. Khi đó ∀i,j:1≤i≤j≤k,\forall i, j: 1 \le i \le j \le k, đoạn đường Pij={vi,vi+1,…,vj}P_{ij} = \{v_i, v_{i + 1}, \dots, v_j\} là một đường đi ngắn nhất từ viv_i tới vjv_j. Bởi tính chất bài toán con tối ưu nói trên, hầu hết các thuật toán tìm đường đi ngắn nhất đều là thuật toán Quy hoạch động (ví dụ như Floyd) hoặc Tham lam (ví dụ như Dijkstra). Nếu như đồ thị có chu trình âm (chu trình với độ dài âm) thì khoảng cách giữa một số cặp đỉnh nào đó có thể không xác định, bởi vì bằng cách đi vòng theo chu trình này một số lần đủ lớn, ta có thể chỉ ra đường đi giữa hai đỉnh nào đó trong chu trình này nhỏ hơn bất kỳ một số cho trước nào. Trong trường hợp như vậy, bài toán có thể quy về tìm đường đi đơn ngắn nhất, tuy nhiên hiện nay vẫn chưa ai tìm được một thuật toán tìm đường đi đơn ngắn nhất trên đồ thị có chu trình âm. Vì vậy, trong phạm vi bài viết này, ta sẽ xét các thuật toán tìm đường đi ngắn nhất trên đồ thị không có chu trình âm. II. Thuật toán Ford Bellman - Đồ thị không có chu trình âm1. Ý tưởngTrên đơn đồ thị có hướng không có chu trình âm, thuật toán Ford Bellman có ý tưởng rất đơn giản như sau: Với đỉnh xuất phát s,s, gọi d[v]d[v] là độ dài đường đi ngắn nhất từ ss tới vv. Ban đầu ta khởi tạo:
Các d[v]d[v] sẽ lần lượt được tối ưu hóa như sau: Xét mọi cặp đỉnh u,vu, v của đồ thị, nếu như có một cặp đỉnh mà d[v]>d[u]+wu,vd[v] > d[u] + w_{u, v} và d[u]≠+∞d[u] \ne +\infty thì ta gán lại: d[v]=d[u]+wu,vd[v] = d[u] + w_{u, v} Tức là, nếu độ dài đường đi từ ss tới vv hiện tại bị lớn hơn độ dài đường đi từ ss tới uu cộng với trọng số đi từ uu tới vv thì ta sẽ hủy bỏ đường đi cũ và coi đường đi mới từ ss tới vv chính là đường đi từ ss tới uu rồi từ uu tới vv. Thuật toán sẽ kết thúc khi không thể tối ưu thêm bất kì một giá trị d[v]d[v] nào nữa.
Chứng minh: Tại bước khởi tạo, mỗi d[v]d[v] chính là độ dài ngắn nhất của đường đi từ ss tới vv đi qua không quá 00 cạnh. Muốn truy vết lại đường đi ngắn nhất, ta sử dụng mảng trace[v]trace[v] để lưu lại đỉnh liền trước vv trong đường đi ngắn nhất từ ss tới vv. Mỗi khi cập nhật lại một d[v]=d[u]+wu,vd[v] = d[u] + w_{u, v} thì đồng thời cập nhật lại đỉnh liền trước vv trên đường đi tối ưu sẽ là u,u, và gán luôn: trace[v]=utrace[v] = u Thuật toán cũng có thể được cải tiến để xác định được đồ thị có tồn tại chu trình âm hay không theo ý tưởng sau:
2. Cài đặtDanh sách cạnh là phương pháp tốt nhất để biểu diễn đồ thị trong thuật toán Ford Bellman. Giá trị +∞+\infty sẽ được gán bằng một hằng số thật lớn trong khi lập trình, ở đây tác giả lựa chọn giá trị 101810^{18}.
Trong trường hợp đề bài yêu cầu tìm ra một chu trình âm gặp phải trên đường đi từ s,s, ta có thể thực hiện thêm một số bước dưới đây:
Đoạn chương trình kiểm tra chu trình âm có thể điều chỉnh như sau để thu được các đỉnh thuộc chu trình âm:
3. Đánh giáVề mặt bộ nhớ, do sử dụng danh sách cạnh để biểu diễn đồ thị nên độ phức tạp bộ nhớ của thuật toán là O(∣E∣)O\big(|E|\big). Về mặt thời gian, dễ thấy thuật toán lặp không quá n−1n - 1 lần, và mỗi lần phải lặp qua cả mm cạnh để tối ưu các d[v]d[v]. Vì thế độ phức tạp tính toán tổng quát là O(∣V∣.∣E∣)O\big(|V|.|E|\big). III. Thuật toán Dijkstra - Đồ thị có trọng số trên các cung không âm1. Giải thuật cơ bảnÝ tưởngTrong trường hợp đồ thị có trọng số trên các cung không âm, thuật toán Dijkstra là một thuật toán hoạt động hiệu quả hơn rất nhiều so với Ford Bellman. Thuật toán Ford Bellman mặc dù giải quyết được bài toán trong trường hợp tổng quát, nhưng tồn đọng sự bất tiện sau đây: Với mỗi đỉnh v,v, đặt d[v]d[v] là độ dài đường đi ngắn nhất từ ss tới vv. Thuật toán Ford Bellman khởi tạo d[s]=0d[s] = 0 và d[v]=+∞;∀v:v≠s;d[v] = +\infty; \forall v: v \ne s; rồi tối ưu hóa dần các giá trị d[v]d[v] theo công thức: d[v]=min(d[v],d[u]+wu,v)d[v] = \text{min}\big(d[v], d[u] + w_{u, v}\big). Như vậy, nếu ta dùng một đỉnh uu để tối ưu hóa đỉnh v,v, và sau đó ở một bước nào đấy d[u]d[u] lại được tối ưu thêm, thì mọi d[v]d[v] liên quan tới d[u]d[u] ở bước trước đó đều sẽ phải sửa lại, dẫn tới việc sửa đi sửa lại nhiều lần. Thuật toán Dijkstra có ý tưởng là thay vì duyệt lại mọi cung (u→v)(u \to v) để tối ưu hóa d[v],d[v], ta sẽ chọn ngay một đỉnh uu mà d[u]d[u] không thể tối ưu thêm nữa rồi dùng nó để tối ưu hóa các đỉnh vv kề với nó. Đỉnh uu được chọn này sẽ là đỉnh uu có d[u]d[u] nhỏ nhất hiện tại. Như vậy, thuật toán có thể mô tả như sau:
Tại sao ở mỗi bước lặp, đỉnh uu với d[u]d[u] nhỏ nhất lại được chọn để cố định và tối ưu các đỉnh xung quanh nó? Ta sử dụng phương pháp phản chứng: Giả sử đỉnh uu như vậy vẫn còn có thể tối ưu thêm, thì phải tồn tại một đỉnh u1u_1 nào đó với is_free[u1]=TRUE\text{is\_free}[u_1] = \text{TRUE} sao cho d[u]>d[u1]+wu1,ud[u] > d[u_1] + w_{u_1, u}. Do trọng số wu1,uw_{u_1, u} không âm nên chắc chắn d[u1]>d[u],d[u_1] > d[u], điều này trái với cách chọn d[u]d[u] là nhỏ nhất lúc đầu. Vậy đỉnh uu với d[u]d[u] nhỏ nhất tất nhiên phải là đỉnh không thể tối ưu được thêm nữa.Việc truy vết có thể thực hiện tương tự như trong giải thuật Ford Bellman, với trace[v]trace[v] là đỉnh liền trước vv trên đường đi ngắn nhất từ ss tới vv. Mỗi khi cập nhật một giá trị d[v]=d[u]+wu,vd[v] = d[u] + w_{u, v} thì đồng thời cập nhật luôn trace[v]=utrace[v] = u. Cài đặt
Đánh giáĐộ phức tạp bộ nhớ khi sử dụng danh sách kề để biểu diễn đồ thị sẽ là O(n+m)O(n + m). Trong trường hợp xấu nhất, thuật toán đơn giản này sẽ cần nn lần cố định các d[v]d[v] và mất thêm O(n)O(n) để tìm đỉnh ra đỉnh uu có d[u]d[u] nhỏ nhất. Vì thế thuật toán có độ phức tạp tính toán tổng quát là là O(n2)O(n^2). 2. Cải tiến với cấu trúc HeapÝ tưởngVới những đồ thị có khoảng trên 1000010000 đỉnh, giải thuật ở trên sẽ không thể đáp ứng được yêu cầu về thời gian thực thi. Giải pháp là cải tiến việc tìm ra đỉnh uu có d[u]d[u] nhỏ nhất, thông qua cấu trúc dữ liệu Heap (mà trong C++ đã cài đặt sẵn dưới dạng hàng đợi ưu tiên -
3):
Thuật toán diễn ra thông qua các bước lặp trên hàng đợi ưu tiên cho tới khi hàng đợi rỗng, hoặc phần tử chứa d[t]d[t] được đưa lên đầu hàng đợi (tức là đường đi tới đỉnh tt đã được tối ưu):
Cài đặt
Đánh giáVới việc sử dụng danh sách kề, độ phức tạp về bộ nhớ của thuật toán chỉ là O(∣V∣+∣E∣)O\big(|V| + |E|\big). Về độ phức tạp tính toán, do việc áp dụng
3 để tìm ra đỉnh uu tốt nhất ở mỗi bước, nên độ phức tạp tổng quát của thuật toán là O(max(∣V∣,∣E∣).log(∣V∣))O\Big(\text{max}\big(|V|, |E|\big).\log\big(|V|\big)\Big). IV. Bài tập áp dụng1. Cai trị vương quốcĐề bài (Tóm tắt)Nhà vua của một vương quốc nọ là người không thông minh cho lắm, ông ta chỉ có thể làm được các phép tính cộng, so sánh lớn hơn (
Ngày nọ, một nhóm những kẻ chống đối trình lên nhà vua một vấn đề lớn dưới dạng một dãy số nguyên S={a1,a2,…,an};S = \{a_1, a_2, \dots, a_n\}; sau đó xin quyết định của nhà vua cho mm vấn đề nhỏ dưới dạng một dãy con S_i = \{a_{s_i}, a_{s_i + 1}, \dots, a_{s_i + n_i\} \ (1 \le i \le m) thuộc dãy SS. Nhà vua đã đưa ra quyết định nhanh chóng: Với mỗi dãy con Si,S_i, ngài đưa ra một số nguyên kik_i rồi thông báo tổng của dãy con SiS_i lớn hơn hoặc nhỏ hơn kik_i - đó chính là quyết định của ngài. Rồi như thường lệ, nhà vua lãng quên dãy số SS ban đầu đã nhận được. Tuy nhiên, nhà vua đã rơi vào âm mưu của những kẻ chống đối! Chúng đã tìm ra những sai sót trong quyết định của nhà vua và vin vào đó để tổ chức những cuộc biểu tình, hòng bắt ép nhà vua thoái vị. Nhận được tin báo, nhà vua rất lo lắng vì ngài đã quên mất dãy số SS ban đầu mình nhận được. Vì vậy, nhà vua quyết định đưa ra một dãy SS giả mạo sao cho nó thỏa mãn tất cả các quyết định mà ngài đã đưa ra, rồi tuyên bố rằng đó chính là vấn đề ban đầu mà mình nhận được. Yêu cầu: Hãy xác định xem nhà vua có thể tìm ra dãy SS giả mạo thỏa mãn tất cả những quyết định mà ngài đã đưa ra hay không? Input:
Ràng buộc:
Output:
Sample Input:
Sample Output:
Ý tưởngCai Trị Vương Quốc - EditorialXét hàm sum(k)=∑i=1kaisum(k) = \sum_{i = 1}^k a_i và coi sum(0)=0sum(0) = 0. Gọi tổng của dãy con si={asi,asi+1,…,asi+ni}s_i = \{a_{s_i}, a_{s_i + 1}, \dots, a_{s_i + n_i}\} là sumi (1≤i≤m)sum_i \ (1 \le i \le m). Từ tính chất tổng tiền tố, ta biết rằng sumi=sum(si+ni)−sum(si−1)sum_i = sum(s_i + n_i) - sum(s_i - 1). Khi đó, quyết định thứ i (1≤i≤m)i \ (1 \le i \le m) của nhà vua sẽ tương ứng với: sum(si+ni)−sum(si−1)>ki hoặc sum(si+ni)−sum(si−1) Như vậy, một dãy số S={a1,a2,…,an}S = \{a_1, a_2, \dots, a_n\} thỏa mãn tất cả các quyết định của nhà vua sẽ tồn tại khi và chỉ khi tồn tại một dãy các giá trị {sum(1),sum(2),…,sum(n)}\big\{sum(1), sum(2), \dots, sum(n)\big\} thỏa mãn điều kiện (∗)(*) với mọi i=1...mi = 1...m. Ta lại biến đổi một chút ở các điều kiện:
Áp dụng biến đổi trên vào dãy điều kiện (∗)(*) với mọi i=1...m,i = 1...m, ta có một dãy các ràng buộc ở dạng: {sum(xi)−sum(yi)≤cixi,yi,ci∈N0≤xi,yi≤n\begin{cases}sum(x_i) - sum(y_i) \le c_i \\ x_i, y_i, c_i \in \mathbb{N} \\ 0 \le x_i, y_i \le n \end{cases} |