网络流 (学习笔记)(26.1.24)
网络流 (学习笔记)
概述
网络流初步学习,首先感觉是一种抽象的图论,把一些权值作为如水一样的流,然后以一些限制作为管道的容量变化。
概念
对于一张有向图,其中含有 \(S\) 和 \(T\) 作为源点,汇点,之后我们以 \(c(x,y)\) 定义为边的容量
我们又称 \(f(x,y)\) 为边的流量,其有以下三个性质:
- \(f(x,y)\le c(x,y)\)
- \(f(x,y)=-f(y,x)\)
- \(正向流向和\) = \(反向流向和\)
此外,我们定义所有节点与剩余容量 \(>0\) 的边构成的子图为:残量网络
最大流
对于最大流,我们有一种贪心想法,这里先引入增广路
增广路,这里指一条从 \(S\) 到 \(T\) 的路径,中间每条边的剩余容量(\(c(x,y)-f(x,y)\))都 \(> 0\)
所以如果要求最大流,我们就可以贪心的想,一直增广(指对于某条路径的流量增加)一条增广路是不是就行了,但是通过以下这个样例发现,貌似还需要去做一些反悔操作,也就是需要去建反悔边,所以算法就可以这样得出了:
dinic
#include <bits/stdc++.h>
using namespace std;
using i64 = long long;
const i64 inf = 1e16;
const int N = 501000;
int n, m, s, t;
i64 w, ans=0, dis[N];
int cnt=1,now[N],h[N];
struct node{
int to, nxt;i64 w;
}s1[N<<1];
void add(int u, int v, i64 w){
s1[++cnt] = {v,h[u],w};
h[u] = cnt;
s1[++cnt] = {u,h[v],0};//建立反向边用于反悔
h[v] = cnt;
}
int bfs(){
for(int 1; i<=n; i++) dis[i]=inf;//这里表示的dis是层数
queue<int> q;q.push(s);
now[s]=h[s], dis[s]=0;
while(!q.empty()){
int u = q.front();
q.pop();
for(int i=s1[u]; i; i=s1[i].nxt){
int v = s1[i].to;
if(s1[i].w>0&&dis[v]==inf){//对于还可以增广的边进行弧优化
q.push(v);
now[v]=h[v], dis[v]=dis[u]+1;
if(v==t)return 1;
}
}
}
}
i64 dfs(int u, int sum){//sum在这里可以理解为当前节点拥有的水的总量
if(u==t) return sum;//建立边界,返回数值
i64 k, res=0;//res是流出总量
for(int i=now[u]; i&∑ i==s1[i].nxt){//sum必须有才可以去增广
now[u] = i;
int v=s1[i].to;
if(s1[i].w>0&&(dis[v]==dis[u]+1)){
k = dfs(v,min(sum,s1[i].w));//k表示下面节点用了多少的水
if(!k) dis[v]=inf;//如果下面节点没有用水,就说明没用了
s1[i].w -= k;//把可用的水减掉
s1[i^1].w += k;//在反向边添加水,便于反悔
res+=k, sum-=k;//总消耗水量增加,减去储备水量
}
}
return res;//反悔用了多少水,便于递归
}
int main(){
cin>>n>>m>>s>>t;
for(int i=1; i<=m; i++){
int u, v, w;
cin>>u>>v>>w;
add(u,v,w);
}
while(bfs){
ans+=dfs(s,inf);
}
cout<<ans;
}
EK
#include <bits/stdc++.h>
using namespace std;
using i64 = long long;
const int N= 5000100;
const i64 inf = 1e16;
int n, m, s, t;
struct node{
int to, nxt;i64 w;
}s1[N];
int h[N], cnt=1;
void add(int u, int v, i64 w){
s1[++cnt] = {v,h[u],w};
h[u] = cnt;
s1[++cnt] = {u,h[v],0};
h[v] = cnt;
}
i64 E[5010][5010];
i64 ans, dis[N];
int b[N];
bool vis[N];
bool bfs(){
for(int i=1; i<=n; i++) vis[i]=0;//记得初始化
queue<int> q;
q.push(s);
vis[s]=1,dis[s]=inf;
while(!q.empty()){
int u = q.front();
q.pop();
for(int i=h[u]; i; i=s1[i].nxt){
if(s1[i].w){
int v = s1[i].to;
if(vis[v])continue;
dis[v]=min(dis[u],s1[i].w);
b[v]=i,vis[v]=1;
q.push(v);
if(v==t) return 1;
}
}
}
return 0;
}
void upd(){//找到一条之后就不断回溯,改一些节点的边
int u=t;
while(u!=s){
int i=b[u];
s1[i].w-=dis[t];
s1[i^1].w+=dis[t];
u=s1[i^1].to;
}
ans+=dis[t];
}
int main(){
cin>>n>>m>>s>>t;
for(int i=1; i<=m; i++){
int x,y,z;
cin>>x>>y>>z;
if(!E[x][y]) add(x,y,z),E[x][y]=cnt;//重边判断
else s1[E[x][y]^1].w+=z;
}
while(bfs())upd();
cout<<ans;
return 0;
}
最小割证明
定义:把一个网络分成两个集合,其中连向 \(S\to T\) 的边,称之为割边,割边容量之和最小值为最小割,可经证明得,\(最大流=最小割\)
证明:
首先,我们需要确定的事实是,这是一个已经被增广完毕的图,残量网络上 \(S \to T\) 不存在
所以我们列出关系式:对于任意的割边 \((A,B)\) 之间,流量肯定会经过一次从 A 到 B 的割边(可以用反证法证明,若有流量还有剩余 \(C(A,B)\) 则 \(> 0\) ,所以 \(A \to B\) 还会存在,与我们设定背景(\(S \to T\) 不存在)相悖,所以证明流量肯定会经过割边,并且不会有剩余流量)所以,我们列出式子:
同时,根据流量守恒,除 \(S\) 与 \(T\) 之外的点,流入=流出,我们将 A 视为超级节点,则 \(A流出=割边总流量=B流入\) ,所以我们列出式子:
最后我们综合式子:
因为任意流 \(f\) 肯定会经过割,并且根据流量守恒,A 或 B 肯定不会出现超过割边总流量的总流量,所以我们得出,对于任意流 \(f\) 其肯定是会小于等于割的流量的,所以:
同时,对于割边流量与容量关系:
所以在此情况下,只有最大流才会等于割边总流,而割边总流最大(最大流)才会等于最小割(最小割的总容量)
证毕:\(最大流=最小割\)

浙公网安备 33010602011771号