[算法] A LITTLE 网络流
简介
所谓网络流,就是给了一张图,有源点和汇点,让你求从源点放水,到汇点的水最多能有多少;
这实际上是一个最大流的问题;
最大流
我们把这张图的每个边看作一条水管,每个水管都有一个容量,那么对于一条从源点到汇点的路径,其最大通过量是这些水管中容量最小的那一个的容量;
有个定理,叫最大流最小割定理;
对于这个问题,我们有如下的处理方法:
EK 算法
定义一条增广路为从源点(设为s)到汇点(设为t)的一条路径,满足其所有边的剩余容量非负;
对于此算法,我们每次都找一条增广路,然后更新每条边的权值直到剩余图中(也即残量网络)没有增广路;
因为我们要重新找增广路,所以还要建反向边,便于回去重新找;
可以看出,它的复杂度瓶颈在边数;
那么最坏情况是每次只能确定一条边,时间复杂度 $ O(nm^2) $,不太适用于稠密图,但实际使用其实达不到此上界;
因为下一个算法使用较普遍,所以这里就不放代码;
Dinic 算法
考虑对于一个残量网络,EK算法会重新遍历整个残量网络,然后只找出一条增广路,考虑将复杂度瓶颈由边转移到点;
于是有了Dinic算法;
首先对图进行分层(就是进行一边BFS,然后将遍历顺序(即深度)相同的点纳入同一层);
然后每次处理一层的所有点的增广路,直到残量网络不能分层(即s不能到达t);
我们发现,它和EK的本质区别在于它是以点为单位(每次分层是 $ \Theta(n) $ 的)找增广路,而前者是以边为单位;
时间复杂度: $ O(n^2m) $;
当然,它还有两个优化:当前弧优化和剪枝;
前者是在当前层中只去找能扩展的边,后者是去掉增广完毕的点;
点击查看代码
#include <iostream>
#include <cstdio>
#include <queue>
#include <cstring>
using namespace std;
int n, m, s, t;
struct sss{
int t, ne;
long long w;
}e[200005];
int h[200005], cnt;
void add(int u, int v, long long ww) {
e[++cnt].t = v;
e[cnt].ne = h[u];
h[u] = cnt;
e[cnt].w = ww;
}
int now[200005];
long long ans;
int dis[205];
bool bfs() { //点分层;
queue<int> q;
for (int i = 1; i <= n; i++) dis[i] = 0x3f3f3f3f;
q.push(s);
now[s] = h[s];
dis[s] = 0;
while(!q.empty()) {
int tt = q.front();
q.pop();
for (int i = h[tt]; i; i = e[i].ne) {
int u = e[i].t;
if (e[i].w > 0 && dis[u] == 0x3f3f3f3f) {
dis[u] = dis[tt] + 1;
now[u] = h[u];
q.push(u);
// if (u == t) return true;
}
}
}
if (dis[t] != 0x3f3f3f3f) return true;
else return false;
}
long long dfs(int x, long long sum) {
if (x == t) return sum;
long long k = 0; //当前最小剩余流量;
long long res = 0; //流过点x的总流量;
for (int i = now[x]; i; i = e[i].ne) {
int u = e[i].t;
now[x] = i; //当前弧优化;
if (e[i].w > 0 && (dis[u] == dis[x] + 1)) {
k = dfs(u, min(sum, e[i].w));
if (k == 0) dis[u] = 0x3f3f3f3f; //剪枝;
e[i].w -= k;
e[i ^ 1].w += k;
res += k;
sum -= k; //sum是经过该点的剩余流量;
}
}
return res;
}
void Dinic() {
while(bfs()) {
ans += dfs(s, 0x3f3f3f3f3f3f3f3f);
}
}
int main() {
ios::sync_with_stdio(0);
cin.tie(0);
cout.tie(0);
cin >> n >> m >> s >> t;
int u, v;
long long w;
cnt = 1;
for (int i = 1; i <= m; i++) {
cin >> u >> v >> w;
add(u, v, w);
add(v, u, 0);
}
Dinic();
cout << ans;
return 0;
}
费用流
这里主要研究最小费用最大流;
给每条路赋一个单位费用 $ c $ 表示每流过 $ a $ 的流时,需要花费 $ a \times c $ 的代价;
求在满足最大流的前提下,我们的最小费用是多少;
我们想到Dinic的分层操作,其实我们可以把其替换成一个最短路算法(如SPFA),算单位费用的最短路,每次找增广路时判断一下这条路是否在增广路中即可;
注意这时因为没有层数的限制,所以一个点可能被遍历多次以致死循环,用一个vis数组标记一下即可;
时间复杂度:设最大流为 $ F $,SPFA理论上界为 $ \Theta(nm) $,则时间复杂度为: $ O(nmF) $(每次只减 $ 1 $),但SPFA一般不会达到上界,且 $ F $ 一般也不会达到,所以实际要快很多;
其实网络流这里只有两种复杂度: $ \Theta (能过) $ 和 $ \Theta (不能过) $;
点击查看代码
#include <iostream>
#include <cstdio>
#include <queue>
#include <cstring>
using namespace std;
int n, m, s, t;
struct sss{
int t, ne;
long long w, cos;
}e[200005];
int h[200005], cnt;
void add(int u, int v, long long ww, long long co) {
e[++cnt].t = v;
e[cnt].ne = h[u];
h[u] = cnt;
e[cnt].w = ww;
e[cnt].cos = co;
}
int now[200005];
long long ans, an;
int dis[5005];
bool vis[5005];
bool SPFA() {
memset(dis, 0x3f, sizeof(dis));
memset(vis, 0, sizeof(vis));
now[s] = h[s];
queue<int> q;
q.push(s);
dis[s] = 0;
vis[s] = true;
while(!q.empty()) {
int tt = q.front();
q.pop();
vis[tt] = false;
for (int i = h[tt]; i; i = e[i].ne) {
int u = e[i].t;
if (e[i].w > 0 && dis[u] > dis[tt] + e[i].cos) {
dis[u] = dis[tt] + e[i].cos;
now[u] = h[u];
if (!vis[u]) {
vis[u] = true;
q.push(u);
}
}
}
}
if (dis[t] != 0x3f3f3f3f) return true;
else return false;
}
long long dfs(int x, long long sum) {
if (x == t) return sum;
long long k = 0;
long long res = 0;
vis[x] = true;
for (int i = now[x]; i; i = e[i].ne) {
int u = e[i].t;
now[x] = i;
if (!vis[u] && e[i].w > 0 && (dis[u] == dis[x] + e[i].cos)) {
k = dfs(u, min(sum, e[i].w));
if (k == 0) dis[u] = 0x3f3f3f3f;
e[i].w -= k;
e[i ^ 1].w += k;
res += k;
sum -= k;
an += e[i].cos * k;
}
}
return res;
}
void Dinic() {
while(SPFA()) {
ans += dfs(s, 0x3f3f3f3f3f3f3f3f);
}
}
int main() {
ios::sync_with_stdio(0);
cin.tie(0);
cout.tie(0);
cin >> n >> m >> s >> t;
int u, v;
long long w, co;
cnt = 1;
for (int i = 1; i <= m; i++) {
cin >> u >> v >> w >> co;
add(u, v, w, co);
add(v, u, 0, -co);
}
Dinic();
cout << ans << ' ' << an;
return 0;
}
网络流24题
主要是建模;
LOJ P6000 搭配飞行员
对于这道题,我们虚拟一个原点 $ s $,一个汇点 $ t $,然后将 $ s $ 与每个第一飞行员连一条边权为 $ 1 $ 的边,将每个第二飞行员与 $ t $ 连一条边权为 $ 1 $ 的边,将每个第一飞行员与其配对的第二飞行员连一条边权为 $ INF $ 或 $ 1 $ 的边,跑一个最大流即可;
正确性:因为每个可能的流最大流量为 $ 1 $,所以求出的最大流即为答案;
当然也可以用二分图最大匹配做;
网络流
#include <iostream>
#include <cstdio>
#include <queue>
using namespace std;
int n, m;
struct sss{
int t, ne, w;
}e[5005];
int h[5005], cnt;
void add(int u, int v, int ww) {
e[++cnt].t = v;
e[cnt].w = ww;
e[cnt].ne = h[u];
h[u] = cnt;
}
int ans;
int dis[3005], now[3005];
bool bfs() {
queue<int> q;
q.push(1);
for (int i = 1; i <= n + 2; i++) dis[i] = 0x3f3f3f3f;
dis[1] = 0;
now[1] = h[1];
while(!q.empty()) {
int t = q.front();
q.pop();
for (int i = h[t]; i; i = e[i].ne) {
int u = e[i].t;
if (e[i].w > 0 && dis[u] == 0x3f3f3f3f) {
q.push(u);
dis[u] = dis[t] + 1;
now[u] = h[u];
if (u == n + 2) return true;
}
}
}
return false;
}
int dfs(int x, int sum) {
if (x == n + 2) return sum;
int k = 0;
int res = 0;
for (int i = now[x]; i; i = e[i].ne) {
int u = e[i].t;
now[x] = i;
if (e[i].w > 0 && dis[u] == dis[x] + 1) {
k = dfs(u, min(sum, e[i].w));
res += k;
sum -= k;
e[i].w -= k;
e[i ^ 1].w += k;
}
}
return res;
}
void Dinic() {
while(bfs()) {
ans += dfs(1, 0x3f3f3f3f);
}
}
int main() {
ios::sync_with_stdio(0);
cin.tie(0);
cout.tie(0);
cin >> n >> m;
int a, b;
cnt = 1;
while(cin >> a >> b) {
add(a + 1, b + 1, 1);
add(b + 1, a + 1, 0);
}
for (int i = 1; i <= m; i++) {
add(1, i + 1, 1);
add(i + 1, 1, 0);
}
for (int i = m + 1; i <= n; i++) {
add(i + 1, n + 2, 1);
add(n + 2, i + 1, 0);
}
Dinic();
cout << ans;
return 0;
}
二分图最大匹配
#include <iostream>
#include <cstring>
using namespace std;
int n, m;
int g[1005][1005];
bool vis[1005];
int mat[1005];
bool hgy(int x) {
for (int i = 0; i <= n; i++) {
if (g[x][i] && !vis[i]) {
vis[i] = true;
if (!mat[i] || hgy(mat[i])) {
mat[i] = x;
return true;
}
}
}
return false;
}
int main() {
cin >> n >> m;
int a, b;
while(cin >> a >> b) {
g[a][b] = 1;
g[b][a] = 1;
}
int ans = 0;
for (int i = 1; i <= m; i++) {
memset(vis, 0, sizeof(vis));
if (hgy(i)) {
ans++;
}
}
cout << ans;
return 0;
}
LOJ P6001 太空飞行计划
这个题用到了最大权闭合子图转最小割的建模方法;
考虑将所有实验向其所需要的仪器连有向边,实验的点权为得到的价钱,仪器的点权为花费的价钱(负数),那么我们要找到一个点权和最大的子图 $ G(V, E) $,满足其 $ E $ 中没有指向外部的边(及 $ E $ 的两个端点都在 $ V $ 中),且 $ V $ 中没有 $ E $ 的两个端点所不含有的点,这就是最大权闭合子图,也就是我们要求的;
考虑如何转化;
我们将原点 $ s $ 与所有实验连一条以其价值为边权的有向边,将每个实验与其依赖的仪器连一条边权为 $ INF $ 的有向边,将所有仪器与汇点 $ t $ 连一条边权为其花费的绝对值的有向边,则所有实验的价值和 $ - $ 其最小割即为所求;
考虑正确性,这样减去相当于将两个集合的意义全部取反,那么答案为选的实验 $ - $ 依赖的仪器,且被减数最小而且满足要求(因为求得是最小割),所以答案正确(建议自己想想);
考虑输出路径,只要 $ Dinic $ 最后一次分层时分到了即可输出(建议自己画一画);
输入挺难受,可以看看代码;
点击查看代码
#include <iostream>
#include <cstdio>
#include <queue>
using namespace std;
int m, n;
int s, t;
struct sss{
int t, ne, w;
}e[5005];
int h[5005], cnt;
void add(int u, int v, int ww) {
e[++cnt].t = v;
e[cnt].w = ww;
e[cnt].ne = h[u];
h[u] = cnt;
}
char c;
int ans, sum;
int dis[5005], now[5005];
bool bfs() {
for (int i = s; i <= t; i++) dis[i] = 0x3f3f3f3f;
dis[s] = 0;
now[s] = h[s];
queue<int> q;
q.push(s);
while(!q.empty()) {
int tt = q.front();
q.pop();
for (int i = h[tt]; i; i = e[i].ne) {
int u = e[i].t;
if (e[i].w > 0 && dis[u] == 0x3f3f3f3f) {
dis[u] = dis[tt] + 1;
now[u] = h[u];
q.push(u);
}
}
}
if (dis[t] == 0x3f3f3f3f) return false;
else return true;
}
int dfs(int x, int sum) {
if (x == t) return sum;
int k = 0;
int res = 0;
for (int i = now[x]; i; i = e[i].ne) {
int u = e[i].t;
now[x] = i;
if (e[i].w > 0 && dis[u] == dis[x] + 1) {
k = dfs(u, min(e[i].w, sum));
res += k;
sum -= k;
e[i].w -= k;
e[i ^ 1].w += k;
}
}
return res;
}
void Dinic() {
while(bfs()) {
ans += dfs(s, 0x3f3f3f3f);
}
}
int main() {
scanf("%d %d", &m, &n);
s = 1;
t = n + m + 2;
int x, a;
cnt = 1;
for (int i = 1; i <= m; i++) {
scanf("%d", &x);
sum += x;
add(s, i + 1, x);
add(i + 1, s, 0);
while(1) {
do {
c = getchar();
} while(c == ' ');
ungetc(c, stdin); //回退操作,将 c 退回到标准输入流;
if (c == '\n' || c == '\r') break;
scanf("%d", &a);
add(i + 1, m + 1 + a, 0x3f3f3f3f);
add(m + 1 + a, i + 1, 0);
}
}
for (int i = 1; i <= n; i++) {
scanf("%d", &x);
add(i + m + 1, t, x);
add(t, i + m + 1, 0);
}
Dinic();
for (int i = 2; i <= m + 1; i++) {
if (dis[i] != 0x3f3f3f3f) printf("%d ", i - 1);
}
printf("\n");
for (int i = m + 2; i <= n + m + 1; i++) {
if (dis[i] != 0x3f3f3f3f) printf("%d ", i - m - 1);
}
printf("\n");
printf("%d", sum - ans);
return 0;
}