huayucaiji浅谈网络流

浅谈网络流

前言

网络流算法是一个基本上只用记住模板的算法,但是其精髓就在于如何 记忆模板 建立模型,来描述当前的问题。这是网络流的一大难点,也是其最然人着迷的地方,接下来就让我们来康康如何解决网络流问题吧!

什么是网络流?

具体来讲网络流是一个就是一个带权有向图 G = ( V , E ) G=(V,E) G=(V,E), 而且对于每一条边 ( u , v ) ∈ E (u,v)\in E (u,v)E 都有一个流量 w ( u , v ) w(u,v) w(u,v) ,而且在这张图中还有两个特殊的点 s , t s,t s,t,也就是所谓的源点和汇点,而这连个点保证 s , t ∈ V , s ≠ t s,t\in V,s\neq t s,tV,s=t。网络流算法就是解决计算这两个点之间,满足一系列要求的流量大小。

网络流大体上分为这几个部分:最大流,最小割,二分图匹配,费用流,下面我们来逐一讲述这四个算法。

最大流

最大流所求解得的就是在源点和汇点之间的最大流量。最大流有许多算法,我们只详细讲述Dinic算法,Dinic算法也是我认为最大流算法中最有用的算法了。

Dinic算法

首先我们要了解一些基本概念:

残量网络

对于每一条边我们定义 f ( u , v ) f(u,v) f(u,v) 为目前流过了 ( u , v ) (u,v) (u,v) 这条边的流量,这条边还剩余的流量为 w ′ ( u , v ) = w ( u , v ) − f ( u , v ) w'(u,v)=w(u,v)-f(u,v) w(u,v)=w(u,v)f(u,v) w ′ ( u , v ) w'(u,v) w(u,v) V V V 组成的网络 G ′ G' G 即为残量网络,即为 G ′ = ( V , ( u , v ) ∈ E , w ′ ( u , v ) > 0 ) G'=(V,{(u,v)\in E,w'(u,v)>0}) G=(V,(u,v)E,w(u,v)>0)
注意:特别的,原图也是一张残量网络(即 ( u , v ) ∈ G , f ( u , v ) = 0 (u,v)\in G, f(u,v)=0 (u,v)G,f(u,v)=0)

增广路

我们先来放一张图:

最大流1

我们还没有解释什么叫做增广路,增广路 顾名思义 就是在一张残量网络上一条从源点到汇点的路径,比如说在这幅图中:
S − > 1 − > 2 − > T S->1->2->T S>1>2>T 流过流量为 2 2 2 的流
S − > 3 − > 4 − > T S->3->4->T S>3>4>T 流过流量为 2 2 2 的流
S − > 3 − > 2 − > T S->3->2->T S>3>2>T 流过流量为 1 1 1 的流
这样总流量就为 2 + 2 + 1 = 5 2+2+1=5 2+2+1=5 的流量,也就是本图中的最大流

如果想要更好地体会这个过程,请看这里:戳我

了解了这些概念,我们就可以来了解Dinic算法啦!

算法

我们还是看上面这张图,我们首先进行BFS对残量网络进行分层,对于每个点,都有一个 l e v e l level level 值,记录的就是分层后的结果,对于这张图来说,分层结果就是这样的(每个节点的 l e v e l level level 值在节点旁的括号中):

level

我们来模拟一下这个过程:

  1. S S S 入队,由于是源点, l e v e l [ S ] = 0 level[S]=0 level[S]=0
  2. S S S 可找到 1 , 3 1,3 1,3 l e v e l [ 1 ] = l e v e l [ 3 ] = l e v e l [ S ] + 1 = 1 level[1]=level[3]=level[S]+1=1 level[1]=level[3]=level[S]+1=1 1 , 3 1,3 1,3 入队列, S S S 出队;
  3. 1 , 3 1,3 1,3 可找到 2 , 4 2,4 2,4 l e v e l [ 2 ] = l e v e l [ 4 ] = l e v e l [ 1 ] + 1 = 2 level[2]=level[4]=level[1]+1=2 level[2]=level[4]=level[1]+1=2 2 , 4 2,4 2,4 入队列, 1 , 3 1,3 1,3 出队;
  4. 2 , 4 2,4 2,4 可找到 T T T l e v e l [ T ] = l e v e l [ 4 ] + 1 = 3 level[T]=level[4]+1=3 level[T]=level[4]+1=3 T T T 入队列, 2 , 4 2,4 2,4 出队;
  5. T T T 可找到程序结束的曙光, T T T 出队;

如果找到了一个节点 v v v,而 l e v e l [ v ] level[v] level[v] 已经有值了,那么就不再入队列, l e v e l [ v ] level[v] level[v] 的值也不必再更新。

这就是BFS的全过程啦!是不是很简单好理解?

接下来我们来看DFS,DFS的作用就是来找到增广路,然后计算结果。如何找增广路呢?我们要利用跟之前的分层结果,只有当 l e v e l [ v ] = l e v e l [ u ] + 1 level[v]=level[u]+1 level[v]=level[u]+1 时,我们才会从 u u u DFS道 v v v。这样保证我们找到的增广路是最短的。

但是,这一定是最优解吗?我们还是看这张图,如果我们用DFS找到的第一条增广路是 S − > 3 − > 2 − > T S->3->2->T S>3>2>T ,流量为 3 3 3,我们接下来不管怎么找都无法与我们之前提到过的答案 5 5 5 大,所以这个算法的正确性是靠运气的,但是我们在考场上我们就需要一个正确性为 100 % 100\% 100% 的算法才行所以呢,我们就要研究一个方法,让这个算法变得正确。

我们发现,这个算法最大的问题就是在于如果一旦走错一步,那么我们就几乎无可救药了,所以我们要增添一个“反悔”操作,一旦我们走错了某一步,我们还有机会走回来。那么我们如何实现这个功能呢?我们的方法是建反向边。所有的反向边一开始流量都为 0 0 0,然后每在残量网络中找到一条增广路,从增广路里的所有边的流量扣去这条增广路上的最大流量 c m a x c_{max} cmax,所有这条增广路上的边的反向边的流量要加上 c m a x c_{max} cmax。注意,原图的反向边也可以进入残量网络。还是这张图,我们把原图的边的流量标在"/“前面,反向边的流量标在”/"后面,如下图:

anti-label

如果我们再次碰到上面的坏运气情况,我们找到了一条增广路 S − > 3 − > 2 − > T S->3->2->T S>3>2>T,按照上面的操作,我们可以得到下面这张图(注意,图中 l e v e l level level 有变化,因为找到这条增广路后图中没有增广路了,所以要重新做BFS):

anti-label1

然后再做DFS时也只能找到一条增广路 S − > 1 − > 2 − > 3 − > 4 − > T S->1->2->3->4->T S>1>2>3>4>T,最大流量为 2 2 2,然后我们会再得到一张图:

anti-label2

我们发现 l e v e l [ T ] = − 1 level[T]=-1 level[T]=1 已经没有增广路了,这样,程序结束,得到答案 3 + 2 = 5 3+2=5 3+2=5 是正确的。

为什么呢,其实反向边就是我们所说的“反悔”操作,如果我们找到的增广路里出现了原图的反向边,就相当于把之前从这里流过的流量退回去,知道已经无法“反悔”为止,也就是我们找到了最优解。真是妙哉!

代码

具体代码中的细节我在注释里有讲,可以康康哦。

#include<bits/stdc++.h>
using namespace std;

const int maxn=1e4+10;

struct edge {
	int v,w,next;
}e[maxn*20]; 

int n,m,cnt,h[maxn];

void addedge(int u,int v,int w) {
	e[cnt].v=v;
	e[cnt].w=w;
	e[cnt].next=h[u];
	h[u]=cnt++;
}//链式前向星不多说了,不会的自己百度

int level[maxn],s,t;

bool bfs() {
	memset(level,-1,sizeof(level));
	
	queue<int> q;
	q.push(s);
	level[s]=0;
	
	while(!q.empty()) {
		int u=q.front();
		q.pop();
		
		for(int i=h[u];~i;i=e[i].next) {
			int v=e[i].v;
			
			if(e[i].w>0&&level[v]==-1) {//保证我们找到的边在残量网络里而且v未被更新过
				level[v]=level[u]+1;
				q.push(v);
			}
		}
	}
	
	return level[t]!=-1;//如果level[t]=-1,则没有增广路了
}

int dfs(int u,int maxflow) {//maxflow记录的是当前这条增广路上可流过最大的流量。
	if(u==t) {
		return maxflow;//如果我们到了汇点,则找到了一条增广路。
	}
	
	int sum=0;//sum记录当前答案
	for(int i=h[u];~i;i=e[i].next) {//~i与i!=-1的效果一样。
		int v=e[i].v;
		
		if(e[i].w>0&&level[v]==level[u]+1) {//保证我们找到的边在残量网络里且满足条件。
			int flow=dfs(v,min(maxflow,e[i].w));//flow就是我们找到的增广路的最大流量
			
			if(flow>0) {
				e[i].w-=flow;
				e[i^1].w+=flow;//e[i^1]就是e[i]的反向边,原因可以自己上网查,这里不再赘述
				maxflow-=flow;//这条增广路上的的流量要减去flow
				sum+=flow;
				if(maxflow==0) {
					break;//如果最大可行流量为0,那么就结束程序
				}
			}
		}
		
		
	}
	return sum;
}

int main()
{
	
	cin>>n>>m>>s>>t;
	fill(h,h+n+1,-1);
	for(int i=1;i<=m;i++) {
		int a,b,c;
		scanf("%d%d%d",&a,&b,&c);
		addedge(a,b,c);
		addedge(b,a,0);//反向边流量一开始为0
	}
	
	long long ans=0;
	while(bfs()) {//找到了在目前残量网络的增广路后,重新BFS来分层
		ans+=dfs(s,2e9+10);
	}
	cout<<ans<<endl;
	
	return 0;
}

应用

我们之前说过网络流的精髓在于如何对问题进行建模,下面的二分图匹配和最小割就是最大流的应用

二分图匹配

对于一张图 G = ( V , E ) G=(V,E) G=(V,E) 来说,如果我们把点集 V V V 分成两个无交集的集合 V 1 , V 2 V_1,V_2 V1,V2,对于所有的 ( u , v ) ∈ E (u,v)\in E (u,v)E u , v u,v u,v 不能同时属于 V 1 , V 2 V_1,V_2 V1,V2 中的任何一个集合,这样的图,叫二分图,比如说下图:

erfen

这里 V 1 = { 1 , 2 , 3 , 4 } , V 2 = { 5 , 6 , 7 } V_1=\left\{1,2,3,4\right\},V_2=\left\{5,6,7\right\} V1={1,2,3,4},V2={5,6,7}。这样是不是对上面的表达有了更好的理解呢?我们下面再来看一个反面例子:

!erfen

无论如何分点集 V V V,都无法满足上述的概念,所以这不是二分图。

那么如何判断二分图呢?我们可以运用匈牙利算法,这里不展开说啦,因为对于二分图我们只要知道概念即可。

圆桌聚餐问题

我们了解了二分图后就可以来看一看题目啦,题目大意我就不概括了。

首先我们很容易想到人与人之间没有关系,桌与桌之间也没有关系,所以能够想到二分图,那么对于样例,我们可以想到这样一个二分图(左边的点代表人,右边代表桌子):

yuanzhuo1

没错,每张桌子和每个人之间都有一条联线,但这有什么用呢?现在我们把源点和汇点加上,桌与人之间的边权为 1 1 1,人与源点之间边权为这个企业的人数,而桌与汇点的边权为桌课容纳的人数量。

yuanzhuo2

这个图很复杂,然而我们现在要干什么呢。我们要求一遍最大流,然后如果与企业人数和相等,则可以安排,输出 1 1 1,然后下面的自己贪心搞一搞就好了,否则输出 0 0 0.

为什么这么做?下面我们来玄学地理解一下这张图,中间的人与桌的连线边权为 1 1 1 是因为题目中要求“为了使代表们充分交流,希望从同一个单位来的代表不在同一个餐桌就餐”,所以流量为 1 1 1。然而这样求最大流的目的就是为了把所有的人“流到桌子里去”,然而桌子的接受量有限,所以人不能无限制地“流”。如果所有的人“都流掉了”,那么代表是可以把人们安排到桌子去同时也满足要求。

是不是很妙啊?有米有体会到网络流的邪恶乐趣呢?这个方法叫做“类”最大匹配,这不是真正的最大匹配,那什么是真正的最大匹配呢?它的定义是:对于二分图 G G G,他的一个满足条件的最大子图 T T T 的边的数量就是最大匹配,满足的要求就是在 T T T 中,任意两条边没有交点。最大匹配也可以用网络流来解决。这里不再赘述(作者也讲不动了QAQ)

代码:

#include<bits/stdc++.h>
using namespace std;

const int maxn=9e4+10,INF=0x7fffffff;
int cnt,h[500],n,m,level[500],s,t;

struct edge
{
    int v,w,next;
}e[maxn];

void addedge(int u,int v,int w)
{
    e[cnt].v=v;
    e[cnt].w=w;
    e[cnt].next=h[u];
    h[u]=cnt++;
}

void insert(int u,int v,int w)
{
    addedge(u,v,w);
    addedge(v,u,0);
}

bool bfs()
{
    memset(level,-1,sizeof(level));
    queue<int> q;
    q.push(s);
    level[s]=0;
    
    while(!q.empty())
    {
        int u=q.front();
        q.pop();
        
        for(int i=h[u];~i;i=e[i].next)
        {
            int v=e[i].v;
            
            if(e[i].w>0&&level[v]==-1)
            {
                level[v]=level[u]+1;
                q.push(v);
            }
        }
    }
    return !(level[t]==-1);
}

int dfs(int u,int maxflow)
{
    if(u==t)
    {
        return maxflow;
    }
    
    int sum=0;
    for(int i=h[u];~i;i=e[i].next)
    {
        int v=e[i].v;
        
        if(e[i].w>0&&level[v]>level[u])
        {
            int flow=dfs(v,min(maxflow,e[i].w));
            if(flow>0)
            {
                e[i].w-=flow;
                e[i^1].w+=flow;
                sum+=flow;
                maxflow-=flow;
                if(maxflow==0) break;
            }
        }
        if(sum==0) level[u]=-1;
    }
    return sum;
}

int main()
{
    int ans=0;
    memset(h,-1,sizeof(h)); 
    
    cin>>n>>m;
    s=n+m+1;t=s+1;
    
    for(int i=1;i<=n;i++)
    {
        int a;
        cin>>a;
        ans+=a;
        insert(s,i,a);
    }
    for(int i=1;i<=m;i++)
    {
        int a;
        cin>>a;
        insert(i+n,t,a);
    }
    for(int i=1;i<=n;i++)
    {
        for(int j=1;j<=m;j++)
        {
            insert(i,j+n,1);
        }
    }//建图
    
    while(bfs()) ans-=dfs(s,INF);//板子
    
    if(ans==0)
    {
        cout<<1<<endl;
        
        for(int i=1;i<=n;i++)
        {
            for(int j=h[i];~j;j=e[j].next)
            {
                int v=e[j].v;
                
                if(v!=s&&e[j].w==0)
                {
                    printf("%d ",v-n);
                }
            }
            cout<<endl;
        }
    }//贪心
    else cout<<0<<endl;
    return 0;
}

骑士共存问题

这道题首先会有人想到八皇后,比如说我。但在他们的做法完全不同。这道题是一道纯的网络流在二分图上的经典应用!首先题目中给了一个非常好的图,是那个棋盘,我们很快就可以看出,在红色格子上的骑士只能攻击黄色格子,这样就可以想到二分图,红色一边,黄的一边(我就不画图了,感觉很恐怖复杂),能互相攻击到的点建流量为 1 1 1 的边,再把源点和汇点加上,就建好图了(障碍物不要弄进去)。

那我们要求什么呢?这就是所谓的最大独立集,那什么是最大独立集呢。对于一张图 G = ( V , E ) G=(V,E) G=(V,E),选出一个最大的点集 V ′ V' V,满足 V ′ ∈ V V'\in V VV,是的对于任意的 u , v ∈ V ′ , ( u , v ) ∉ E u,v\in V',(u,v)\notin E u,vV,(u,v)/E。,具体画图可以百度一下。最大点独立集=点数-最大流。

很显然,这道题就是然我们求最大独立集,这样选出来的点才不会互相攻击。这道题就迎刃而解了。

#include<bits/stdc++.h>
#define nc(a,b) (a-1)*n+b
using namespace std;

const int maxn=201*201;
const int dx[]={1,2,-1,-2,1,2,-1,-2};
const int dy[]={2,1,-2,-1,-2,-1,2,1};

struct edge {
	int v,w,next;
}e[maxn<<4]; 

int n,m,cnt,h[maxn];
bool ob[201][201];

void addedge(int u,int v,int w) {
	e[cnt].v=v;
	e[cnt].w=w;
	e[cnt].next=h[u];
	h[u]=cnt++;
}

void insert(int a,int b,int w) {
	addedge(a,b,w);
	addedge(b,a,0);
}

int level[maxn],s,t;

bool bfs() {
	memset(level,-1,sizeof(level));
	
	queue<int> q;
	q.push(s);
	level[s]=0;
	
	while(!q.empty()) {
		int u=q.front();
		q.pop();
		
		for(int i=h[u];~i;i=e[i].next) {
			int v=e[i].v;
			
			if(e[i].w>0&&level[v]==-1) {
				level[v]=level[u]+1;
				q.push(v);
			}
		}
	}
	
	return level[t]!=-1;
}

int dfs(int u,int maxflow) {
	if(u==t||!maxflow) {
		return maxflow;
	}
	
	int sum=0;
	for(int i=h[u];~i;i=e[i].next) {
		int v=e[i].v;
		
		if(e[i].w>0&&level[v]==level[u]+1) {
			int flow=dfs(v,min(maxflow,e[i].w));
			
			if(flow>0) {
				e[i].w-=flow;
				e[i^1].w+=flow;
				maxflow-=flow;
				sum+=flow;
			}
		}
	}
	if(!sum) {
		level[u]=-1;
	}
	return sum;
}

int main()
{
	//freopen("temp.in","r",stdin);
	//freopen(".out","w",stdout);
	
	cin>>n>>m;
	memset(h,-1,sizeof(h));
	s=0;
	t=n*n+1;
	
	for(int i=1;i<=m;i++) {
		int a,b;
		scanf("%d%d",&a,&b);
		ob[a][b]=1;
	}
	
	for(int i=1;i<=n;i++) {//建图
		for(int j=1;j<=n;j++) {
			if(ob[i][j]) {
				continue;
			}
			if((i+j)&1) {
				insert(s,nc(i,j),1);
				for(int k=0;k<8;k++) {
					int x=i+dx[k];
					int y=j+dy[k];
					
					if(x<1||y<1||x>n||y>n||ob[x][y]){
						continue;
					}
					insert(nc(i,j),nc(x,y),1e9);
				}
			}
			else {
				insert(nc(i,j),t,1);
			}
		}
	}
	
	int ans=0;
	while(bfs()) {
		ans+=dfs(s,2e9+10);//板子
	}
	cout<<n*n-m-ans<<endl;//最大点独立集
	
	//fclose(stdin);
	//fclose(stdout);
	return 0;
}

最小割

算法

最小割就是在一个网络 G = ( V , E ) G=(V,E) G=(V,E) 中,找到一张子图 G ′ = ( V ′ , E ′ ) G'=(V',E') G=(V,E),其中 V ′ ∈ V , E ′ ∈ E V'\in V,E'\in E VV,EE,使得 ∑ ( u , v ) ∈ E ′ w ( u , v ) \sum_{(u,v)\in E'} w(u,v) (u,v)Ew(u,v) 最小,这个最小值就是最小割,比如说对于我们上面将最大流的图:

最小割

这张图的最小割就是 w ( S , 3 ) + w ( S , 1 ) = 3 + 2 = 5 w(S,3)+w(S,1)=3+2=5 w(S,3)+w(S,1)=3+2=5,当然这张图有多解,这里只给出这一种了。

然而我们发现最小割的值与最大流一样,这不是一个巧合,而是一个定理,所以我们只要能证明它,我们就可以用最大流算法求解最小割了,证明如下:

prove

来自 OI-WIKI

所以问题就迎刃而解了。用最大流即可解决最小割。

方格取数

我们现在来将一道例题,这道题首先题意很简单虽然我看了20min才看懂qwawq,我们可以发现,这道题和上面讲过的骑士共存问题很类似,依然用染色法可以将图构建出来,但构完了图,有什么用呢?

我们把问题转化一下, c 取 出 的 最 大 值 = c 总 − c 不 去 出 的 最 小 值 c_{取出的最大值}=c_总-c_{不去出的最小值} c=cc,所以我们把问题转化为了求如何取,使得剩下的方格的和最小,这样我们用最小割可以解决问题。

我们让源点指向一个点集,流量为其方格内的点权,而其它点指向汇点,流量也为其点权,相邻的点之间建流量为 i n f inf inf 的边,是的最小割不会割到这些边上。然后根据之前的定理“最小割=最大流”,我们跑一边 Dinic 即可求出答案。

#include<bits/stdc++.h>
#define nc(i,j) (i-1)*m+j
using namespace std;

const int maxn=35,inf=0x7fffffff;
const int dx[]={1,0,-1,0};
const int dy[]={0,1,0,-1};

struct edge {
	int v,w,next;
}e[maxn*maxn*maxn*maxn]; 

int a[maxn][maxn],h[maxn*maxn],level[maxn*maxn],cnt;
int n,s,t,m;

void addedge(int u,int v,int w) {
	e[cnt].v=v;
	e[cnt].w=w;
	e[cnt].next=h[u];
	h[u]=cnt++;
}
void insert(int u,int v,int w) {
	addedge(u,v,w);
	addedge(v,u,0);
}

bool bfs() {
	queue<int> q;
	q.push(s);
	fill(level,level+t+1,-1);
	level[s]=0;
	while(!q.empty()) {
		int u=q.front();
		q.pop();
		for(int i=h[u];~i;i=e[i].next) {
			int v=e[i].v;
			if(e[i].w>0&&level[v]==-1) {
				level[v]=level[u]+1;
				q.push(v);
			}
		}
	}
	return level[t]!=-1;
}

int dfs(int u,int maxflow) {
	if(u==t) {
		return maxflow;
	}
	int sum=0;
	for(int i=h[u];~i;i=e[i].next) {
		int v=e[i].v;
		if(e[i].w>0&&level[v]==level[u]+1) {
			int flow=dfs(v,min(maxflow,e[i].w));
			e[i].w-=flow;
			e[i^1].w+=flow;
			maxflow-=flow;
			sum+=flow;
			if(maxflow==0) {
				return sum;
			}
		}
	}
	if(sum==0) {
		level[u]=-1;
	}
	return sum;
}

int dinic() {
	int ans=0;
	while(bfs()) {
		ans+=dfs(s,inf);
	}
	return ans;
}

int main()
{
	//freopen(".in","r",stdin);
	//freopen(".out","w",stdout);
	
	cin>>n>>m;
	int sm=0;
	for(int i=1;i<=n;i++) {
		for(int j=1;j<=m;j++) {
			cin>>a[i][j];
			sm+=a[i][j];
		}
	}
	s=0,t=n*m+2;
	fill(h,h+t+1,-1);
	for(int i=1;i<=n;i++) {
		for(int j=1;j<=m;j++) {
			if((i+j)&1) {
				insert(s,nc(i,j),a[i][j]);
				for(int k=0;k<4;k++) {
					int x=i+dx[k];
					int y=j+dy[k];
					if(x<1||y<1||x>n||y>m) {
						continue;
					}
					insert(nc(i,j),nc(x,y),inf);
				}
			}
			else {
				insert(nc(i,j),t,a[i][j]);
			}
		}
	}
	
	cout<<sm-dinic()<<endl;

	//fclose(stdin);
	//fclose(stdout);
	return 0;
}

费用流

基本概念

嗯,这是一个新的名词与概念。那么费用流的全称就是“最小费用最大流问题”,也就是在带权有向图 G = ( V , E ) G=(V,E) G=(V,E) ∀ ( u , v ) ∈ E \forall(u,v)\in E (u,v)E,都有一个流量值 w ( u , v ) w(u,v) w(u,v),和一个费用 c ( u , v ) c(u,v) c(u,v),这个费用表示的是在这条边上流过单位流量的费用。举个例子:在一条满足 ( u , v ) ∈ E (u,v)\in E (u,v)E 的边 ( u , v ) (u,v) (u,v) 上,当流过大小为 f ( u , v ) f(u,v) f(u,v) 的流量时(满足 f ( u , v ) < w ( u , v ) f(u,v) < w(u,v) f(u,v)<w(u,v)),我们所需要花费的费用是 c ( u , v ) ∗ w ( u , v ) c(u,v)*w(u,v) c(u,v)w(u,v)。那么我们要求的就是在最大化 ∑ ( u , v ) ∈ E f ( u , v ) \sum_{(u,v)\in E} f(u,v) (u,v)Ef(u,v)时,最小化 ∑ ( u , v ) ∈ E c ( u , v ) \sum_{(u,v)\in E} c(u,v) (u,v)Ec(u,v) 我们来看一张图,注意:这一次的图流量与之前的不同哦!!!(换图是因为之前那张不太好,最大流只有一种路径 qwawq):

费用流

对于这张图来说,它的最大流很明显是 6 6 6,但最小费用呢?如果我们选择:

S − > 3 − > 2 − > T S->3->2->T S>3>2>T 费用为 3 × 2 + 3 × 1 + 3 × 2 = 15 3\times 2+3 \times 1+3\times 2=15 3×2+3×1+3×2=15
S − > 1 − > 2 − > T S->1->2->T S>1>2>T 费用为 3 × 1 + 3 × 3 + 3 × 2 = 18 3\times 1+3 \times 3+3\times 2=18 3×1+3×3+3×2=18
总费用为 15 + 18 = 33 15+18=33 15+18=33

没有在满足最大流的前提下,比 33 33 33 更小的费用了,所以 33 33 33 就s是最大流最小费用了。

算法

首先,很显然,这个算法在计算的的同时,我们也能得到最大流,所以我们来介绍一种“类Dinic”算法。

我们发现,对于一条增广路 P P P,其费用为
C t o t a l = ∑ ( u , v ) ∈ P ( min ⁡ ( u , v ) ∈ P w ( u , v ) ) × c ( u , v ) C_{total}=\sum_{(u,v)\in P} (\min_{(u,v)\in P} w(u,v))\times c(u,v) Ctotal=(u,v)P((u,v)Pminw(u,v))×c(u,v)
运用乘法分配律:
C t o t a l = min ⁡ ( u , v ) ∈ P w ( u , v ) × ∑ ( u , v ) ∈ P c ( u , v ) C_{total}=\min_{(u,v)\in P} w(u,v)\times \sum_{(u,v)\in P} c(u,v) Ctotal=(u,v)Pminw(u,v)×(u,v)Pc(u,v)

所以我们以费用为边权跑一遍最短路即可。那怎么建图,我们还是运用之前的“反悔”退流思想,如果我们要反悔,我们把费用还回来,所以反向边的费用就是其正向边的费用的相反数。

Q:这样如何保证最大流呢?
A:我们把Dinic算法的BFS部分换成了这里的最短路,因为有负边权,所以可以用SPFA,如果不放心,可以用玄学版Dijsktra把负边权变成正的。因为我们有“反悔”操作,所以就算我们贪心地找到了某一条路径,他的流量太小了,我们就“反悔”,把他的费用给还回来。然后对DFS的内容进行一下改动,这样整个算法就结束了。由于再算最大流时,我们时刻贪心,让费用最小,所以,最后算出来的费用就是最小的了。

接下来,我们就注释里具体来讲每个部分:

#include<bits/stdc++.h>
using namespace std;

const int maxn=5e3+10,inf=0x3fffffff; 

struct edge{
	int v,cap,cost,next;//cap代表流量,cost代表费用
}e[maxn*20];

int n,h[maxn],cnt,s,t,m,vis[maxn],dis[maxn],mincost;

void addedge(int u,int v,int w1,int w2) {
	e[cnt].v=v;
	e[cnt].cap=w1;
	e[cnt].cost=w2;
	e[cnt].next=h[u];
	h[u]=cnt++;
}

void insert(int u,int v,int w1,int w2) {
	addedge(u,v,w1,w2);
	addedge(v,u,0,-w2);//建边
}

bool spfa() {
	fill(vis,vis+n+1,0);
	fill(dis,dis+n+1,inf);
	queue<int> q;
	q.push(s);
	vis[s]=1,dis[s]=0;
	
	while(!q.empty()) {
		int u=q.front();
		q.pop();
		vis[u]=0;
		
		for(int i=h[u];~i;i=e[i].next) {
			int v=e[i].v;
			
			if(e[i].cap&&dis[u]+e[i].cost<dis[v]) {//只有在残量网络里,才做松弛操作
				dis[v]=dis[u]+e[i].cost;
				if(!vis[v]) {
					vis[v]=1;
					q.push(v);
				}
			}
		}
	}
	
	return dis[t]!=inf;//与Dinic算法里的BFS一样,判断S到T是否还有流量
}

int dfs(int u,int maxflow) {
	if(u==t) {
		return maxflow;
	}
	vis[u]=1;
	
	int ans=0;
	for(int i=h[u];~i&&ans<maxflow;i=e[i].next) {
		int v=e[i].v;
		
		if(e[i].cap&&!vis[v]&&dis[v]==dis[u]+e[i].cost) {//dis[v]=dis[u]+e[i].w就相当于分层。
			int flow=dfs(v,min(maxflow,e[i].cap));
			
			if(flow) {
				maxflow-=flow;
				ans+=flow;
				e[i].cap-=flow;
				e[i^1].cap+=flow;
				mincost+=e[i].cost*flow;//这里与最大流一样,不再赘述。
				
				if(maxflow==0) {
					break;
				}
			}
		}
	}
	vis[u]=0;
	
	return ans;
}

int main()
{
	memset(h,-1,sizeof(h));
	
	cin>>n>>m;
	s=1;
	t=n;
	
	for(int i=1;i<=m;i++) {
		int a,b,c,d;
		scanf("%d%d%d%d",&a,&b,&c,&d);
		
		insert(a,b,c,d);//建边
	} 
	
	int ans=0;//记录最大流
	while(spfa()) {
		ans+=dfs(s,inf);//与Dinic一样的~~~
	}
	
	cout<<ans<<" "<<mincost<<endl;

	return 0;//结束的曙光
}

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值