网络流与二分图
至于为什么把两类归在一起,原因很简单,他们极为相似!
先说说比较特殊的二分图,再说一般化的网络流
二分图最大匹配
比如这样一个问题,有N头牛和M堆草,告诉你某些牛喜欢某些草,当然那头牛吃掉某堆草后那堆草就没了,问你最多有几头牛能吃到自己喜欢的草
我们连一条A→①的边来表示A牛喜欢①草,那么我们可以发现,牛之间不会有连边,而草之间同样不会有连边
这样我们把所有牛放在左边归入集合X,把草放在右边归入集合Y,这样就构成了一个二分图。
匈牙利算法可以求出二分图最大匹配
它引入了类似网络流退流的思想,具体不再多说
//X集有n个点,Y集有m个点 //g[A][B]表示A->B有边 bool search(int x) { for(int i=1;i<=m;i++) if(g[x][i] && !h[i]) { h[i]=1; if(pre[i]==-1 || search(pre[i])) { pre[i]=x; return 1; } } return 0; }
| int MaxMatch() { int res=0; for(int i=1;i<=n;i++) { memset(h,0,sizeof(h)); if(search(i)) res++; } return res; }
int main() { 建图; printf("%d\n",MaxMatch()); }
|
最大流
前面已经说了最大匹配和最大流之间是可以互相转化的,如下图
求最大流的算法很多
我个人觉得效率高代码短的是sap算法(其实也只会这一种。。。)
其他的不再多说,附上sap标号法代码
int sap(int x,int flow) { if(x==n) return flow; int res=0; for(int i=1;i<=n;i++) if(g[x][i] && h[x]==h[i]+1) { int t=sap(i,min(flow-res,g[x][i])); g[x][i]-=t;g[i][x]+=t; if((res+=t)==flow) return flow;//饱和 } if(h[1]>=n) return res; if(--v[h[x]]==0) h[1]=n; ++v[++h[x]]; return res; } |
最优匹配
顾名思义,如果我们给上面的问题再加一个描述,也就是没堆草吃下去后所获得的营养不同,问能获得的最大营养是多少
显然,这个肯定是最大匹配的所有方案中去选择一个最优方案
这就要用到KM算法
不过KM我不会。。。。
但是,前面说过,可以转化成费用流来做
费用流
接着上面的话题,建模相信大家应该已经有一点想法了,同样加一个源和一个汇,源和每个起点连一条费用为0流量为1的边,而每个重点和汇连一条费用为0流量为1的边
这样图就建好了,跑一次费用流
怎么写?spfa就可以了,我把它称作连续最短路算法
有一次YY同学问我问什么这是对的,这里顺便我也理一下思路
Sap算法是每次随便找一条路然后填满,不过按理来说这种贪心是不可行的,但是引入了退流这个牛逼的东西后,这就对了!现在我们不再是盲目的随便找一条边增广,而是找费用最小的增广,这样保证每次增广出来的路费用最小,就能保证最终费用最小,并且在退流这个牛逼东西的影响下,最终我们还可以找出正确的最大流!
好了,证明就到这里了
算法基本过程以及很明了了,找一条最短路(要记录下路径),然后在这条最短路上尽可能多地填充流量
看看下面的代码
bool spfa() { memset(inq,0,sizeof(inq)); memset(dist,0x3f,sizeof(dist)); dist[S]=0; q.push(S); while(!q.empty()) { int x=q.front(); q.pop();inq[x]=false; for(int i=S;i<=T;i++) if(g[x][i]>0 && dist[i]>dist[x]+w[x][i]) { pre[i]=x; dist[i]=dist[x]+w[x][i]; if(!inq[i]) { inq[i]=true; q.push(i); } } } //如不为极大值就返回true,即成功找到一条最短路 } | int MFMC() { memset(pre,-1,sizeof(pre)); int maxflow=0,mincost=0; while(spfa()) { int res=1; for(int i=T;i!=S;i=pre[i]) { g[pre[i]][i]-=res; g[i][pre[i]]+=res; } maxflow+=res; mincost+=res*dist[T]; } return mincost; } |
在spfa中唯一改动的地方就是红色部分,因为我们需要判断是否还有流量
想想一下水管,整个水管的流量取决于最细的一截,所以要在最短路上找到流量最小的一条边,填满
至于怎么记录最短路,很简单,一个pre[]数组搞定
不过遗憾的是,由很多情况的费用流点数很多,如N=10000,M=50000,这样用矩阵就存不下了,所以我们要用到邻接表,代码如下
void addedge(int a,int b,int c,int d) { edge[L]=(EG){a,b,c,+d,head[a]}; head[a]=L++; edge[L]=(EG){b,a,0,-d,head[b]}; head[b]=L++; } bool spfa() { memset(h,0,sizeof(h)); memset(dist,0x3f,sizeof(dist)); dist[S]=0;q.push(S);h[S]=1; while(!q.empty()) { int x=q.front(); q.pop();h[x]=false; for(int i=head[x];i!=-1;i=edge[i].next) if(edge[i].flow>0 && dist[edge[i].to]>dist[x]+edge[i].cost) { pre[edge[i].to]=i; dist[edge[i].to]=dist[x]+edge[i].cost; if(!h[edge[i].to]) { h[edge[i].to]=true; q.push(edge[i].to); } } } return dist[T]!=inf; } int MFMC() { int mincost=0,maxflow=0; while(spfa()) { int res=inf; for(int i=T;i!=S;i=edge[pre[i]].from) { res=min(res,edge[pre[i]].flow); } for(int i=T;i!=S;i=edge[pre[i]].from) { edge[pre[i]].flow-=res; edge[pre[i]^1].flow+=res; } maxflow+=res; mincost+=res*dist[T]; } return mincost; } |
注意上面标红的几个地方
1) 首先是addedge模块,我们在用矩阵的时候是用了g[a][b]和g[b][a],这里同理,要加上两条边
2) 在spfa中的pre记录下当前边在链表中的位置
3) pre[i]^1表示找到如(1)所说中该边所对的另一条边(听懂没?)
然后就是题目变形了,就像最优匹配那一题,它要求我们求出最大值,所以我们就要转换成最大费用最大流了,怎么转换?简单,spfa哪里的”>”改成”<”,然后memset那里赋极大值改成赋极小值即可
我做过的网络流(二分图)题目
费用流/最优匹配
花店橱窗布置 wikioi1028 http://blog.csdn.net/jiangzh7/article/details/8706627
疯狂的方格取数 vijos 1653 http://blog.csdn.net/jiangzh7/article/details/8706937
丘比特的烦恼 Vijos 1169 http://blog.csdn.net/jiangzh7/article/details/8735843
最大流/最大匹配
ditch http://blog.csdn.net/jiangzh7/article/details/8545279
COURSES POJ1469 http://blog.csdn.net/jiangzh7/article/details/8644173
Chessboard POJ2446 http://blog.csdn.net/jiangzh7/article/details/8665765
Way Selection Vijos1212 http://blog.csdn.net/jiangzh7/article/details/8731492