Cho một đồ thị có hướng, mỗi cung có một sức chứa và một chi phí cho mỗi đơn vị hàng đi qua, cùng hai đỉnh và . Hãy chuyển đúng đơn vị hàng từ tới sao cho tổng chi phí nhỏ nhất. Với đồ thị đỉnh, cung và , bài này giải được bằng cách tăng luồng lần, mỗi lần theo đường đi rẻ nhất trong đồ thị dư.
Đây là bài toán luồng chi phí nhỏ nhất (min-cost flow). Ở bài luồng cực đại, mọi đường tăng luồng đều như nhau; ở đây ta chọn đường rẻ nhất, và cung ngược mang chi phí âm để việc rút lại hàng cũng được tính tiền đúng. Bài này dạy cách đó với thuật toán tìm đường ngắn nhất chịu được chi phí âm, rồi cách dùng thế đỉnh (potential) để chạy Dijkstra nhanh hơn, và cuối cùng là các bài quy về luồng chi phí: phân công, tìm đường không chung cung.
ITăng luồng theo đường đi rẻ nhất
Một mạng có chi phí gồm các cung , mỗi cung có sức chứa và chi phí cho mỗi đơn vị luồng; luồng trên cung đó tốn , và chi phí của luồng là tổng trên mọi cung. Ta muốn một luồng giá trị đúng có chi phí nhỏ nhất. Nếu mạng không có luồng giá trị thì không có đáp án.
Cách vét cạn là thử mọi cách phân đơn vị lên các cung, quá nhiều khi đồ thị lớn. Ta có thể bắt đầu từ cách của bài luồng cực đại: lần lượt tăng luồng theo các đường tăng luồng (bài 4.9.1). Câu hỏi là chọn đường nào. Nếu chọn đường tùy ý thì luồng có thể đắt hơn mức cần thiết; nếu mỗi lần chọn đường rẻ nhất trong đồ thị dư thì kết quả tối ưu. Cung ngược của cung có chi phí , vì rút lại một đơn vị hàng cũng lấy lại tiền đã tốn.
Hỏi nhanh
Cung có sức chứa 2 và chi phí 5 cho mỗi đơn vị. Sau khi đẩy 1 đơn vị qua nó, trong đồ thị dư có cung ngược với sức chứa và chi phí là bao nhiêu?
Cung ngược chỉ rút lại được đúng lượng đã đẩy, tức 1 đơn vị, nên sức chứa là 1. Rút lại thì lấy lại phần tiền đã tốn, nên chi phí là .
Ví dụ nhỏ: mạng có bốn đỉnh và năm cung, mọi sức chứa đều là 1. Các cung , và có chi phí 1; cung và có chi phí 4. Ta cần đơn vị. Đường rẻ nhất là , chi phí 3. Sau khi đẩy một đơn vị theo đó, ba cung của đường này đầy, đồ thị dư chỉ còn ba cung như hình, và đường tăng luồng duy nhất là với chi phí . Tổng chi phí là , đúng bằng cách đi riêng và , tốn .
Vì có cung chi phí âm nên không dùng Dijkstra thẳng được. Ta dùng SPFA, tức thuật toán Bellman-Ford có hàng đợi: một đỉnh được đưa vào hàng đợi mỗi khi khoảng cách tới nó giảm, và khi lấy ra, ta thử cải thiện khoảng cách tới các đỉnh kề. SPFA đúng với chi phí âm miễn là đồ thị không có chu trình âm; đồ thị dư của ta không có (xem ô Chứng minh).
Chứng minh
Đây là các ý chính, không phải chứng minh đầy đủ.
Luồng giá trị rẻ nhất khi và chỉ khi đồ thị dư không có chu trình âm. Nếu có chu trình âm, đẩy một đơn vị quanh chu trình đó giữ nguyên giá trị luồng và giảm chi phí. Ngược lại, hiệu của hai luồng cùng giá trị là một dòng tuần hoàn, phân tách được thành các chu trình trong đồ thị dư của luồng thứ nhất; nếu mọi chu trình không âm thì luồng thứ hai không rẻ hơn.
Tăng theo đường rẻ nhất giữ tính chất này. Gọi là khoảng cách ngắn nhất từ tới trong đồ thị dư hiện tại, tính cho các đỉnh tới được. Với mọi cung trong đồ thị dư mà tới được, , và dấu bằng xảy ra trên đường vừa chọn. Sau khi đẩy, các cung mới là cung ngược của các cung trên đường, đều cũng thỏa dấu bằng. Vậy mọi cung giữa các đỉnh tới được vẫn có ; cộng dọc một chu trình thì các triệt tiêu, nên tổng chi phí chu trình không âm. Cung đi từ đỉnh tới được sang đỉnh không tới được không tồn tại, nên chu trình đi qua một đỉnh tới được chỉ nằm trong các đỉnh tới được; còn chu trình gồm toàn đỉnh không tới được thì không bị thay đổi.
Mỗi lần đẩy ta lấy lượng bằng sức chứa nhỏ nhất trên đường, nhưng không vượt quá số đơn vị còn thiếu. Chi phí tăng thêm bằng lượng đẩy nhân chi phí của đường.
Hỏi nhanh
Một bạn tăng luồng theo đường rẻ nhất nhưng cộng chi phí bằng ans += d[t]; thay vì lượng đẩy nhân d[t]. Mạng chỉ có một cung với sức chứa 5 và chi phí 3, và cần đơn vị. Chương trình in bao nhiêu?
Chỉ có một đường, và lượng đẩy là trong một lần duy nhất. Chương trình cộng chi phí đường là 3 đúng một lần và in , trong khi đáp án đúng là . Vì một lần đẩy có thể mang nhiều đơn vị, chi phí phải nhân với lượng đẩy.
Ví dụChuyển Kiện Tiết Kiệm
Đề. Có thành phố và tuyến vận chuyển có hướng; tuyến chở tối đa kiện và mỗi kiện qua tuyến đó tốn (). Cần chuyển đúng kiện từ thành phố tới thành phố . In tổng chi phí nhỏ nhất, hoặc nếu không đủ sức chứa.
Lời giải. Mỗi tuyến là một cung với sức chứa và chi phí , và ta cần luồng giá trị nhỏ nhất chi phí. Ta tăng luồng cho tới khi đủ đơn vị hoặc không còn đường; nếu chưa đủ thì in . Mảng pe[v] ghi cung dẫn vào trên đường rẻ nhất, từ đó đi ngược từ về .
#include <bits/stdc++.h>
using namespace std;
#define ll long long
const ll N = 505, M = 2005, INF = 1e18;
ll n, m, k, ec, to[M], cap[M], cost[M], d[N], pe[N], inq[N];
vector<ll> g[N];
void addEdge(ll u, ll v, ll c, ll w) {
to[ec] = v, cap[ec] = c, cost[ec] = w, g[u].push_back(ec++);
to[ec] = u, cap[ec] = 0, cost[ec] = -w, g[v].push_back(ec++);
}
bool spfa(ll s, ll t) {
for (ll i = 1; i <= n; i++) d[i] = INF, inq[i] = 0;
queue<ll> q;
d[s] = 0;
q.push(s);
while (!q.empty()) {
ll u = q.front();
q.pop();
inq[u] = 0;
for (ll e : g[u])
if (cap[e] > 0 && d[u] + cost[e] < d[to[e]]) {
d[to[e]] = d[u] + cost[e];
pe[to[e]] = e;
if (!inq[to[e]]) inq[to[e]] = 1, q.push(to[e]);
}
}
return d[t] < INF;
}
signed main() {
ios::sync_with_stdio(false);
cin.tie(0);
cin >> n >> m >> k;
for (ll i = 1; i <= m; i++) {
ll a, b, r, c;
cin >> a >> b >> r >> c;
addEdge(a, b, r, c);
}
ll flow = 0, ans = 0;
while (flow < k && spfa(1, n)) {
ll f = k - flow;
for (ll v = n; v != 1; v = to[pe[v] ^ 1]) f = min(f, cap[pe[v]]);
for (ll v = n; v != 1; v = to[pe[v] ^ 1]) {
cap[pe[v]] -= f;
cap[pe[v] ^ 1] += f;
}
flow += f;
ans += f * d[n];
}
cout << (flow < k ? -1 : ans) << '\n';
}
4 5 3
1 2 5 100
1 3 10 50
1 4 7 500
2 4 8 350
3 4 2 100
750
Độ phức tạp: mỗi lần tăng luồng đẩy ít nhất 1 đơn vị nên có không quá lần, mỗi lần SPFA tốn trong trường hợp xấu nhất; với , , (tính cả cung ngược) cận này là , và thực tế SPFA nhanh hơn nhiều.
IIThế đỉnh và Dijkstra
SPFA có thể chậm khi đồ thị lớn. Ta muốn dùng Dijkstra, nhưng nó cần mọi cung không âm. Thế đỉnh (potential) là một số gán cho mỗi đỉnh, dùng để đổi chi phí của cung thành chi phí rút gọn
Mọi đường đi từ tới có chi phí rút gọn bằng chi phí thật cộng , vì các ở giữa triệt tiêu, nên đường nào ngắn nhất theo chi phí rút gọn cũng ngắn nhất theo chi phí thật. Nếu chọn làm cho mọi cung của đồ thị dư có thì Dijkstra dùng được.
Cách chọn là dùng chính khoảng cách đã tính. Sau mỗi lần Dijkstra, ta có khoảng cách rút gọn từ , và cập nhật . Khi đó chi phí thật của đường ngắn nhất tới chính là , vì luôn đúng. Các cung nằm trên đường vừa đẩy có chi phí rút gọn mới bằng , nên cung ngược của chúng cũng , và các cung khác vẫn không âm theo ý của ô Chứng minh ở mục trên. Đỉnh không tới được thì giữ nguyên thế: về sau nó vẫn không tới được.
Còn thế ban đầu: nếu mọi cung của mạng có chi phí không âm thì chọn . Nếu có cung chi phí âm, ta chạy Bellman-Ford một lần từ để có ban đầu, sau đó chỉ dùng Dijkstra.
Ví dụPhân Công Tối Ưu
Đề. Có nhân viên và công việc; nhân viên nhận công việc tốn (). Mỗi nhân viên nhận đúng một công việc và mỗi công việc giao cho đúng một nhân viên. In tổng chi phí nhỏ nhất và một phép gán tối ưu (mỗi dòng gồm nhân viên và công việc được giao).
Lời giải. Dựng mạng nhân viên công việc . Mỗi cung và mỗi cung công việc có sức chứa 1 và chi phí 0; cung nhân viên công việc có sức chứa 1 và chi phí . Một luồng giá trị phải đẩy đủ đơn vị qua cung ra khỏi , mỗi nhân viên đúng một đơn vị, mỗi công việc đúng một đơn vị, nên nó chính là một phép gán, và chi phí của luồng là tổng chi phí phép gán. Luồng chi phí nhỏ nhất cho phép gán tối ưu. Các cung nhân viên công việc đã đầy (sức chứa dư bằng 0) cho biết ai nhận việc nào. Mạng có đỉnh và cung, nên Dijkstra với thế đỉnh thích hợp; Hungary (thuật toán chuyên cho bài phân công, ) là một lựa chọn khác.
#include <bits/stdc++.h>
using namespace std;
#define ll long long
const ll N = 405, M = 81005, INF = 1e18;
ll n, V, ec, to[M], cap[M], cost[M], h[N], d[N], pe[N];
vector<ll> g[N];
void addEdge(ll u, ll v, ll c, ll w) {
to[ec] = v, cap[ec] = c, cost[ec] = w, g[u].push_back(ec++);
to[ec] = u, cap[ec] = 0, cost[ec] = -w, g[v].push_back(ec++);
}
bool dijkstra(ll s, ll t) {
for (ll i = 0; i < V; i++) d[i] = INF;
priority_queue<pair<ll, ll>, vector<pair<ll, ll> >, greater<pair<ll, ll> > > q;
d[s] = 0;
q.push(make_pair(0, s));
while (!q.empty()) {
ll du = q.top().first, u = q.top().second;
q.pop();
if (du > d[u]) continue;
for (ll e : g[u]) {
ll v = to[e], nd = du + cost[e] + h[u] - h[v];
if (cap[e] > 0 && nd < d[v]) {
d[v] = nd;
pe[v] = e;
q.push(make_pair(nd, v));
}
}
}
if (d[t] >= INF) return 0;
for (ll i = 0; i < V; i++)
if (d[i] < INF) h[i] += d[i];
return 1;
}
signed main() {
ios::sync_with_stdio(false);
cin.tie(0);
cin >> n;
ll s = 0, t = 2 * n + 1;
V = 2 * n + 2;
for (ll i = 1; i <= n; i++) {
addEdge(s, i, 1, 0);
addEdge(n + i, t, 1, 0);
for (ll j = 1; j <= n; j++) {
ll c;
cin >> c;
addEdge(i, n + j, 1, c);
}
}
ll ans = 0;
while (dijkstra(s, t)) {
ll f = INF;
for (ll v = t; v != s; v = to[pe[v] ^ 1]) f = min(f, cap[pe[v]]);
for (ll v = t; v != s; v = to[pe[v] ^ 1]) {
cap[pe[v]] -= f;
cap[pe[v] ^ 1] += f;
}
ans += f * h[t];
}
cout << ans << '\n';
for (ll i = 1; i <= n; i++)
for (ll e : g[i])
if (e % 2 == 0 && to[e] > n && to[e] <= 2 * n && cap[e] == 0) cout << i << ' ' << to[e] - n << '\n';
}
4
17 8 16 9
7 15 12 19
6 9 10 11
14 7 13 10
33
1 4
2 1
3 3
4 2
Độ phức tạp: có lần tăng luồng, mỗi lần Dijkstra tốn với ; với cần khoảng phép tính.
IIICác bài quy về luồng chi phí nhỏ nhất
Nhiều bài trông không liên quan cũng đưa được về luồng chi phí nhỏ nhất: phân công, vận chuyển từ nhiều nguồn tới nhiều đích (thêm một nguồn chung nối tới các nguồn thật), và chọn đối tượng với giới hạn số lần dùng (sức chứa của cung là giới hạn đó). Điều cần làm là xác định ba thứ: đỉnh là gì, cung nào có sức chứa bao nhiêu, và đại lượng cần tối thiểu là chi phí của cung nào.
Ví dụĐội Giao Hàng Không Chung Cung
Đề. Có điểm và tuyến một chiều. Phải chọn đúng lộ trình từ điểm tới điểm sao cho mỗi tuyến được dùng trong nhiều nhất một lộ trình, và tổng số tuyến đã đi là nhỏ nhất. In nếu không chọn được; nếu được, in tổng số tuyến, rồi với mỗi lộ trình in số đỉnh trên đường và dãy đỉnh.
Lời giải. Gán cho mỗi tuyến sức chứa 1 (mỗi tuyến dùng tối đa một lần) và chi phí 1 (mỗi tuyến đã đi đếm là một). Khi đó lộ trình không chung cung cho một luồng nguyên giá trị , và chi phí luồng chính là tổng số tuyến. Ngược lại, từ một luồng nguyên giá trị ta lấy lại được lộ trình không chung cung bằng cách đi từ theo các cung đang có luồng, mỗi lần lấy một cung chưa dùng, cho tới khi tới ; luồng chi phí nhỏ nhất không có chu trình vì chi phí dương, nên cách đi này không lặp. Không đủ đơn vị thì in . Ta in các lộ trình theo thứ tự từ điển.
#include <bits/stdc++.h>
using namespace std;
#define ll long long
const ll N = 505, M = 2005, INF = 1e18;
ll n, m, k, ec, to[M], cap[M], cost[M], d[N], pe[N], inq[N], used[M];
vector<ll> g[N];
vector<vector<ll> > ps;
void addEdge(ll u, ll v, ll c, ll w) {
to[ec] = v, cap[ec] = c, cost[ec] = w, g[u].push_back(ec++);
to[ec] = u, cap[ec] = 0, cost[ec] = -w, g[v].push_back(ec++);
}
bool spfa(ll s, ll t) {
for (ll i = 1; i <= n; i++) d[i] = INF, inq[i] = 0;
queue<ll> q;
d[s] = 0;
q.push(s);
while (!q.empty()) {
ll u = q.front();
q.pop();
inq[u] = 0;
for (ll e : g[u])
if (cap[e] > 0 && d[u] + cost[e] < d[to[e]]) {
d[to[e]] = d[u] + cost[e];
pe[to[e]] = e;
if (!inq[to[e]]) inq[to[e]] = 1, q.push(to[e]);
}
}
return d[t] < INF;
}
signed main() {
ios::sync_with_stdio(false);
cin.tie(0);
cin >> n >> m >> k;
for (ll i = 1; i <= m; i++) {
ll a, b;
cin >> a >> b;
addEdge(a, b, 1, 1);
}
ll flow = 0, ans = 0;
while (flow < k && spfa(1, n)) {
ll f = k - flow;
for (ll v = n; v != 1; v = to[pe[v] ^ 1]) f = min(f, cap[pe[v]]);
for (ll v = n; v != 1; v = to[pe[v] ^ 1]) {
cap[pe[v]] -= f;
cap[pe[v] ^ 1] += f;
}
flow += f;
ans += f * d[n];
}
if (flow < k) {
cout << -1 << '\n';
return 0;
}
for (ll i = 1; i <= k; i++) {
vector<ll> p;
ll v = 1;
p.push_back(v);
while (v != n) {
for (ll e : g[v])
if (e % 2 == 0 && cap[e] == 0 && !used[e]) {
used[e] = 1;
v = to[e];
break;
}
p.push_back(v);
}
ps.push_back(p);
}
sort(ps.begin(), ps.end());
cout << ans << '\n';
for (ll i = 0; i < k; i++) {
cout << ps[i].size() << '\n';
for (ll v : ps[i]) cout << v << ' ';
cout << '\n';
}
}
5 6 2
1 2
2 5
1 3
3 4
4 5
2 3
5
3
1 2 5
4
1 3 4 5
Độ phức tạp: SPFA mỗi lần và không quá lần tăng luồng, với , , cận trên là nhưng thực tế chạy nhanh hơn nhiều; phần tách đường tốn nhiều nhất.
Bài nâng cao
Xem 1 bài nâng cao
Ví dụDãy Gần Hoán Vị
Đề. Cần chọn dãy với , mỗi phần tử thuộc , thỏa ràng buộc: loại 1 đòi với mọi , loại 2 đòi . Gọi là số lần giá trị xuất hiện; tối thiểu hóa , hoặc in nếu không có dãy hợp lệ.
Lời giải. Từ các ràng buộc, mỗi vị trí có một khoảng giá trị hợp lệ . Ta xếp mỗi vị trí vào một giá trị trong khoảng của nó, và cần tối thiểu tổng bình phương số vị trí ở mỗi giá trị. Chi phí là chi phí lồi, tăng nhanh dần, và có thể cài bằng luồng chi phí nhỏ nhất: với mỗi giá trị , thay một cung bằng cung sức chứa 1 tới với chi phí . Dùng cung rẻ nhất thì tổng chi phí là , và luồng chi phí nhỏ nhất luôn dùng các cung rẻ trước. Mạng là vị trí (sức chứa 1, chi phí 0), vị trí giá trị trong khoảng hợp lệ (sức chứa 1, chi phí 0), giá trị (các cung chi phí lẻ). Nếu luồng không đủ đơn vị thì in .
#include <bits/stdc++.h>
using namespace std;
#define ll long long
const ll N = 105, M = 12005, INF = 1e18;
ll n, q, V, ec, lo[N], hi[N], to[M], cap[M], cost[M], d[N], pe[N], inq[N];
vector<ll> g[N];
void addEdge(ll u, ll v, ll c, ll w) {
to[ec] = v, cap[ec] = c, cost[ec] = w, g[u].push_back(ec++);
to[ec] = u, cap[ec] = 0, cost[ec] = -w, g[v].push_back(ec++);
}
bool spfa(ll s, ll t) {
for (ll i = 0; i < V; i++) d[i] = INF, inq[i] = 0;
queue<ll> qu;
d[s] = 0;
qu.push(s);
while (!qu.empty()) {
ll u = qu.front();
qu.pop();
inq[u] = 0;
for (ll e : g[u])
if (cap[e] > 0 && d[u] + cost[e] < d[to[e]]) {
d[to[e]] = d[u] + cost[e];
pe[to[e]] = e;
if (!inq[to[e]]) inq[to[e]] = 1, qu.push(to[e]);
}
}
return d[t] < INF;
}
signed main() {
ios::sync_with_stdio(false);
cin.tie(0);
cin >> n >> q;
for (ll i = 1; i <= n; i++) lo[i] = 1, hi[i] = n;
for (ll i = 1; i <= q; i++) {
ll t, l, r, v;
cin >> t >> l >> r >> v;
for (ll x = l; x <= r; x++) {
if (t == 1) lo[x] = max(lo[x], v);
else hi[x] = min(hi[x], v);
}
}
ll s = 0, t = 2 * n + 1;
V = 2 * n + 2;
for (ll i = 1; i <= n; i++) {
addEdge(s, i, 1, 0);
for (ll y = lo[i]; y <= hi[i]; y++) addEdge(i, n + y, 1, 0);
for (ll j = 1; j <= n; j++) addEdge(n + i, t, 1, 2 * j - 1);
}
ll flow = 0, ans = 0;
while (spfa(s, t)) {
ll f = INF;
for (ll v = t; v != s; v = to[pe[v] ^ 1]) f = min(f, cap[pe[v]]);
for (ll v = t; v != s; v = to[pe[v] ^ 1]) {
cap[pe[v]] -= f;
cap[pe[v] ^ 1] += f;
}
flow += f;
ans += f * d[t];
}
cout << (flow < n ? -1 : ans) << '\n';
}
4 2
1 1 2 2
2 3 4 3
4
Độ phức tạp: mạng có đỉnh và cung, lần tăng luồng mỗi lần SPFA ; với cần khoảng phép tính.
Khi đi thi
- Có chi phí âm ban đầu: chạy Bellman-Ford một lần để có thế đỉnh, rồi dùng Dijkstra cho mọi lần sau.
- Luồng nhỏ (tổng luồng vài trăm tới vài nghìn) thì SPFA đủ nhanh; luồng lớn hay đồ thị dày thì cần Dijkstra với thế đỉnh.
- Chi phí lồi theo lượng luồng qua một đối tượng: tách thành nhiều cung sức chứa 1 với chi phí tăng dần.
Tóm tắt
- Luồng chi phí nhỏ nhất cho giá trị có được bằng cách tăng luồng lần, mỗi lần theo đường rẻ nhất trong đồ thị dư; cung ngược có chi phí đối của cung xuôi.
- Đồ thị dư của một luồng rẻ nhất không có chu trình âm, nên SPFA tìm đường rẻ nhất đúng dù có cung chi phí âm.
- Thế đỉnh làm chi phí rút gọn không âm; sau mỗi lần Dijkstra, cộng khoảng cách vào , và chi phí thật của đường là .
- Phân công là luồng giá trị trên mạng ghép cặp có chi phí; đường không chung cung là luồng giá trị với sức chứa và chi phí đều 1.
- Chi phí lồi tách thành các cung sức chứa 1 với chi phí tăng dần.
Bài tập
Đọc thêm
- Luồng với chi phí nhỏ nhấtVNOI Wiki
- Minimum-cost flowcp-algorithms
- 费用流OI Wiki