HihoCoder上網絡流算法題目建模總結
經過了幾天的學習和做題,我利用劉汝佳書上的網絡流算法模板完成了HihoCoder上的幾個網絡流算法,HihoCoder可能還會繼續更新網絡流算法,所以我也會接著總結。
這個主要是對網絡流算法的建模做分析和理解,不具體分析網絡流算法,網絡流算法會單獨總結。
網絡流一·Ford-Fulkerson算法
本題沒有建模,就是標準的網絡最大流求解,將圖建完後直接應用最大流算法即可解決。但在此記錄幾點註意的地方:
所謂的“殘留網絡”就是為了讓程序在遍歷時可以會推所添加的記錄流量差的反向邊。比如 a-->b 容量為10,流量為3,其意義為從a到b已經走了3個流量,還有7個流量可以走過去,3個流量可以再退回來。
增廣路徑就是找從 s 到 t 的能通過的路徑,所謂能通過就是還存在未滿流的邊可以再走一些流量。這類增廣路徑算法的思想就是不斷地在“殘留網絡”上找“增廣路徑”,然後修改殘留網絡上的流量,直到不通為止。
代碼如下,為劉汝佳書《算法競賽入門經典》中一個模板:
const int maxn = 505; const int INF = 0x7fffffff; struct Edge { int from, to, cap, flow; Edge(int u, int v, int c, int f) : from(u), to(v), cap(c), flow(f) {} }; struct EdmondsKarp { int n, m; vector<Edge> edges; vector<int> G[maxn]; int a[maxn]; int p[maxn]; void init(int n) { for (int i = 0; i < n; ++i) G[i].clear(); edges.clear(); } void AddEdge(int from, int to, int cap) { edges.push_back(Edge(from, to, cap, 0)); edges.push_back(Edge(to, from, 0, 0)); // reverse edge m = edges.size(); G[from].push_back(m - 2); G[to].push_back(m - 1); } int Maxflow(int s, int t) { int flow = 0; for (;;) { memset(a, 0, sizeof(a)); queue<int> Q; Q.push(s); a[s] = INF; while (!Q.empty()) { int x = Q.front(); Q.pop(); for (int i = 0; i < G[x].size(); ++i) { Edge &e = edges[G[x][i]]; if (!a[e.to] && e.cap > e.flow) { p[e.to] = G[x][i]; a[e.to] = min(a[x], e.cap - e.flow); Q.push(e.to); } } if (a[t]) break; } if (!a[t]) break; for (int u = t; u != s; u = edges[p[u]].from) { edges[p[u]].flow += a[t]; edges[p[u] ^ 1].flow -= a[t]; } flow += a[t]; } return flow; } }; int main() { #ifdef LOCAL freopen("input.txt", "r", stdin); // freopen("sorted.txt", "w", stdout); #endif int N, M, u, v, c; cin >> N >> M; EdmondsKarp ek; // construct the graph ek.init(N); for (int i = 0; i < M; ++i) { cin >> u >> v >> c; ek.AddEdge(u, v, c); } cout << ek.Maxflow(1, N) << endl; return 0; }
網絡流二·最大流最小割定理
這部分主要是證明最小割等於最大流,證明詳細步驟見上面題目,這裏記錄下主要步驟:
f(S, T) 等於從 s 出來的流,等於當前的網絡流量 f。f(S, T) 表示割 (S, T)的凈流量。
對於網絡的任何一個流,一定小於等於任何一個割的容量(f(S, T) <= C(S, T)
對於一個網絡 G=(V, E),有源點 s 匯點 t,以下三個等價:
1、f 是圖 G 的最大流
2、殘留網絡不存在增廣路
3、對於G的一個割(S, T),此時 f = C(S, T)
證明:
1=>2:假設 f 是圖 G 的最大流,如果殘留網絡存在增廣路 p,流量為 fp,那麽有流 f‘ = f + fp > f ,與 f 是最大流矛盾。
2=>3:對於任意的 u S v T,有 f(u, v) = c(u, v),即 \[\sum f(u, v)=\sum c(u, v) = f(S, T) = C(S, T) = f\]
這樣,找不到增廣路的時候求得的一定是最大流,最大流等於最小割。
另外,本題要求求出最小割集合 S,在割 (S, T) 中,計算出的殘留網絡從 s 開始遍歷,所能遍歷到的點即為 S 集合,因為求得的最小割就是最大流,最大流中殘留網絡不存在增廣路徑,也就是說從 s 沒法走到 t,故從 s 開始遍歷,所得到的點的集合就是 S。
const int maxn = 505;
const int INF = 0x7FFFFFFF;
struct Edge {
int from, to, cap, flow;
Edge(int u, int v, int c, int f) : from(u), to(v), cap(c), flow(f) {}
};
bool used[maxn];
std::vector<int> rst;
struct EdmondsKarp {
int n, m;
vector<Edge> edges;
vector<int> G[maxn];
int a[maxn];
int p[maxn];
void init(int n) {
for (int i = 0; i < n; ++i) G[i].clear();
edges.clear();
}
void AddEdge(int from, int to, int cap) {
edges.push_back(Edge(from, to, cap, 0));
edges.push_back(Edge(to, from, 0, 0)); // reverse edge
m = edges.size();
G[from].push_back(m - 2);
G[to].push_back(m - 1);
}
int Maxflow(int s, int t) {
int flow = 0;
for (;;) {
memset(a, 0, sizeof(a));
queue<int> Q;
Q.push(s);
a[s] = INF;
while (!Q.empty()) {
int x = Q.front(); Q.pop();
for (int i = 0; i < G[x].size(); ++i) {
Edge &e = edges[G[x][i]];
if (!a[e.to] && e.cap > e.flow) {
p[e.to] = G[x][i];
a[e.to] = min(a[x], e.cap - e.flow);
Q.push(e.to);
}
}
if (a[t]) break;
}
if (!a[t]) break;
for (int u = t; u != s; u = edges[p[u]].from) {
edges[p[u]].flow += a[t];
edges[p[u] ^ 1].flow -= a[t];
}
flow += a[t];
}
return flow;
}
void GetMinCutSetS(int s) {
rst.push_back(s); used[s] = true;
for (int i = 0; i < G[s].size(); ++i) {
Edge &e = edges[G[s][i]];
if (!used[e.to] && e.flow != e.cap) {
GetMinCutSetS(e.to);
}
}
}
};
int main(int argc, char** argv) {
#ifdef LOCAL
freopen("input.txt", "r", stdin);
freopen("output.txt", "w", stdout);
#endif
// 2 <= N <= 500, 1 <= M <= 20000
int N, M; cin >> N >> M;
EdmondsKarp ek;
ek.init(N);
// construct the graph
int u, v, c;
for (int i = 0; i < M; ++i) {
cin >> u >> v >> c;
ek.AddEdge(u, v, c);
}
int flow = ek.Maxflow(1, N);
ek.GetMinCutSetS(1);
std::cout << flow << " " << rst.size() << std::endl;
for (int i = 0; i < rst.size() - 1; ++i) {
cout << rst[i] << " ";
}
std::cout << rst[rst.size() - 1] << std::endl;
return 0;
}
說明:代碼中的 GetMinCutSetS
就是一個 DFS 方法,從一個點開始遍歷得到最終的 S 集合,沒什麽難的。結果保存在一個 vector 中。
網絡流三·二分圖多重匹配
二分圖的多重匹配,其實質就是需要規定 X 集中的點可以使用多少次,Y 中的點可以重用多少次,如果 X 中的某個點的流量使用完畢,則這條邊滿流,則不可再次使用。從源點 s 指向 X 集中的邊的容量則規定了這個點能用多少次!Y 集中的指向匯點 t 的邊的容量也是如此含義。所以,如果 Y 集中的容量沒有用光,則說明當前的流(匹配)還沒有達到所期望的要求。
這題使用了CheckMaxMatch
用來判斷指向匯點的邊是否滿流。
const int maxn = 1005;
const int INF = 0x7FFFFFFF;
struct Edge {
int from, to, cap, flow;
Edge(int u, int v, int c, int f) : from(u), to(v), cap(c), flow(f) {}
};
// 求最小割所用到的兩個
// 求最小割點的思路為在原來求最大流的殘留網絡上從 s 點開始 DFS,所有能遍歷到的點都是 S 集合裏面的,
// 剩余沒有遍歷到的點就是 T 集合裏的點。
// bool used[maxn];
// std::vector<int> rst;
struct EdmondsKarp {
int n, m;
vector<Edge> edges;
vector<int> G[maxn];
int a[maxn];
int p[maxn];
void init(int n) {
for (int i = 0; i < n; ++i) G[i].clear();
edges.clear();
}
void AddEdge(int from, int to, int cap) {
edges.push_back(Edge(from, to, cap, 0));
edges.push_back(Edge(to, from, 0, 0)); // reverse edge
m = edges.size();
G[from].push_back(m - 2);
G[to].push_back(m - 1);
}
int Maxflow(int s, int t) {
int flow = 0;
for (;;) {
memset(a, 0, sizeof(a));
queue<int> Q;
Q.push(s);
a[s] = INF;
while (!Q.empty()) {
int x = Q.front(); Q.pop();
for (int i = 0; i < G[x].size(); ++i) {
Edge &e = edges[G[x][i]];
if (!a[e.to] && e.cap > e.flow) {
p[e.to] = G[x][i];
a[e.to] = min(a[x], e.cap - e.flow);
Q.push(e.to);
}
}
if (a[t]) break;
}
if (!a[t]) break;
for (int u = t; u != s; u = edges[p[u]].from) {
edges[p[u]].flow += a[t];
edges[p[u] ^ 1].flow -= a[t];
}
flow += a[t];
}
return flow;
}
bool CheckMaxMatch(int N, int M) {
for (int i = 1; i <= M; ++i) {
for (int j = 0; j < G[N + i].size(); ++j) {
Edge &e = edges[G[N + i][j]];
if (e.flow != e.cap && e.flow > 0) { return false; }
}
}
return true;
}
/*
// 遍歷求網絡的最小割中的 S 集合點,結果儲存在上面的 vector<int> rst 中;
void GetMinCutSetS(int s) {
rst.push_back(s); used[s] = true;
for (int i = 0; i < G[s].size(); ++i) {
Edge &e = edges[G[s][i]];
if (!used[e.to] && e.flow != e.cap) {
GetMinCutSetS(e.to);
}
}
}
*/
};
int main(int argc, char** argv) {
#ifdef LOCAL
freopen("input.txt", "r", stdin);
freopen("output.txt", "w", stdout);
#endif
int T; cin >> T;
while (T--) {
int N, M; cin >> N >> M;
EdmondsKarp ek;
ek.init(N + M + 2);
int m[maxn + 5], a[maxn + 5], b[maxn + 5];
for (int i = 0; i < M; ++i) cin >> m[i];
for (int i = 0; i < N; ++i) {
cin >> a[i] >> b[i];
int tmprecv;
for (int j = 0; j < b[i]; ++j) {
cin >> tmprecv;
// X -> Y
ek.AddEdge(i + 1, tmprecv + N, 1);
}
}
for (int i = 1; i <= N; ++i) {
ek.AddEdge(0, i, a[i - 1]);
}
for (int i = 1; i <= M; ++i) {
ek.AddEdge(N + i, N + M + 1, m[i - 1]);
}
ek.Maxflow(0, N + M + 1);
cout << (ek.CheckMaxMatch(N, M) ? "Yes" : "No") << endl;
}
return 0;
}
網絡流四·最小路徑覆蓋
建圖的方法為:
1、添加源點 s 和匯點 t。
2、拆點,將每個點拆成兩個點,比如 a 拆成 a1, a2,b 拆成 b1, b2。
3、從源點向 X 集合中每個點添加一條容量為 1 的有向邊。
4、從 Y 集合向匯點中每個點添加一條容量為 1 的有向邊。
5、如果 a -> b 有邊,則從 a1 向 b2 添加一條容量為 1 的有向邊。
最小路徑覆蓋就是總點數 N - 最小割。證明在我學會之前暫時不寫。
推薦去看《計算機算法設計與分析》中的網絡流 24 題中的魔術球問題,這是一道很隱晦的利用網絡流的最小路徑覆蓋問題,很經典。
const int maxn = 1005;
const int INF = 0x7FFFFFFF;
struct Edge {
int from, to, cap, flow;
Edge(int u, int v, int c, int f) : from(u), to(v), cap(c), flow(f) {}
};
// 求最小割所用到的兩個
// 求最小割點的思路為在原來求最大流的殘留網絡上從 s 點開始 DFS,所有能遍歷到的點都是 S 集合裏面的,
// 剩余沒有遍歷到的點就是 T 集合裏的點。
// bool used[maxn];
// std::vector<int> rst;
struct EdmondsKarp {
int n, m;
vector<Edge> edges;
vector<int> G[maxn];
int a[maxn];
int p[maxn];
void init(int n) {
for (int i = 0; i < n; ++i) G[i].clear();
edges.clear();
}
void AddEdge(int from, int to, int cap) {
edges.push_back(Edge(from, to, cap, 0));
edges.push_back(Edge(to, from, 0, 0)); // reverse edge
m = edges.size();
G[from].push_back(m - 2);
G[to].push_back(m - 1);
}
int Maxflow(int s, int t) {
int flow = 0;
for (;;) {
memset(a, 0, sizeof(a));
queue<int> Q;
Q.push(s);
a[s] = INF;
while (!Q.empty()) {
int x = Q.front(); Q.pop();
for (int i = 0; i < G[x].size(); ++i) {
Edge &e = edges[G[x][i]];
if (!a[e.to] && e.cap > e.flow) {
p[e.to] = G[x][i];
a[e.to] = min(a[x], e.cap - e.flow);
Q.push(e.to);
}
}
if (a[t]) break;
}
if (!a[t]) break;
for (int u = t; u != s; u = edges[p[u]].from) {
edges[p[u]].flow += a[t];
edges[p[u] ^ 1].flow -= a[t];
}
flow += a[t];
}
return flow;
}
};
int main(int argc, char** argv) {
#ifdef LOCAL
freopen("input.txt", "r", stdin);
freopen("output.txt", "w", stdout);
#endif
int N, M; cin >> N >> M;
EdmondsKarp edk;
edk.init(N + N);
int u, v;
for (int i = 1; i <= M; ++i) {
cin >> u >> v;
edk.AddEdge(u, v + N, 1);
}
for (int i = 1; i <= N; ++i) {
edk.AddEdge(0, i, 1);
edk.AddEdge(N + i, N + N + 1, 1);
}
cout << N - edk.Maxflow(0, N + N + 1) << endl;
return 0;
}
網絡流五·最大權閉合子圖
最大權閉合子圖:目前就我的理解是用來建模求解一些有“收入”以及“支出”並且求最後最大的收益類問題的。建模方法如下:
1、添加源點 s 和匯點 t 。
2、從源點 s 向 X 集合中每個點連一條容量為該點“收入”的有向邊。
3、從 Y 集合中每個點向匯點 t 連一條容量為該點“支出”的有向邊。
4、若 X 和 Y 集合中的點有依賴關系,則從 X 集合向 Y 集合每個關系添加一條容量為無限大的有向邊。
最終的結果為所有收入之和 - 最小割。
const int maxn = 1005;
const int INF = 0x7FFFFFFF;
struct Edge {
int from, to, cap, flow;
Edge(int u, int v, int c, int f) : from(u), to(v), cap(c), flow(f) {}
};
// 求最小割所用到的兩個
// 求最小割點的思路為在原來求最大流的殘留網絡上從 s 點開始 DFS,所有能遍歷到的點都是 S 集合裏面的,
// 剩余沒有遍歷到的點就是 T 集合裏的點。
// bool used[maxn];
// std::vector<int> rst;
struct EdmondsKarp {
int n, m;
vector<Edge> edges;
vector<int> G[maxn];
int a[maxn];
int p[maxn];
void init(int n) {
for (int i = 0; i < n; ++i) G[i].clear();
edges.clear();
}
void AddEdge(int from, int to, int cap) {
edges.push_back(Edge(from, to, cap, 0));
edges.push_back(Edge(to, from, 0, 0)); // reverse edge
m = edges.size();
G[from].push_back(m - 2);
G[to].push_back(m - 1);
}
int Maxflow(int s, int t) {
int flow = 0;
for (;;) {
memset(a, 0, sizeof(a));
queue<int> Q;
Q.push(s);
a[s] = INF;
while (!Q.empty()) {
int x = Q.front(); Q.pop();
for (int i = 0; i < G[x].size(); ++i) {
Edge &e = edges[G[x][i]];
if (!a[e.to] && e.cap > e.flow) {
p[e.to] = G[x][i];
a[e.to] = min(a[x], e.cap - e.flow);
Q.push(e.to);
}
}
if (a[t]) break;
}
if (!a[t]) break;
for (int u = t; u != s; u = edges[p[u]].from) {
edges[p[u]].flow += a[t];
edges[p[u] ^ 1].flow -= a[t];
}
flow += a[t];
}
return flow;
}
bool CheckMaxMatch(int N, int M) {
for (int i = 1; i <= M; ++i) {
for (int j = 0; j < G[N + i].size(); ++j) {
Edge &e = edges[G[N + i][j]];
if (e.flow != e.cap && e.flow > 0) { return false; }
}
}
return true;
}
/*
// 遍歷求網絡的最小割中的 S 集合點,結果儲存在上面的 vector<int> rst 中;
void GetMinCutSetS(int s) {
rst.push_back(s); used[s] = true;
for (int i = 0; i < G[s].size(); ++i) {
Edge &e = edges[G[s][i]];
if (!used[e.to] && e.flow != e.cap) {
GetMinCutSetS(e.to);
}
}
}
*/
};
int main(int argc, char** argv) {
#ifdef LOCAL
freopen("input.txt", "r", stdin);
freopen("output.txt", "w", stdout);
#endif
int N, M; cin >> N >> M;
int b[maxn], sum = 0;
EdmondsKarp ek;
ek.init(N + M + 2);
// 第i個數表示邀請編號為i的學生需要花費的活躍值b[i]
for (int i = 1; i <= M; ++i) cin >> b[i];
for (int i = 1; i <= N; ++i) {
int a, k, recvtmp; cin >> a >> k;
sum += a;
ek.AddEdge(0, i, a);
for (int j = 1; j <= k; ++j) {
cin >> recvtmp;
ek.AddEdge(i, recvtmp + N, INF);
}
}
for (int i = 1; i <= M; ++i) {
ek.AddEdge(i + N, N + M + 1, b[i]);
}
cout << sum - ek.Maxflow(0, N + M + 1) << endl;
return 0;
}
HihoCoder上網絡流算法題目建模總結