网络流:
定义:
网络流,指的是在一个每条边都有容量的有向图分配流,使得一条边的流量不会超过他的容量
有向图称为网络,顶点称为结点,边称为弧
源点:只出不进的顶点
汇点:只进不出的顶点
容量和流量:每条有向边上有两个两,容量和流量
相关性质:
容量限制:对于所有顶点u,v,满足f(u,v)<=c(u,v) ( f:流量,c:容量)
流守恒性:对于所有顶点u,满足流入等于流出
反对称性:对所有顶点u,v满足f(u,v)=-f(v,u)
最大流:
可行流:当前有一条路,所有边上的流量没有超过其容量,那那么我们将这个流称为一个可行流
增广路:假设我们找到这样一条路,从源点连向汇点,且每条边的流量都严格小于其容量,那么我们一定可以给这条路增加一个delta流量,并使其增加以后仍然为可行流
基本思路:我们在网络中不断的寻找增广路,并对其进行流量增量增广delta,(也就是使这条路上的每条边的容量减少delta,并在总流量中增加delta),直到找不到增广路为止,我们就可以找到最大流
反悔机制:
我们在找寻增广路增广的同时,有可能会堵死别的更好的增广路,这个时候怎么办呢?
我们在建立网络时,给每条边(u,v)都建立一条反向边(v,u),且c(v,u)=0,我们在每次增广的时候,把路上的每一条边的容量减少delta时,同时也把其反向边的容量增加delta,这样一来,当我们下次找寻增广路时,就可以通过反向边退流,从而达到反悔的效果
对于一个n节点m边的网络求最大流:
题目链接:P3376 【模板】网络最大流 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)
两种算法:ek算法和dinic算法
ek算法:
时间复杂度:
思路:基于bfs找寻增广路,找到增广路后进行增广delta,直到源点与汇点不连通
实现代码如下:
#include <iostream>
#include <cstdio>
#include <cstring>
#include <queue>
using namespace std;
#define ll long long
const int N = 210, M = 5010 << 1;
int n, m, s, t;
int h[N], tto[M], nxt[M], tot, pre[N];
ll ww[M], mf[N];
//h,tto,nxt:链式前向星
//ww[i]:边i的容量
//mf[v]:节点v的当前流量
//pre[v]:结点v的前驱边(指向增广路起点方向的)
void add(int a, int b, int c) {
tto[++tot] = b;
nxt[tot] = h[a];
ww[tot] = c;
h[a] = tot;
}
bool bfs() {
memset(mf, 0, sizeof(mf));
queue<int> q;
q.push(s); mf[s] = 1ll << 33;
int u;
while (!q.empty()) {
u = q.front(); q.pop();
//求增广路
for (int i = h[u], v; v = tto[i]; i = nxt[i]) {
//mf[v]==0,意味着没有被访问过,ww[i]大于0,意味着可流
if (mf[v] == 0 && ww[i]) {
mf[v] = min(mf[u], ww[i]);
pre[v] = i;
q.push(v);
if (v == t) return true;
}
}
}
return false;
}
ll EK() {
ll flow = 0;
while (bfs()) {
int v = t;
//从t开始,给正向边减流,反向边加流
while (v != s) {
//i为v的前驱边
int i = pre[v];
//给前驱边减流
ww[i] -= mf[t];
//给反向边加流
ww[i ^ 1] += mf[t];
//迭代到前驱变指向的结点
v = tto[i ^ 1];
}
flow += mf[t];
}
return flow;
}
int main() {
tot = 1;
scanf("%d%d%d%d", &n, &m, &s, &t);
for (int i = 1, a, b, c; i <= m; i++) {
scanf("%d%d%d", &a, &b, &c);
add(a, b, c);
add(b, a, 0);//反向边
}
printf("%lld\n", EK());
return 0;
}
dinic算法:
时间复杂度:
基于bfs分层优化的dfs:
先用bfs对网络进行分层,找到多条增广路
再用dfs走每条增广路,进行流量增广
其中bfs的分层限制了搜索深度
当前弧优化剪枝:(走完当前弧后,下一个dfs遍历到该结点一定不用再走这条弧,因此可以直接舍去剪枝)
剩余流量优化剪枝:当该条增广路可流流量为0时,不再搜索
残枝优化剪枝:当这个节点指向的所有增广路都不可流时,将该节点踢出分层图,以防被下一个dfs访问
最后dinic进行多次bfs,每次bfs完后都进行dfs增广,直到找不到增广路为止
实现代码如下:
#include <iostream>
#include <cstdio>
#include <algorithm>
#include <queue>
#include <cstring>
using namespace std;
const int N=10010,M=200010;
int n,m,s,t;
#define LL long long
int h[N],nxt[M],tto[M],tot,d[N],cur[N];
LL ww[M];
//d[v]:结点v在分层图的深度
//cur[u]:当前弧
void add(int a,int b,int c){
tto[++tot]=b;
ww[tot]=c;
nxt[tot]=h[a];
h[a]=tot;
}
bool bfs(){
memset(d,0,sizeof(d));
queue<int> q;
q.push(s);d[s]=1;
int u;
while(!q.empty()){
u=q.front();q.pop();
for(int i=h[u],v;v=tto[i];i=nxt[i]){
//当该节点未分层且可流时,其有可能是增广路
if(!d[v]&&ww[i]){
d[v]=d[u]+1;
q.push(v);
if(v==t) return true;
}
}
}
return false;
}
//u:当前访问节点
//mf:结点u的剩余流量
//函数返回值:结点u可以流出去的流量
LL dfs(int u,LL mf){
//已经搜索到了汇点
if(u==t) return mf;
LL sum=0;
for(int i=cur[u],v;v=tto[i];i=nxt[i]){
//当前弧优化
cur[u]=i;
//如果是下一层的,且可流
if(d[v]==d[u]+1&&ww[i]){
//可流向v的流量
LL f=dfs(v,min(mf,ww[i]));
ww[i]-=f;
ww[i^1]+=f;
//累加可流出流量
sum+=f;
//减少u的剩余流量
mf-=f;
//剩余流量优化
if(mf==0) break;
}
}
//残枝优化
if(!sum) d[u]=0;
return sum;
}
LL dinic(){
LL flow=0;
while(bfs()){
memcpy(cur,h,sizeof(h));
flow+=dfs(s,1ll<<33);
}
return flow;
}
int main(){
tot=1;
scanf("%d%d%d%d",&n,&m,&s,&t);
for(int i=1,a,b,c;i<=m;i++){
scanf("%d%d%d",&a,&b,&c);
add(a,b,c); add(b,a,0);
}
printf("%lld",dinic());
return 0;
}
最小割问题:
定义割掉每条边的代价等于该条边的容量大小,要求割掉任意多条边,使得源点s流向汇点t的流量为0的情况下,使代价总和最小,这被称为最小割问题。
题目本质:
当一个图被分割为两个部分时,不再存在源点s到汇点t的通路,那么我们就要割掉原本每条通路上容量最小的边,而在最大流问题中,每条通路的流量恰好又是取决于其容量最小的边,因此,最小割的代价总和一定等于最大流,即最小割==最大流,此为最大流最小割定理
三种基本问题:
1.求最小割:即求最大流
2.求最小割的划分:
我们先求出最大流,然后从源点开始对残留网进行bfs,只有容量大于0的边可以通过,那么我们标记能够到达的点,则标记的点构成s集合,未标记的点构成t集合,此划分就是最小割的其中一个划分
3.求最小割的最少边数:
题目链接:P1344 [USACO4.4] 追查坏牛奶 Pollutant Control - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)
基本思路:我们求出最小割的时候,由最小割等于最大流,最小割一定等于,其中ci为被割掉的边的容量,假设有m条边,我们只需将每条边的容量修改为c*k+1,其中k>m,那么这样新跑出来的最小割res就会等于,其中n<=m,因此n<k,则n就代表了我们割的边数,且是最少边数(因为我们每次都选取了每条通路容量最小的边来割),因此答案就是res mod k
实现代码:
#include <cstdio>
#include <iostream>
#include <cstring>
#include <queue>
using namespace std;
#define LL long long
const int N = 35, M = 1010 << 1,mod=1001;
int h[N], nxt[M], tto[M], tot, d[N], cur[N];
LL ww[M];
void add(int a, int b, LL c) {
tto[++tot] = b;
ww[tot] = c;
nxt[tot] = h[a];
h[a] = tot;
}
int n, m;
bool bfs() {
memset(d, 0, sizeof(d));
int u;
queue<int> q;
q.push(1); d[1] = 1;
while (!q.empty()) {
u = q.front(); q.pop();
for (int i = h[u], v; v = tto[i]; i = nxt[i]) {
if (!d[v] && ww[i]) {
d[v] = d[u] + 1;
q.push(v);
if (v == n) return true;
}
}
}
return false;
}
LL dfs(int u, LL mf) {
if (u == n) return mf;
LL sum = 0;
for (int i = cur[u], v; v = tto[i]; i = nxt[i]) {
cur[u] = i;
if (d[v] == d[u] + 1 && ww[i]) {
LL f = dfs(v, min(mf, ww[i]));
ww[i] -= f;
ww[i ^ 1] += f;
sum += f;
mf -= f;
if (mf == 0) break;
}
}
if (!sum) d[u] = 0;
return sum;
}
LL dinic() {
LL flow = 0;
while (bfs()) {
memcpy(cur, h, sizeof(h));
flow += dfs(1, 1ll << 35);
}
return flow;
}
int main() {
scanf("%d%d", &n, &m);
tot = 1;
int a,b;LL c;
for (int i = 1; i <= m; i++) {
scanf("%d%d%lld", &a, &b, &c);
c=c*mod+1;
add(a, b, c);
add(b, a, 0);
}
LL ans = dinic();
LL res=ans/mod;
LL res2=ans%mod;
printf("%lld %lld", res, res2);
return 0;
}
费用流问题:
费用流与网络流相比,每条边增加了一个单位费用cst,当该边流过流量f时,会产生f*p的费用代价
最小费最大流问题:在最大流的前提下使总费用最小
思路:
由于我们每对一条增广路进行增广delta时,delta会在这条路的每条边都产生收费,则总费用就是,因此,我们希望每次找增广路的时候,都优先找cst总和最小的路来进行增广,这样就可以贪心地使费用最小。
那么我们就明白了,这就是找最短路,因此,我们在ek算法中,用spfa来代替bfs,我们每次都找能够增广的最短路来进行增广即可
实现代码:
#include <iostream>
#include <cstdio>
#include <cstring>
#include <queue>
using namespace std;
#define LL long long
const int N = 5E3 + 10, M = 5E4 + 10;
int n, m, s, t, flow, cost;
int h[N], tto[M << 1], ww[M << 1], cst[M << 1], nxt[M << 1], tot;
int pre[N], mf[N], dis[N], vis[N];
void add(int a, int b, int c, int d) {
tto[++tot] = b;
nxt[tot] = h[a];
ww[tot] = c;
cst[tot] = d;
h[a] = tot;
}
bool spfa() {
memset(mf, 0, sizeof(mf));
memset(dis, 0x3f, sizeof(dis));
queue<int> q;
q.push(s); mf[s] = 1 << 29; dis[s] = 0; vis[s] = true;
int u;
while (!q.empty()) {
u = q.front(); q.pop(); vis[u] = false;
for (int i = h[u], v; v = tto[i]; i = nxt[i]) {
//松弛操作
if (dis[v] > dis[u] + cst[i]&& ww[i]) {
dis[v] = dis[u] + cst[i];
pre[v] = i;
mf[v] = min(mf[u], ww[i]);
if(!vis[v]){
q.push(v); vis[v] = true;
}
}
}
}
return mf[t];
}
void ek() {
while (spfa()) {
int v = t, i;
while (v != s) {
i = pre[v];
ww[i] -= mf[t];
ww[i ^ 1] += mf[t];
v = tto[i ^ 1];
}
flow += mf[t];
cost += mf[t] * dis[t];
}
}
int main() {
scanf("%d%d%d%d", &n, &m, &s, &t);
tot = 1;
for (int i = 1, a, b, c, d; i <= m; i++) {
scanf("%d%d%d%d", &a, &b, &c, &d);
add(a, b, c, d);
add(b, a, 0, -d);
}
ek();
printf("%d %d", flow, cost);
return 0;
}