网络流 (学习笔记)(26.1.24)

网络流 (学习笔记)

概述

网络流初步学习,首先感觉是一种抽象的图论,把一些权值作为如水一样的流,然后以一些限制作为管道的容量变化。

概念

对于一张有向图,其中含有 \(S\)\(T\) 作为源点,汇点,之后我们以 \(c(x,y)\) 定义为边的容量

我们又称 \(f(x,y)\) 为边的流量,其有以下三个性质:

  1. \(f(x,y)\le c(x,y)\)
  2. \(f(x,y)=-f(y,x)\)
  3. \(正向流向和\) = \(反向流向和\)

此外,我们定义所有节点与剩余容量 \(>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&&sum; 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\) 不存在)相悖,所以证明流量肯定会经过割边,并且不会有剩余流量)所以,我们列出式子:

\[\sum_{(x,y)\in(A,B)}f(x,y) = \sum_{(x,y)\in(A,B)}c(x,y) \]

同时,根据流量守恒,除 \(S\)\(T\) 之外的点,流入=流出,我们将 A 视为超级节点,则 \(A流出=割边总流量=B流入\) ,所以我们列出式子:

\[|f| = \sum_{(x,y)\in(A,B)}f(x,y) \]

最后我们综合式子:

\[|f| = \sum_{(x,y)\in(A,B)}f(x,y) = \sum_{(x,y)\in(A,B)}c(x,y) \]

因为任意流 \(f\) 肯定会经过割,并且根据流量守恒,A 或 B 肯定不会出现超过割边总流量的总流量,所以我们得出,对于任意流 \(f\) 其肯定是会小于等于割的流量的,所以:

\[|f| \le \sum_{(x,y)\in(A,B)}f(x,y) \]

同时,对于割边流量与容量关系:

\[\sum_{(x,y)\in(A,B)}f(x,y) \le \sum_{(x,y)\in(A,B)}c(x,y) \]

所以在此情况下,只有最大流才会等于割边总流,而割边总流最大(最大流)才会等于最小割(最小割的总容量)

证毕:\(最大流=最小割\)

posted @ 2026-01-24 11:31  Yuriha  阅读(29)  评论(0)    收藏  举报