【洛谷】P3381 最小费用最大流
写一篇题解来加深印象。其他题解都没有说明复杂度的问题和常见易错点。本文将介绍用 vector 来存图,采用 spfa + dinic 的算法,也就是 SSP 算法。
SSP 算法
默认读者已经学会了最大流算法。现在,我们每条边加上单位流量的费用,要求在最大流的基础上满足费用最小,这就是最小费用最大流问题。
简单来说,我们每次贪心地选择费用最小的增广路增广,直到不存在增广路时终止算法。需要注意的是,图中不能有负圈,如果存在负圈,首先要用消圈算法消除负圈。存在负圈的题目这是一道有负圈的费用流模板题。
现在证明一下贪心的正确性。我们已知图中不存在负圈。假设流量为 $i$ 的流的费用为 $f_i$,我们通过增广一次最短增广路求出 $f_j$。如果存在另一种从 $f_i$ 增广一次增广路且小于 $f_j$ 的费用流 $f_k$,则该增广必然走过负圈,与已知图中不存在负圈矛盾,所以贪心是可行的。
由于极端情况下 spfa 找增广路的时间复杂度为 $O(nm)$,故 SSP 算法的复杂度为 $O(nmf)$,其中 $f$ 为网络最大流,需要注意的是,这并不是一个多项式复杂度,例如 $f = 2^{n/2}$ 时,复杂度为指数级别。
实现代码时,可能存在一些易错点。首先,用 vector 来存图,找反向边时需要添加对应编号快速寻找反向边,其次,反向边的费用应该是 $-c$,这样走反向边时才满足最短路的判断条件,正向边的条件为 $dis[v]=dis[u]+c$,反向边就是 $dis[u]=dis[v]+(-c)$。最后,跑一次 spfa 应该多次 dfs 找增广路而不是只找一次,有的题目会卡只找一次的代码。
附上代码。
#include <cstdio>
#include <iostream>
#include <vector>
#include <cstring>
#include <queue>
#define int long long//开longlong
using namespace std;
const int N = 5e3 + 5;
const int inf = 0x7fffffff;
int n, m, s, t, mincos;
struct node
{
int v, w, c, id;
};
vector<node> val[N];
int dis[N], cur[N];
bool vis[N];
bool spfa()
{
for (int i = 1; i <= n; i++)
dis[i] = inf;
queue<int> q;
q.push(s);
dis[s] = 0;
while (!q.empty())
{
int x = q.front();
q.pop();
vis[x] = false;
for (auto &e : val[x])
if (e.w && dis[e.v] > dis[x] + e.c)
{
dis[e.v] = dis[x] + e.c;
if (!vis[e.v])
q.push(e.v), vis[e.v] = true;
}
}
return dis[t] != inf;//如果存在增广路就返回true,否则返回false
}
int dfs(int x, int res)
{
if (x == t || !res)
return res;
vis[x] = true;
int used = 0;
for (int i = cur[x]; i < val[x].size(); ++i)
{
auto &e = val[x][i];
cur[x] = i;//当前弧优化
if (!vis[e.v] && e.w && dis[e.v] == dis[x] + e.c)
{
int cnt = dfs(e.v, min(res - used, e.w));//递归找流量
if (cnt)
{
e.w -= cnt;
val[e.v][e.id].w += cnt;
mincos += cnt * e.c;
used += cnt;
if (used == res)
break;//残留优化
}
}
}
vis[x] = false;
return used;
}
int dinic()
{
int ans = 0, flow;
while (spfa())
{
memset(cur, 0, sizeof(cur));
while ((flow = dfs(s, inf)))//一次spfa要找多次增广路
ans += flow;
}
return ans;
}
signed main()
{
scanf("%lld%lld%lld%lld", &n, &m, &s, &t);
for (int i = 1; i <= m; ++i)
{
int u, v, w, c;
scanf("%lld%lld%lld%lld", &u, &v, &w, &c);
val[u].push_back({v, w, c, (int)val[v].size()});//记录反向边的id
val[v].push_back({u, 0, -c, (int)val[u].size() - 1});//记录反向边的id
}
int maxflow = dinic();
printf("%lld %lld\n", maxflow, mincos);
return 0;
}
评论