一个网络\(G=(V,E)\)是一张有向图,图中每条有向边\((x,y)\in E\)都有一个给定的权值\(c(x,y)\),称为边的容量。特别地,若\((x,y) \notin E\),则\(c(x,y)=0\)。图中还有两个指定的特殊节点\(S,T \in V(S \neq T)\)分别被称为源点和汇点。
设\(f(x,y)\)是定义在节点二元组\((x \in V,y \in V)\)上的实数函数,且满足:
容量限制:\(f(x,y) \leq c(x,y)\)
斜对称:\(f(x,y)=-f(y,x)\)
流量守恒:\(\forall x \neq S,\ x \neq T,\ \sum_{(u,x)\ \in E} f(u,x) = \sum_{(x,v)\ \in E}f(x,v)\)
\(f\)称为网络的流函数,对于\((x,y) \in E\),\(f(x,y)\)称为边的流量,\(c(x,y)-f(x,y)\)称为边的剩余流量
\(\sum_{(S,v)\ \in E} f(S,v)\)称为整个网络的流量(\(S\)为源点)
最大流
Edmond—Karp算法
若一条从源点\(S\)到汇点\(T\)的路径上各条边的剩余容量都大于\(0\),则称这条路径为一条增广路
\(EK\)算法为用\(bfs\)不断寻找增广路,直到网络上不存在增广路为止
用\(bfs\)找到任意一条从\(S\)到\(T\)的路径,记录路径上各边的剩余容量的最小值,则网络的流量就可以增加这个最小值
利用邻接表成对存储来实现\((x,y)\)剩余容量的减小,\((y,x)\)剩余容量的增大
时间复杂度上界为\(O(nm^2)\),一般可以处理\(1e3 \sim 1e4\)规模的网格
\(code\):
bool bfs() { memset(vis,0,sizeof(vis)); queue<int> q; q.push(s); vis[s]=true; res[s]=inf; while(!q.empty()) { int x=q.front(); q.pop(); for(int i=head[x];i;i=e[i].nxt) { int y=e[i].to,v=e[i].v; if(vis[y]||!v) continue; res[y]=min(res[x],v); pre[y]=i; q.push(y); vis[y]=true; } } return vis[t]; } void update() { int x=t; while(x!=s) { int i=pre[x]; e[i].v-=res[t]; e[i^1].v+=res[t]; x=e[i^1].to; } ans+=res[t]; } ...... while(bfs()) update();
Dinic算法
在任意时刻,网络中所有节点以及剩余容量大于\(0\)的边构成的子图称为残量网络
\(Dinic\)算法引入分层图的概念,\(d[x]\)表示从\(S\)到\(x\)最少需要经过的边数,为了方便处理设\(d[S]=1\),分层图为残量网络中满足\(d[y]=d[x]+1\)的边\((x,y)\)构成的子图
时间复杂度上界为\(O(n^2m)\),一般可以处理\(1e4 \sim 1e5\)规模的网格,求解二分图最大匹配的时间复杂度为\(O( \sqrt nm)\)
在\(Dinic\)算法中还可以加入若干剪枝来优化
\(res\),表示当前节点的流量剩余,若\(res \leqslant 0\),停止寻找增广路
\(cur[x]\),表示当到达到\(x\)节点时,直接从\(cur[x]\)对应的边开始遍历,实际表示上一次从\(x\)遍历到了哪一条边,因为在这之间的边都已经被彻底增广过了,所以可以直接跳转,称为当前弧优化
\(code\):
bool bfs() { memcpy(cur,head,sizeof(head)); memset(d,0,sizeof(d)); queue<int> q; q.push(s); d[s]=1; while(!q.empty()) { int x=q.front(); q.pop(); for(int i=head[x];i;i=e[i].nxt) { int y=e[i].to,v=e[i].v; if(d[y]||!v) continue; d[y]=d[x]+1; q.push(y); } } return d[t]; } int dfs(int x,int lim) { if(x==t) return lim; int res=lim,flow; for(int &i=cur[x];i;i=e[i].nxt) { int y=e[i].to,v=e[i].v; if(d[y]!=d[x]+1||!v) continue; if(flow=dfs(y,min(res,v))) { res-=flow; e[i].v-=flow; e[i^1].v+=flow; if(!res) break; } } return lim-res; } int dinic() { int flow,ans=0; while(bfs()) while(flow=dfs(s,inf)) ans+=flow; return ans; }
若要求每条边的所用的流量,可以将原图备份,跑完最大流后,用原图的容量减去当前的剩余容量即可求得所用流量
可以去求解二分图多重匹配
利用最小割,将求解最大收益转化为最小代价
费用流
Edmond—Karp算法
\(code\):
bool spfa() { for(int i=1;i<=n;++i) dis[i]=inf; memset(vis,0,sizeof(vis)); queue<int> q; q.push(s); vis[s]=true; dis[s]=0; res[s]=inf; while(!q.empty()) { int x=q.front(); q.pop(); vis[x]=false; for(int i=head[x];i;i=e[i].nxt) { int y=e[i].to,v=e[i].v,c=e[i].c; if(dis[y]>dis[x]+c&&v) { dis[y]=dis[x]+c; res[y]=min(res[x],v); pre[y]=i; if(!vis[y]) { vis[y]=true; q.push(y); } } } } return dis[t]!=inf; } void update() { int x=t; while(x!=s) { int i=pre[x]; e[i].v-=res[t]; e[i^1].v+=res[t]; x=e[i^1].to; } ans+=res[t]; sum+=res[t]*dis[t]; } ...... while(spfa()) update();
Dinic算法
\(code\):
bool spfa() { for(int i=1;i<=n;++i) dis[i]=inf; memset(vis,0,sizeof(vis)); queue<int> q; q.push(s); dis[s]=0; vis[s]=true; while(!q.empty()) { int x=q.front(); q.pop(); vis[x]=false; for(int i=head[x];i;i=e[i].nxt) { int y=e[i].to,v=e[i].v,c=e[i].c; if(dis[y]>dis[x]+c&&v) { dis[y]=dis[x]+c; if(!vis[y]) { vis[y]=true; q.push(y); } } } } return dis[t]!=inf; } int dfs(int x,int lim) { if(x==t) return lim; vis[x]=true; int res=lim,flow; for(int i=head[x];i;i=e[i].nxt) { int y=e[i].to,v=e[i].v,c=e[i].c; if(dis[y]!=dis[x]+c||!v||vis[y]) continue; if(flow=dfs(y,min(res,v))) { res-=flow; e[i].v-=flow; e[i^1].v+=flow; if(!res) break; } } return lim-res; } void dinic() { int flow; while(spfa()) while(flow=dfs(s,inf)) ans+=flow,sum+=flow*dis[t]; }
可以去求解二分图带权匹配
有上下界限制的网络流
无源汇有上下界可行流
\(n\)个点,\(m\)条边的网络,求一个可行解,使得边\((x,y)\)的流量介于\([\ low_{x,y},up_{x,y}\ ]\)之间,并且整个网络满足流量守恒
将\(up_{x,y}-low_{x,y}\)作为容量上界,\(0\)作为容量下界
设\(in[x]=\sum\limits_{i\to x} low(i,x)-\sum\limits_{x\to i} low(x,i)\)
若\(in[x]>0\),则从源点\(S\)向\(x\)连边,容量为\(in[x]\),反之,则从\(x\)向汇点\(T\)连边,容量为\(-in[x]\)
在该网络上求最大流,求完后每条边的流量再加上容量下界即为一种可行流
\(code:\)
bool bfs() { memcpy(cur,head,sizeof(head)); memset(d,0,sizeof(d)); queue<int> q; q.push(s); d[s]=1; while(!q.empty()) { int x=q.front(); q.pop(); for(int i=head[x];i;i=e[i].nxt) { int y=e[i].to,v=e[i].v; if(d[y]||!v) continue; d[y]=d[x]+1; q.push(y); } } return d[t]; } int dfs(int x,int lim) { if(x==t) return lim; int res=lim,k; for(int &i=cur[x];i;i=e[i].nxt) { int y=e[i].to,v=e[i].v; if(d[y]!=d[x]+1||!v) continue; if(k=dfs(y,min(res,v))) { res-=k; e[i].v-=k; e[i^1].v+=k; if(!res) break; } } return lim-res; } int dinic() { int flow,ans=0; while(bfs()) while(flow=dfs(s,inf)) ans+=flow; return ans; } bool check() { for(int i=head[s];i;i=e[i].nxt) if(e[i].v) return false; return true; } ...... for(int i=1;i<=m;++i) { int a,b,up; read(a),read(b),read(low[i]),read(up); in[a]-=low[i],in[b]+=low[i]; add(a,b,up-low[i]); } for(int i=1;i<=n;i++) { if(in[i]>0) add(s,i,in[i]); else add(i,t,-in[i]); } dinic(); if(check()) { puts("YES"); for(int i=1;i<=m;i++) printf("%d\n",e[(i<<1)^1].v+low[i]); } else puts("NO");
有源汇有上下界最大流
从\(T\)向\(S\)连一条容量上界为\(inf\),容量下界为\(0\)的边,使有源汇转化为无源汇
在残量网络上再求原源点到原汇点的最大流
\(code:\)
bool bfs() { memcpy(cur,head,sizeof(head)); memset(d,0,sizeof(d)); queue<int> q; q.push(s); d[s]=1; while(!q.empty()) { int x=q.front(); q.pop(); for(int i=head[x];i;i=e[i].nxt) { int y=e[i].to,v=e[i].v; if(d[y]||!v) continue; d[y]=d[x]+1; q.push(y); } } return d[t]; } int dfs(int x,int lim) { if(x==t) return lim; int res=lim,k; for(int &i=cur[x];i;i=e[i].nxt) { int y=e[i].to,v=e[i].v; if(d[y]!=d[x]+1||!v) continue; if(k=dfs(y,min(res,v))) { res-=k; e[i].v-=k; e[i^1].v+=k; if(!res) break; } } return lim-res; } int dinic() { int flow,ans=0; while(bfs()) while(flow=dfs(s,inf)) ans+=flow; return ans; } bool check() { for(int i=head[s];i;i=e[i].nxt) if(e[i].v) return false; return true; } ...... for(int i=1;i<=m;++i) { int a,b,up,low; read(a),read(b),read(low),read(up); in[a]-=low,in[b]+=low; add(a,b,up-low); } for(int i=1;i<=n;i++) { if(in[i]>0) add(s,i,in[i]); else add(i,t,-in[i]); } add(T,S,inf); dinic(); ans=e[edge_cnt].v; e[edge_cnt].v=e[edge_cnt^1].v=0; if(check()) { s=S,t=T; printf("%d",ans+dinic()); } else puts("NO");
有源汇有上下界最小流
先不添加 \(T\) 到 \(S\) 的边,求一次超级源到超级汇的最大流。
然后再添加一条从 \(T\) 到 \(S\) 下界为 \(0\) ,上界为 \(inf\) 的边,在残量网络上再求一次超级源到超级汇的最大流
流经 \(T\) 到 \(S\) 的边的流量就是最小流的值
\(code:\)
bool bfs() { memcpy(cur,head,sizeof(head)); memset(d,0,sizeof(d)); queue<int> q; q.push(s); d[s]=1; while(!q.empty()) { int x=q.front(); q.pop(); for(int i=head[x];i;i=e[i].nxt) { int y=e[i].to,v=e[i].v; if(d[y]||!v) continue; q.push(y); d[y]=d[x]+1; } } return d[t]; } int dfs(int x,int lim) { if(x==t) return lim; int res=lim,k; for(int &i=cur[x];i;i=e[i].nxt) { int y=e[i].to,v=e[i].v; if(d[y]!=d[x]+1||!v) continue; if(k=dfs(y,min(res,v))) { res-=k; e[i].v-=k; e[i^1].v+=k; if(!res) break; } } return lim-res; } int dinic() { int k,flow=0; while(bfs()) { while(k=dfs(s,inf)) { flow+=k; } } return flow; } bool check() { for(int i=head[s];i;i=e[i].nxt) if(e[i].v) return false; return true; } ...... for(int i=1;i<=m;++i) { int a,b,up,low; read(a),read(b),read(low),read(up); in[a]-=low,in[b]+=low; add(a,b,up-low); } for(int i=1;i<=n;i++) { if(in[i]>0) add(s,i,in[i]); else add(i,t,-in[i]); } dinic(); add(T,S,inf); dinic(); if(!check()) { puts("please go home to sleep"); return 0; } printf("%d",e[edge_cnt].v);
最大权闭合子图
若有向图 \(G\) 的子图 \(V\) 满足: \(V\) 中顶点的所有出边均指向 \(V\) 内部的顶点,则称 \(V\) 是 \(G\) 的一个闭合子图
若 \(G\) 中的点有点权,则点权和最大的闭合子图称为有向图 \(G\) 的最大权闭合子图
建立源点 \(S\) 和汇点 \(T\) ,源点 \(S\) 连所有点权为正的点,容量为该点点权;其余点连汇点 \(T\) ,容量为该点点权的相反数,对于原图中的边 \((x,y)\) ,连边 \((x,y,inf)\)
最大权闭合图的点权和 \(=\) 所有正权点权值和 \(-\) 最小割
也就是最大收益转化为了最小代价
在残量网络中由源点 \(S\) 能够访问到的点,就构成一个点数最少的最大权闭合图
最大密度子图
一个无向图\(G=(V,E)\)的边数\(|E|\)与点数\(|V|\)的比值\(D=\frac{|E|}{|V|}\)称为它的密度
求\(G\)的一个子图\(G^\prime=(V^\prime,E^\prime)\),使得\(D^\prime=\frac{|E^\prime|}{|V^\prime|}\)最大
二分\(g\leqslant\frac{|E|}{|V|}\)
得\(|E|-|V|×g \geqslant0\)
源点\(S\)向所有边连容量为\(1\)的边,边向其两端的点连容量为\(inf\)的边,点向汇点\(T\)连容量为\(g\)的边
二分下界:\(\frac{1}{n}\),上界:\(m\),精度:\(\frac{1}{n^2}\)
\(code:\)
bool bfs() { for(int i=s;i<=t;++i) cur[i]=head[i]; memset(d,0,sizeof(d)); queue<int> q; q.push(s); d[s]=1; while(!q.empty()) { int x=q.front(); q.pop(); for(int i=head[x];i;i=e[i].nxt) { int y=e[i].to; double v=e[i].v; if(d[y]||fabs(v)<eps) continue; d[y]=d[x]+1; q.push(y); } } return d[t]; } double dfs(int x,double lim) { if(x==t) return lim; double res=lim,flow; for(int &i=cur[x];i;i=e[i].nxt) { int y=e[i].to; double v=e[i].v; if(d[y]!=d[x]+1||fabs(v)<eps) continue; if(fabs(flow=dfs(y,min(res,v)))>=eps) { res-=flow; e[i].v-=flow; e[i^1].v+=flow; if(fabs(res)<eps) break; } } return lim-res; } double dinic() { double flow,ans=0; while(bfs()) while(fabs(flow=dfs(s,inf))>=eps) ans+=flow; return ans; } double check(double x) { edge_cnt=1; memset(head,0,sizeof(head)); for(int i=1;i<=n;++i) add(i+m,t,x); for(int i=1;i<=m;++i) { int x=ed[i].x,y=ed[i].y; add(s,i,1.0),add(i,x+m,inf),add(i,y+m,inf); } return m*1.0-dinic(); } int work() { int ans=0; memset(du,0,sizeof(du)); memset(vis,0,sizeof(vis)); check(g); for(int i=1;i<=m;++i) { int x=ed[i].x,y=ed[i].y; if(d[i]) { if(++du[x]==1) ans++,vis[x]=true; if(++du[y]==1) ans++,vis[y]=true; } } return ans; } ...... l=0,r=m,g=0; while(l+1/((double)n*(double)n)<r) { double mid=(l+r)/2.0; if(check(mid)>eps) g=l=mid; else r=mid; } printf("%d\n",work()); for(int i=1;i<=n;++i) if(vis[i]) printf("%d\n",i);
最小割二元关系
二元关系指如选和不选的关系
建立最小割模型,来解决一系列问题,如happiness、文理分科和人员雇佣
来源:https://www.cnblogs.com/lhm-/p/12229508.html