Trong khi cải tiến thuật toán ở phần trước với Dinitz, chúng ta đẩy nhanh được tốc độ đẩy luồng sử dụng blocking - flow. Tuy nhiên độ phức tập thuật toán vẫn là O(f.E.V) với f là tổng luồng, vì trong trường hợp xấu nhất, mỗi lần scaling flow chúng ta cũng chỉ giảm được 1. Nếu chúng ta muốn đẩy nhanh hơn về mặt tốc độ, thường sẽ nghĩ đến việc tìm đường đi ngắn nhất (về mặt chi phí - cost) bằng Dijkstra thay cho Bellman-Ford.
Vấn đề ở chỗ Dijkstra không thể xử lý nếu đồ thị có chu trình âm. Mà điều này luôn có khả năng xảy ra sau các lần tăng luồng mà mạng luồng ban đầu có chi phí âm. Cho nên thuật toán Algorithm 5 như chúng ta thảo luận trước đó là một phương pháp an toàn, đổi lại cho tốc độ xử lý chậm.
Nhưng vậy, nếu chúng ta được đảm bảo hoặc chứng minh được mạng luồng của mình không tạo ra chu trình âm trên mạng thặng dư thì chúng ta có thể dùng Dijkstra thay cho Bellman để đẩy nhanh tốc độ. Tuy nhiên, để áp dụng được Dijkstra trong trường hợp này, chúng ta vẫn cần thêm một vài kiến thức khác.
CP-Algorithm - Minimum-Cost Flow
CodeForce Tutorial - Minimum Cost Flow
Topcoder - Minimum Cost Flow: Key Concept and Part two
Hackerearth - Minimum Cost Flow
Steven Halim - Competitive Programming 4 - MinCost Flow - Chapter 9, page 571 - 579
Brillian.org - Johnson's Algorithm
GeeksforGeeks - Johnson's Algorithm
Đây là thuật toán cho phép tìm đường đi ngắn nhất giữa mọi cặp đỉnh trên đồ thị có hướng có trọng số và chấp nhật một số cạnh có trọng số âm (nhưng không có chu trình âm).
Ý tưởng chính của thuật toán là dùng thuật toán Bellman-Ford để tìm đường đi ngắn nhất từ s đến mọi đỉnh rời thực hiện chuyển đổi đồ thị sang dạng mới không có trọng số âm trên cạnh nào. Tiếp theo dùng thuật toán Dijsktra tìm đường đi ngắn nhất trên đồ thị mới này. Cuối cùng kết quả trả về là quy đổi giá trị đường đi ngắn nhất ngược về đồ thị góc ban đầu.
Các bước thực hiện cụ thể như sau:
Bước 1. Thêm một đỉnh q vào đồ thị, connect q với các đỉnh khác của đồ thị với chi phí 0.
Bước 2. Dùng Bellman-Ford tìm đường đi ngắn nhất từ q tới mọi đỉnh v, đặt là h(v). Nếu bước này phát hiện chu trình âm thì dừng thuật toán.
Bước 3. Chuyển đổi đồ thị, với mỗi cạnh e(u, v) trên G có trọng số w(u, v) ta chuyển thành cạnh mới với trọng số w(u,v) + h(u) − h(v)
Bước 4. Loại bỏ đỉnh q. Chạy Dijsktra V lần trên đồ thị mới để tìm đường đi ngắn nhất giữa mọi cặp đỉnh, ta có giá trị đường đi ngắn nhất giữa mọi định u, v là D(u, v)
Bước 5. Chuyển toàn bộ đường đi ngắn nhất này về giá trị gốc bằng cách D(u, v) += h(v)-h(u)
Nguồn: wikipedia
Quá trình chuyển đổi đồ thị (từ bước 1 tới bước 3). Nguồn ảnh: Wikipedia
Mã giả thuật toán trên chi tiết như sau:
Johnson(G)
1.
create G` where G`.V = G.V + {s},
G`.E = G.E + ((s, u) for u in G.V), and
weight(s, u) = 0 for u in G.V
2.
if Bellman-Ford(s) == False
return "The input graph has a negative weight cycle"
else:
for vertex v in G`.V:
h(v) = distance(s, v) computed by Bellman-Ford
for edge (u, v) in G`.E:
weight`(u, v) = weight(u, v) + h(u) - h(v)
3.
D = new matrix of distances initialized to infinity
for vertex u in G.V:
run Dijkstra(G, weight`, u) to compute distance`(u, v) for all v in G.V
for each vertex v in G.V:
D_(u, v) = distance`(u, v) + h(v) - h(u)
return D
Nguồn: https://brilliant.org/
Mô phỏng thuật toán Johnson - Nguồn: Brilliant.org
Cách chuyển đổi trên được nhiều tài liệu gọi tên Potential (tạm dịch là thế năng), với ý nghĩa rằng chi phí trên đường đi được cộng thêm một lượng thay đổi (chênh lệch) ở điểm cuối của nó.
Ý tưởng về Potential (thế năng) này sẽ được áp dụng vào bài toán Min Cost Flow cho việc tìm đường đi ngắn nhất dựa vào chi phí thông luồng.
Toàn bộ ý tưởng, mã giả, giải thuật và sourcode của thuật toán Johnson có thể tham khảo tại: Brilliant.org, Wikipedia, GeeksforGeeks
Bây giờ chúng ta đã có thể hiểu được cách cải tiến tốc độ cho bài toán Min Cost Flow khi trong mạng không có chi phí âm. Thuật toán có thể tóm tắt như sau:
Nguồn: Codeforce Tutorial
Use Bellman-Ford ở bước thứ 2 là để tính thế năng, và đảm bảo đồ thì không còn chu trình âm thì mới dùng Dijkstra sau đó được. Nếu tại bước này phát hiện chu trình âm thì thoát và không tiến hành các bước sau đó.
Cài đặt chi tiết tham khảo
#include<bits/stdc++.h>
using namespace std;
const int maxn = 5e4 + 10;//Giới hạn số đỉnh
const int INF_COST = 0X3f3f3f3f; // Giá trị lớn cho chi phí (dist, hay cost)
const int INF_FLOW = 0x3f3f3f3f; // Giá trị lớn cho luồng (flow)
int head[maxn];
int dis[maxn]; // Khoảng cách (dijkstra distance)
int h[maxn]; // Potential Function (h)
int flow_limit[maxn]; // Luồng tối đa có thể đẩy đến đỉnh này
int pre[maxn]; // Đỉnh trước đó trên đường đi
int last[maxn]; // Chỉ số cạnh trước đó trên đường đi
int tot; // Tổng số cạnh đã thêm
int n, m, s, t; // Số đỉnh, số cạnh, Nguồn, Đích
// Khai báo cạnh
struct node{
int to;
int next;
int f; // Capacity (khả năng)
int w; // Cost (chi phí)
node() {}
node(int a, int b, int c, int d) : to(a), next(b), f(c), w(d) {}
}edge[maxn * 10]; // 5e4 * 10 = 500,000 cạnh, đủ cho 5e4 đỉnh.
// Thêm cạnh
void edgeadd(int a, int b, int c, int d){
edge[tot] = node(b, head[a], c, d);
head[a] = tot++;
edge[tot] = node(a, head[b], 0, -d); // Cung ngược với chi phí âm
head[b] = tot++;
}
// Khởi tạo đồ thị
void init(){
memset(head, -1, sizeof(head));
tot = 0;
}
// Khởi tạo các mảng cho tìm đường đi ngắn nhất
void path_init(){
// dis: khoảng cách (chi phí)
// flow_limit: luồng tối đa có thể qua
memset(dis, 0X3f, sizeof(dis));
memset(flow_limit, 0X3f, sizeof(flow_limit));
}
// ----------------------------------------------------
// Bước 1: Khởi tạo Potential Function (dùng SPFA cho lần đầu)
// Cần chạy SPFA lần đầu để tính h[] ban đầu.
// Hàm này là bắt buộc nếu muốn dùng Dijkstra sau đó.
// ----------------------------------------------------
bool initial_spfa() {
// Chỉ dùng cho lần đầu để tính h[] (potential)
memset(dis, 0X3f, sizeof(dis));
memset(h, 0, sizeof(h)); // Khởi tạo h[i] = 0
memset(last, -1, sizeof(last));
// Sử dụng mảng vis[] để kiểm tra chu trình âm
vector<int> cnt(n + 1, 0); // Đếm số lần đỉnh được push vào queue
vector<bool> vis(n + 1, false);
queue<int> q;
q.push(s);
dis[s] = 0;
vis[s] = true;
while (!q.empty()) {
int u = q.front();
q.pop();
vis[u] = false;
for (int i = head[u]; i != -1; i = edge[i].next) {
int v = edge[i].to;
int w = edge[i].w;
int f = edge[i].f;
if (f > 0 && dis[v] > dis[u] + w) {
dis[v] = dis[u] + w;
cnt[v]++;
if (cnt[v] > n) {
// Phát hiện chu trình âm (lỗi logic nếu bài toán không cho phép)
return false;
}
if (!vis[v]) {
q.push(v);
vis[v] = true;
}
}
}
}
// Gán kết quả dis[] thành h[] ban đầu
for(int i = 1; i <= n; i++) h[i] = dis[i];
return true;
}
// ----------------------------------------------------
// Bước 2: Tìm đường ngắn nhất bằng Dijkstra + Potential
// ----------------------------------------------------
bool dijkstra(){
path_init();
// pair<re-weighted cost, node>
priority_queue<pair<int, int>, vector<pair<int, int>>, greater<pair<int, int>>> pq;
dis[s] = 0;
flow_limit[s] = INF_FLOW;
pq.push({0, s});
while(!pq.empty()){
int d = pq.top().first;
int u = pq.top().second;
pq.pop();
if (d > dis[u]) continue; // Đã tìm thấy đường đi ngắn hơn
for(int i = head[u]; i != -1; i = edge[i].next){
int v = edge[i].to;
int w = edge[i].w; // Chi phí gốc
int f = edge[i].f;
// Chi phí tái định trọng số (Reduced Cost): c'(u, v) = c(u, v) + h(u) - h(v)
int reduced_cost = w + h[u] - h[v];
if(f > 0 && dis[v] > dis[u] + reduced_cost){
dis[v] = dis[u] + reduced_cost;
flow_limit[v] = min(flow_limit[u], f);
pre[v] = u;
last[v] = i;
pq.push({dis[v], v});
}
}
}
if (dis[t] == INF_COST) return false; // Không còn đường đi
// Cập nhật Potential Function cho lần lặp tiếp theo
// h'[v] = h[v] + dis[v]
for(int i = 1; i <= n; i++){
if(dis[i] != INF_COST){
h[i] += dis[i];
}
}
return true;
}
// ----------------------------------------------------
// Min Cost Max Flow
// ----------------------------------------------------
void dinic(){
int maxflow = 0;
long long minw = 0; // Chi phí
// Bước 1: Khởi tạo Potential
if (!initial_spfa()) {
// Xử lý chu trình âm
cout << "Chu trinh am trong do thi goc." << endl;
return;
}
// Lặp lại tìm đường ngắn nhất (theo chi phí) và đẩy luồng
while(dijkstra()){
// Luồng được đẩy qua đường đi ngắn nhất về chi phí
int push_flow = flow_limit[t];
// Chi phí thực tế của đường đi: chi phí tái định trọng số + h[t] - h[s]
// Vì h[t] - h[s] đã được đưa vào dis[t] khi cập nhật h[]
// Chi phí thực tế = (dis[t] ban đầu) + (h[t] - h[s])
// Tuy nhiên, cách đơn giản nhất là tính tổng chi phí gốc trên các cạnh.
long long current_cost = 0;
// Tăng luồng
maxflow += push_flow;
// Đẩy luồng trên đường đi
int x = t;
while(x != s){
int edge_id = last[x];
current_cost += (long long)push_flow * edge[edge_id].w;
edge[edge_id].f -= push_flow; // Giảm luồng cung xuôi
edge[(edge_id ^ 1)].f += push_flow; // Tăng luồng cung ngược
x = pre[x];
}
minw += current_cost;
}
// Output: maxflow minw
cout << maxflow << " " << minw << endl;
}
int main(){
ios::sync_with_stdio(false);
cin.tie(NULL);
cin >> n >> m >> s >> t;
init(); // Khởi tạo head[] và tot
for(int i = 1; i <= m; i++){
int a, b, c, d; // a, b: đỉnh; c: khả năng (f); d: chi phí (w)
cin >> a >> b >> c >> d;
edgeadd(a, b, c, d);
}
dinic(); // Min Cost Max Flow
return 0;
}
Nhiệm vụ 2.1. Codeforce - Four Melodies
Nhiệm vụ 2.2. Codeforce - April Fools' Problem
Nhiệm vụ 2.3. Codeforce - Team Building
Nhiệm vụ 2.4. Codeforce - Cow and Exercise
Nhiệm vụ 2.5. Thực hiện contest ngắn - Min Cost Flow 2 (Tuần sau làm)