网络流入门
给出一个源点\(s\), 现在有\(inf\)单位的水,中间经过一大堆水管和一大堆点到达一个汇点\(t\),每个水管都有流量限制,求最多有多少水能流到\(t\)。能流到t的水量就是最大流。
像上面这张图,它的最大流就是\(22 + 10 + 45 = 77\)。(这图是盗的)
那么如何用程序的方法来实现最大流呢?
Edmond-Karp算法(EK)
最大流
这是一种时间复杂度比较不优秀的算法。具体等会再说。
现在我们定义增广路:可以从\(s\)流到\(t\)的一条可行路径。
增广:给一条增广路增流。
于是我们想:可不可以用一些玄妙的算法,每次找到一条增广路进行增广?
这就是EK算法的核心。我们每次从源点进行bfs找向汇点,找到一条增广路之后记录这条增广路上流量限制最小的边,然后把这整条增广路的边权全部扣去这个值。
bool /*SPFA*/dijstra(){
memset(pre, 0, sizeof(pre));
memset(vis, 0, sizeof(vis));
queue<int> q;
q.push(s);
vis[s] = 1;
while(!q.empty()){
int u = q.front();
q.pop();
for(int i = dt[u]; i; i = e[i].next){
int v = e[i].to;
if(e[i].val != 0 && !vis[v]){
pre[v].fa = u;
pre[v].num = i;
if(v == t)return 1;
vis[v] = 1;
q.push(v);
}
}
}
return 0;
}
如上面的代码。我们用\(pre\)数组来记录增广路。当这条边的边权\(>0\)的时候就意味着可以进行增广。
别忘记用一个\(vis\)数组来记录这条边有没有被重复遍历。
void EK(){
while(dijstra()){
mi = 0x3f3f3f3f;
for(int i = t; i != s; i = pre[i].fa)mi = min(mi, e[pre[i].num].val);
for(int i = t; i != s; i = pre[i].fa){
e[pre[i].num].val -= mi;
e[pre[i].num ^ 1].val += mi;
}ans += mi;
}
}
最后递归遍历一下,进行增广。
这里有些奇奇怪怪的东西:为什么要\(e_{pre_{i}.num \ xor \ 1}.val += mi\) ?
这里隆重介绍一个小技巧:反向边
大体来说,就是给了一条流反悔的机会。
(具体我也不是很清楚,反正每次照打就是了)
正向边扣流量的同时反向边要加上同样的流量。
反向边的编号是正向边的编号\(xor \ 1\)。
struct dartou{
int to, val, next;
}e[N];
struct dat{
int fa, num;
}pre[N];
bool vis[N];
int dt[N], s, t;
void add(int x, int y, int value){
cnt++;
e[cnt].to = y;
e[cnt].val = value;
e[cnt].next = dt[x];
dt[x] = cnt;
}
bool /*SPFA*/dijstra(){
memset(pre, 0, sizeof(pre));
memset(vis, 0, sizeof(vis));
queue<int> q;
q.push(s);
vis[s] = 1;
while(!q.empty()){
int u = q.front();
q.pop();
for(int i = dt[u]; i; i = e[i].next){
int v = e[i].to;
if(e[i].val != 0 && !vis[v]){
pre[v].fa = u;
pre[v].num = i;
if(v == t)return 1;
vis[v] = 1;
q.push(v);
}
}
}
return 0;
}
void EK(){
while(dijstra()){
mi = 0x3f3f3f3f;
for(int i = t; i != s; i = pre[i].fa)mi = min(mi, e[pre[i].num].val);
for(int i = t; i != s; i = pre[i].fa){
connect[pre[i].fa] = i;
e[pre[i].num].val -= mi;
e[pre[i].num ^ 1].val += mi;
}ans += mi;
}
}
int main(){
//主体加入处
}
此处建议在add函数处加入以下代码:
cnt++;
e[cnt].to = x;
e[cnt].next = dt[y];
dt[y] = cnt;
这样子就可以正反向边一起建。
EK这就讲完了。时间复杂度\(O(nm^2)\)。这里n是点数,m是边数。
例题1 【Luogu P3376】 网络最大流
EK板子。
这部分就结束了。
最小费用最大流
每条边除了有流量限制以外还有一个\(w\),指流过每一个单位流量所需的费用。
现在问在达到流量最大的前提下,最小费用是多少。
一样找增广路。
只不过这次找的是所需费用最小的增广路。
然后呢?
把bfs改成spfa就行了!虽然它已经死了
bool /*SPFA*/dijstra(){
memset(pre, 0, sizeof(pre));
memset(vis, 0, sizeof(vis));
memset(dis, 0x3f, sizeof(dis));
queue<int> q;
q.push(s);
vis[s] = 1;
dis[s] = 0;
while(!q.empty()){
register int u = q.front();
vis[u] = 0;
q.pop();
for(register int i = dt[u]; i; i = e[i].next){
register int v = e[i].to, w = e[i].w;
if(e[i].val > 0 && dis[v] > dis[u] + w){
dis[v] = dis[u] + w;
pre[v].fa = u;
pre[v].num = i;
if(!vis[v]){
q.push(v);
vis[v] = 1;
}
}
}
}
return dis[t] != 0x3f3f3f3f;
}
void EK(){
while(dijstra()){
mi = 0x3f3f3f3f;
for(int i = t; i != s; i = pre[i].fa)mi = min(mi, e[pre[i].num].val);
for(int i = t; i != s; i = pre[i].fa){
e[pre[i].num].val -= mi;
e[pre[i].num ^ 1].val += mi;
}
maxflow += mi;
cost += mi * dis[t];
}
}
需要注意的是:反向边的费用是正向边的费用的相反数。
很好理解。如果这一段反悔不流了,就要把花的钱退掉。
例题2【Luogu P3381】【模板】最小费用最大流
跑板子。
下面介绍一种更为高效的算法:
Dinic算法
时间复杂度\(O(n^2m)\)。对于稠密图要比EK快很多。
二分图匹配的复杂度则是\(O(sqrt(n)m)\)。(全是抄的)
总之,Dinic很快,比EK快得多。
所以尽量不要打EK。会被卡上天。那还讲它干嘛
基本步骤
1、bfs标号。
2、dfs增广。
3、反复运行。
最大流
如下图:
(用sketchbook画图真难受)
拿这张标完号的图作为例子。
我们利用标号找增广路。一次可以找到三条:
\(s -> 1 -> t\)
\(s -> 2 -> t\)
\(s -> 3 -> t\)
相对于EK每一次只能找到一条增广路来说,dinic效率实在要高很多。
而且我们用dinic还能排除一些不必要的鬼畜路径,比如:
\[ s -> 2 -> 3 -> t \]
这条路径明显较长。
所以,dinic找的是最短增广路。\(\forall u,v \text{}且u -> v\),有\(dep[v] = dep[u] + 1\)时,\(u -> v\)这条路径在一条最短增广路上。
dfs每一次沿着最短增广路进行增广。
再举一个极端例子。
(这图又是偷的。画图很麻烦啊……)
有了标号,就不会兜一大圈了。
bool bfs(){
memset(dep, 0x3f, sizeof(dep));
memset(vis, 0, sizeof(vis));
for(int i = 1; i <= N; i++)cur[i] = head[i];
queue<int> q;
vis[s] = 1;
dep[s] = 0;
q.push(s);
while(!q.empty()){
register int u = q.front();
q.pop();
for(int i = head[u]; i; i = e[i].next){
int v = e[i].to;
if(dep[v] > dep[u] + 1 && e[i].f){
dep[v] = dep[u] + 1;
if(!vis[v])
q.push(v);
vis[v] = 1;
}
}
}
return dep[t] != 0x3f3f3f3f;
}
int dfs(int now, int low){
int flow = 0;
if(now == t)return low;
for(int i = cur[now]; i; i = e[i].next){
cur[now] = i;
int v = e[i].to;
if(e[i].f && dep[v] == dep[now] + 1){
if(flow = dfs(v, min(low, e[i].f))){
e[i].f -= flow;
e[i ^ 1].f += flow;
return flow;
}
}
}
return 0;
}
最小费用最大流
同样地,dinic也可以运用到最小费用最大流上。
先跑一遍spfa。虽然它已经死了
然后在dfs中累加费用。
举一反三一下,\(\forall u,v\) \(u -> v\),若\(dis[v] = dis[u] + e[i].w\),则\(u -> v\)这条路径在最短路上。
原样增广就完事了。
bool /*SPFA*/dijstra(){
memset(vis, 0, sizeof(vis));
memset(dis, 0x3f, sizeof(dis));
for(int i = 0; i <= N; i++)cur[i] = dt[i];
queue<int> q;
q.push(s);
vis[s] = 1;
dis[s] = 0;
while(!q.empty()){
int u = q.front();
vis[u] = 0;
q.pop();
for(int i = dt[u]; i; i = e[i].next){
int v = e[i].to, w = e[i].w;
if(e[i].val > 0 && dis[v] > dis[u] + w){
dis[v] = dis[u] + w;
//cout << v << " " << pre[v].fa << endl;
if(!vis[v]){
q.push(v);
vis[v] = 1;
}
}
}
}
return dis[t] < 0x3f3f3f3f;
}
int dfs(int now, int low){
if(now == t) return low;
int flow = 0;
vis[now] = 1;
for(int i = cur[now]; i; i = e[i].next){
cur[now] = i;
int v = e[i].to;
if(!vis[v] && e[i].val && dis[v] == dis[now] + e[i].w){
if(flow = dfs(v, min(low, e[i].val))){
e[i].val -= flow;
e[i ^ 1].val += flow;
cost += e[i].w * flow;
return flow;
}
}
}
return 0;
}
int dinic(){
int minflow, maxflow = 0;
while(dijstra()){
while(minflow = dfs(s, inf)){
memset(vis, 0, sizeof(vis));
maxflow += minflow;
}
}
return maxflow;
}
记得给图打上vis标记。如果费用为0,就会两个点来回跳,被卡死。感性理解下?
每次照打就是了。没有例题。可以再把板子写一遍。
引用
2.[洛谷日报#56 EK不够快?再学个Dinic吧](