西安多校集训-网络流
前言
最晚想起来学了的内容(雾。
概念
基本概念
网络流就是指一张有向图,有 \(n\) 个点,\(m\) 条边,\(s\) 为源点,\(t\) 为汇点。
边有边权为 \(c(u,v)\) 表示 \(u\to v\) 的容量。
定义 \(f(u,v)\) 表示 \(u\to v\) 的流量。
定义剩余流量为容量与流量之差为 \(c_f(u,v)=c(u,v)-f(u,v)\)。
整个网络的流即为:\(\sum_{(u,v)\in E} f(u,v)\) 。
为了方便,我们定义 \(f(u,v)=0\) 表示 \(u\) 与 \(v\) 之间没有连边,当然也可以表示 \(u\) 与 \(v\) 之间的边已经满流量了。
这些概念其实挺好懂得。
性质
- 流量限制:从
字面意思知道,流量不能大于容量,形式化的说就是:\(f(u,v)\le c(u,v)\) 。 - 流守恒性:\(\sum_{(u,x)\in E}f(u,x)=\sum_{x,v\in E}(x,v)\space (\forall x ≠s,t)\)。说人话就是对于源点与汇点以外的点,流进多少就流出多少,不会堆积。
- 如果我们给网络流建反向边,定义为\(f'(v,u)\),表示 \(v\to u\) 的流量。(用处后面再说)那么就有 \(f(u,v)=-f'(v,u)\)。此时的 \(f'(v,u)\) 可能为负数。
综上可得出:\(c_f(u,v)-f'(v,u)=c(u,v)\) 。
可以这样想:当我们反向边流量越大时,这条边的剩余流量也越多。(也许在后面求最大流有用?)
最大流最小割定理
最大流总等于最小割。
理性证明不会,感性理解的话可以这样想:我们的最大流受限于最小的容量边,而最小割就是要割去最小的容量边,所以它们相等。
【模板】网络最大流
好了终于来到算法部分了,累死我了。
首先,只有 OiWiki 的代码与解释是符合定义的!如果你真的跟我一样突然脑子发病去死扣概念,请阅读 OiWiki 。要不然你会浪费将近 40 分钟的光阴。
题解区的部分(前三篇)代码都直接将反向边流量与剩余流量混用,可能是方便代码也可能是方便理解,但是它不符合我们上文对反向边的定义。如果你是新学网络流的话,我还是推荐直接阅读 OiWiki 。(尽管 OiWiki 可读性差,但是它可以保证正确性啊)
Edmonds–Karp 算法
复杂度为 \(O(nm^2)\) 。
具体做法是:
- 我们如果可以从图上 \(s\) 出发到 \(t\) ,那么就证明我们找到了一条新的增广路 \(p\)。
- 去求这条增广路 \(p\) 上的最小剩余容量,也就是 \(minn=min_{(u,v)\in p} c_f(u,v)\)
- 对于 \(p\) 上每条边加上 \(minn\) 的流量,并给它们的反向边退掉 \(minn\) 的流量,此时最大流增加 \(minn\) 。
- 重复上述操作直到在图上 \(s\) 无法到达 \(t\) 。
具体实现如下,代码借鉴抄袭了 OiWiki:
#include<iostream>
#include<vector>
#include<cstring>
#include<queue>
#define int long long
using namespace std;
const int N=2e2+10;
const int M=5e3+10;
const int inf=2e17;
struct Edge{
int u,v,cap,flow;
};
struct EK{
int n,m;//点数,边数
vector<Edge> e;//边
vector<int> G[N];// G[x] 表示 x 能到达的所有边在 e 中的编号
// 其实是不优美的链式前向星啦
int a[N],p[N];//a[x]表示点 x 的流量,p[x] 表示点 x 的流量从哪一条边来的。
void init(int n){
for(int i=0;i<n;i++) G[i].clear();
e.clear();
return;
}
void add(int u,int v,int cap){
e.push_back({u,v,cap,0});
e.push_back({v,u,0,0});
m=e.size();
G[u].push_back(m-2);
G[v].push_back(m-1);
return ;
}
int maxflow(int s,int t){
int res=0;
while(1){
memset(a,0,sizeof(a));
queue<int> que;
que.push(s);
a[s]=inf;
while(!que.empty()){
int u=que.front();
que.pop();
for(auto i: G[u]){//遍历以 x 为起点的边
Edge edge=e[i];
if(!a[edge.v] && edge.cap > edge.flow){//如果这条边之前没被流过,并且还有剩余流量
p[edge.v]=i;
a[edge.v]=min(a[u],edge.cap-edge.flow);//流过来的和剩余流量取最小值
que.push(edge.v);
}
}
if(a[t]) break; // 如果 t 接到流量,就可以停止 bfs 了
}
if(!a[t]) break;//如果 t 没接到流量,就说明图中没有增广路了
for(int x=t;x!=s;x=e[p[x]].u){//遍历 s->t 的路程,给它们加上最后的流量
e[p[x]].flow+=a[t];
e[p[x]^1].flow-=a[t];
}
res+=a[t];
}
return res;
}
}ek;
signed main(){
int n,m,s,t;
cin>>n>>m>>s>>t;
ek.n=n;
for(int i=1;i<=m;i++){
int u,v,cap;
cin>>u>>v>>cap;
ek.add(u,v,cap);
}
cout<<ek.maxflow(s,t);
return 0;
}
Dinic 算法
观察到上述 EK 算法每一次都暴力扩展并不优美,考虑去给它做一个优化,就是 Dinic 算法。复杂度为 \(O(n^2m)\)
具体做法:
- 首先跑一遍 BFS 将图分层,此后每个点的流只会流给下一层。
- 在分层图上求出最大的增广流,定义为阻塞流。
- 将阻塞流加入原本的最大流中,更新图。
- 重复 1,2,3 直到不能从 s 出发到达 t 为止。
这个算法还可以优化,就是当前弧优化:
如果某一时刻我们已经知道边 \((u, v)\) 已经增广到极限(边 \((u, v)\) 已无剩余容量或 \(v\) 的后侧已增广至阻塞),则 \(u\) 的流量没有必要再尝试流向出边 \((u, v)\)。据此,对于每个结点 \(u\),我们维护 \(u\) 的出边中第一条还有必要尝试的出边。
——摘自 OiWiki
其实有没有点像 DFS 剪枝时的感觉,好吧其实就是。
code:
#include<iostream>
#include<queue>
#include<cstring>
#define ll long long
#define inf 1e18
#define int long long
using namespace std;
const int N=2e3+10;
struct Edge{
int to,nxt,cap,flow;
}e[2*N];
int n,m,s,t;
int head[N],tot=1;
int dep[N],cur[N];
ll maxflow=0;
void add(int u,int v,int cap){
e[++tot].to=v;
e[tot].nxt=head[u];
e[tot].cap=cap;e[tot].flow=0;
head[u]=tot;
e[++tot].to=u;
e[tot].nxt=head[v];
e[tot].cap=0;e[tot].flow=0;
head[v]=tot;
return;
}
bool bfs(){
queue<int> que;
memset(dep,0,sizeof(dep));
dep[s]=1;
que.push(s);
while(!que.empty()){
int u=que.front();
que.pop();
for(int i=head[u];i;i=e[i].nxt){
int v=e[i].to;
if((!dep[v]) && (e[i].cap>e[i].flow)){
dep[v]=dep[u]+1;
que.push(v);
}
}
}
return dep[t];
}
int dfs(int u,int flow){
if((u==t) || (!flow)) return flow;
int res=0;
for(int& i=cur[u];i;i=e[i].nxt){
int v=e[i].to,minflow;
if((dep[v]==dep[u]+1) && (minflow=dfs(v,min(flow-res,e[i].cap-e[i].flow)))){
res+=minflow;
e[i].flow+=minflow;
e[i^1].flow-=minflow;
if(res==flow) return res;
}
}
return res;
}
int Maxflow(){
while(bfs()){
memcpy(cur,head,sizeof(head));
maxflow+=dfs(s,inf);
}
return maxflow;
}
signed main(){
cin>>n>>m>>s>>t;
for(int i=1;i<=m;i++){
int u,v,cap;
cin>>u>>v>>cap;
add(u,v,cap);
}
cout<<Maxflow();
return 0;
}
例题
圆桌问题
可以将单位与餐桌连一条流量为 1 的边,然后建立源点与汇点,源点向单位连一条流量为 \(r_i\) 的边,餐桌向汇点连一条流量为 \(c_i\) 的边,跑一个最大流。
至于统计方案,遍历一遍单位,看哪一条边的剩余流量为 0 ,就表示这条边被使用。
code:
#include<iostream>
#include<queue>
#include<cstring>
#define ll long long
using namespace std;
const int N=300;
const int M=1e5+10;
const int inf=2e9;
int n,m,s,t,sum;
int r[N],c[N];
ll maxflow;
int cur[N],dep[N];
struct Edge{
int to,nxt,c;
}e[M];
int head[N],tot=1;
void add(int u,int v,int c){
e[++tot].to=v;
e[tot].nxt=head[u];
e[tot].c=c;
head[u]=tot;
e[++tot].to=u;
e[tot].nxt=head[v];
e[tot].c=0;
head[v]=tot;
return;
}
bool bfs(){
queue<int> que;
memset(dep,0,sizeof(dep));
dep[s]=1;
que.push(s);
while(!que.empty()){
int u=que.front();
que.pop();
for(int i=head[u];i;i=e[i].nxt){
int v=e[i].to;
if((!dep[v]) && e[i].c>0){
dep[v]=dep[u]+1;
que.push(v);
}
}
}
return dep[t];
}
int dfs(int u,int flow){
if((u==t) || !flow) return flow;
int res=0;
for(int& i=cur[u];i;i=e[i].nxt){
int v=e[i].to,d;
if((dep[v]==dep[u]+1) && (d=dfs(v,min(flow-res,e[i].c)))){
res+=d;
e[i].c-=d;
e[i^1].c+=d;
if(res==flow) return res;
}
}
return res;
}
int Dinic(){
while(bfs()){
memcpy(cur,head,sizeof(head));
maxflow+=dfs(s,inf);
}
return maxflow;
}
int main(){
cin>>n>>m;
for(int i=1;i<=n;i++){
cin>>r[i];
sum+=r[i];
}
for(int i=1;i<=m;i++){
cin>>c[i];
}
s=n+m+1,t=n+m+2;
for(int i=1;i<=n;i++){
add(s,i,r[i]);
}
for(int i=1;i<=m;i++){
add(i+n,t,c[i]);
}
for(int i=1;i<=n;i++){
for(int j=1;j<=m;j++){
add(i,j+n,1);
}
}
int res=Dinic();
cout<<(res==sum)<<'\n';
if(res!=sum) return 0;
for(int i=1;i<=n;i++){
for(int j=head[i];j;j=e[j].nxt){
int v=e[j].to;
if(v!=s && e[j].c==0){
cout<<v-n<<' ';
}
}
cout<<'\n';
}
return 0;
}

浙公网安备 33010602011771号