0. 前置芝士

  • \(G(f)\) 为流了流 \(f\) 之后的残留网络。
  • 定义两个流的加法 \((f+g)(i,j)=f(i,j)+g(i,j)-g(j,i)\)。其中 \(f\) 先流,于是 \(g(j,i)\) 相当于模拟退流的过程。
  • 可行流可加性定理:若 \(f\)\(G\) 的可行流,\(g\)\(G(f)\) 的可行流,则 \(f+g\)\(G\) 的可行流,且 \(v(f+g)=v(f)+v(g)\)

0.1. 最大流最小割定理

对于任意割 \(\mathfrak{G}\),都可以将点集 \(\mathfrak{V}\) 分成 \(\mathfrak{S,T}\),它们分别包含源点和汇点。

感性来看,一定有割 \(\mathfrak{G}\) 的净流量 \(=\) 源点输出流量 \(=\) 网络总流量。同时,由于割 \(\mathfrak{G}\) 的净流量一定 \(\le\)\(\mathfrak{G}\) 的容量,我们得出网络总流量 \(\le\)\(\mathfrak{G}\) 的容量。

假设在经过多次增广后,网络上没有增广路了 —— 此时点集 \(\mathfrak{V}\) 自然地分割成 \(\mathfrak{S,T}\),这两个点集之间的边要么不存在,要么残余流量为零。

那么这些边就是一种割,而且此时割的净流量 \(=\) 割的容量 \(=\) 网络总流量。

下面这张图可以帮助理解网络总流量和割的容量的大小关系。所以此时正好取到最大流和最小割(这指的是容量),故最大流最小割定理得证。另外,这还能证明当没有增广路径时取得最大流。

1. 最大流

1.1. \(\mathtt{EK}\) 算法

1.1.1. 算法流程

首先需要得知 "当无法进行增广时取得最大流",这个已经在上文 0.1. 最大流最小割定理 中得知。

对于每次增广,跑一遍 \(\rm bfs\) 找能够增广的 边数最短 的路径,然后更新边的残余流量。

1.1.2. 理解

\(lef_i\) 表示这一次增广从源点到点 \(i\) 的流量限制。

对反向边的理解:假设有两条增广路分别经过了正向边 \((u,v)\) 与反向边,实际上就是把流出去的流量的一部分或全部推回点 \(u\),按另一条路径行进。

初始化时 \(flow\) 表示边的残余流量,正向边为 \(w\),反向边为零。

1.1.3. 时间复杂度

不明白,是 \(\mathcal O(nm^2)\)。处理数据规模为 \(10^3-10^4\)

1.1.4. 代码

void addEdge(int u,int v,int w) {
	nxt[++cnt]=head[u],to[cnt]=v,flow[cnt]=w,head[u]=cnt;
}

bool bfs() {
	while(!q.empty()) q.pop();
	rep(i,1,n) vis[i]=0;
	q.push(S),vis[S]=1,lef[S]=inf;
	while(!q.empty()) {
		int u=q.front(); q.pop();
		erep(i,u) 
			if(!vis[v] && flow[i]) {
				lef[v]=Min(lef[u],0ll+flow[i]);
				q.push(v),pre[v]=i,vis[v]=1;
				if(v==T) return 1;
			}
	}
	return 0;
}

void EK() {
	while(bfs()) {
		int x=T; MaxFlow+=lef[x];
		while(x^S) {
			flow[pre[x]]-=lef[T],flow[pre[x]^1]+=lef[T];
			x=to[pre[x]^1];
		}
	}
}

int main() {
	rep(i,1,m) 
		u=read(9),v=read(9),w=read(9),
		addEdge(u,v,w),addEdge(v,u,0);
	EK();
	print(MaxFlow,'\n');
	return 0;
} 

1.2. \(\mathtt{Dinic}\) 算法

1.2.1. 算法流程

其实就是对于 \(\mathtt{EK}\) 算法的一些改进:

  • 多路增广:当点 \(u\) 通过 \(\langle u,v\rangle\) 增广之后,还剩下一些流没有用,可以尝试再增广其它的边。
  • 有的时候会发生 "绕路" 的情况,我们考虑先用一次 \(\rm bfs\) 将图分层,增广的时候严格按照分层进行。

1.2.2. 一些优化

  • 当一个点 \(u\) 到汇点不存在可行流时,将 \(u\) 的层数置为 \(+\infty\),这样就避免不必要的增广。
  • 当前弧优化:完全遍历过的边必然增广完成,可以跳过。注意最后一次增广使用的边不能跳过,每次分层之后重置当前弧。

1.2.3. 代码

\(\text{Dinic}\) 有些时间复杂度证明要求 \(\text{bfs}\) 增广完成,但是提前 return 亲测更快也不知道为什么。这就是玄学复杂度吧。

bool bfs() {
    for(int i=1;i<=T;++i)
        dep[i]=inf;
    while(!q.empty()) q.pop();
    q.push(S),arc[S]=head[S],dep[S]=0;
    while(!q.empty()) {
        int u=q.front(),v; q.pop();
        for(int i=head[u];~i;i=e[i].nxt) 
            if(e[i].w and dep[v=e[i].to]==inf) {
                dep[v] = dep[u]+1;
                arc[v] = head[v], q.push(v);
                if(v==T) return true;
            }
    }
    return false;
}

int dfs(int u,int canFlow) {
    if(u==T) return canFlow;
    int sumFlow=0,d,v;
    for(int i=arc[u];~i;i=e[i].nxt) {
        arc[u]=i;
        if(e[i].w and dep[v=e[i].to]==dep[u]+1) {
            d = dfs(v,min(canFlow,e[i].w));
            if(!d) dep[v]=inf;
            e[i].w -= d, e[i^1].w += d;
            canFlow -= d, sumFlow += d;
            if(!canFlow) break;
        }
    }
    return sumFlow;
}

int Dinic() {
    int ret=0;
    while(bfs()) ret += dfs(S,inf);
    return ret;
}

1.2.4. 时间复杂度

戳这,可能还有这个。另外,对于二分图,时间复杂度为 \(\mathcal O(n\sqrt m)\). 普通是 \(\mathcal{O}(n^2m)\) 的,好像加上当前弧优化才是对的。

对于简单容量网络(每一个不是源/汇的点,要么入度为 \(1\),要么出度为 \(1\)):\(\mathcal O(nm\sqrt n)\).

边容量为 \(1\) 的图:\(\mathcal O(\min\{n^{\frac{2}{3}},m^\frac{1}{2}\}\cdot nm)\).

2. 最小费用最大流

复杂度是 \(\cal O(nmf)\) 的,但是时间期望复杂度是 \(\mathcal O(Amf)\),其中 \(A\) 为所有顶点入队的平均次数,可以证明 \(A\) 一般小于等于 \(2\).

(当然这一段都是我抄的。

2.1. \(\mathtt{EK}\) 算法 \(+\) \(\text{dijkstra}\)

2.1.1. 代码

每次增广用 \(\text{Dijkstra}\) 找最短路,然后改变权值。由于不能有负权边,所以定义势函数 \(h(i)\) 等于上次增广的 \(dis_i\),令这次的边权为 \(h(u)-h(v)+cost(u,v)\),根据最短路的转移易知边权恒大于等于零。

虽然下面代码没写,但是第一次要跑一次 \(\text{spfa}\)

void addEdge(int u,int v,int w,int c) {
	nxt[++cnt]=head[u],to[cnt]=v,flow[cnt]=w,Cost[cnt]=c,head[u]=cnt;
	nxt[++cnt]=head[v],to[cnt]=u,flow[cnt]=0,Cost[cnt]=-c,head[v]=cnt;
}

bool Dijkstra() {
	rep(i,1,n) dis[i]=inf;
	q.push(make_pair(0,S)); dis[S]=0;
	while(!q.empty()) {
		Pair t=q.top(); q.pop();
		if(dis[t.second]<t.first) continue;
		int u=t.second;
		erep(i,u) 
			if(flow[i]>0 && dis[v]>dis[u]+h[u]-h[v]+Cost[i]) {
				dis[v]=dis[u]+h[u]-h[v]+Cost[i];
				pred[v]=u,pree[v]=i;
				q.push(make_pair(dis[v],v));
			}
	}
	return dis[T]^inf;
}

void EK() {
	int d,MaxFlow=0,MinCost=0;
	while(Dijkstra()) {
		rep(i,1,n) h[i]+=dis[i];
		// 此时比真实多 h[S]-h[i],所以将 h[i]+dis[i] 得到真实 dis
		d=inf;
		for(int i=T;i^S;i=pred[i])
			d=Min(d,flow[pree[i]]);
		for(int i=T;i^S;i=pred[i])
			flow[pree[i]]-=d,flow[pree[i]^1]+=d;
		MaxFlow+=d,MinCost+=d*h[T];
	}
	print(MaxFlow,' '),print(MinCost,'\n');
}

2.2. \(\mathtt{dinic}\) 算法 \(+\) \(\text{dijkstra}\)

2.2.1. 代码

\(\bf{Warning}\):如果 \(c_i\ge 0\) 就可能有零环的情况,此时 dfs() 可能进入死循环。所以需要一个 vis[] 来标记是否访问某点。

bool Dijkstra() {
	rep(i,1,n) dis[i]=inf,vis[i]=0;
	q.push(make_pair(0,S)); dis[S]=0,arc[S]=head[S];
	while(!q.empty()) {
		Pair t=q.top(); q.pop();
		if(vis[t.second] or dis[t.second]<t.first) continue;
		int u=t.second; vis[u]=1;
		erep(i,u) 
			if(flow[i]>0 && dis[v]>dis[u]+h[u]-h[v]+Cost[i]) {
				dis[v]=dis[u]+h[u]-h[v]+Cost[i];
				arc[v]=head[v],q.push(make_pair(dis[v],v));
			}
	}
	bool ok=(dis[T]^inf);
	if(ok) {
		rep(i,1,n) 
			vis[i]=0,
			dis[i]=h[i]=h[i]+dis[i];
		return 1;
	}
	return 0;
}

int dfs(int u,int CanFlow) {
	vis[u]=1;
	if(u==T) return MaxFlow+=CanFlow,CanFlow;
	int SumFlow=0,d;
	for(int i=arc[u];i;i=nxt[i]) {
		int v=to[i]; arc[u]=i;
		if(!vis[v] && flow[i]>0 && dis[v]==dis[u]+Cost[i]) {
			d=dfs(v,Min(CanFlow,flow[i]));
			if(!d) dis[v]=inf;
			MinCost+=Cost[i]*d;
			flow[i]-=d,flow[i^1]+=d;
			SumFlow+=d,CanFlow-=d;
			if(!CanFlow) break;
		}
	}
	return SumFlow;
}

void Dinic() {
	while(Dijkstra()) dfs(S,inf);
	print(MaxFlow,' '),print(MinCost,'\n');
}

2.3. 原始对偶(\(\text{Primal-Dual}\))算法

(我应该妹咋学懂,但是我已经会敲板子了(满足

\(\rm zkw\) 说:"这个特殊的原始对偶算法在稠密二分费用小的图上不敌原来的 \(\rm zkw\) 算法,但远远胜过暴力 \(\rm spfa\). 在另外的图上,对两者都是稳胜。"

具体实现就是先用 \(\rm spfa\) 跑一遍 从汇点开始 的最短路[1],处理出每个点的距离标号,然后从源点开始跑 \(\rm dinic\)dfs() 操作。这个算法的优势在哪里呢?在于距离标号的调整。

首先需要明确,"增广" 究竟是怎样进行的?考虑朴素 \(\rm dinic\) 算法,假设有有向边 \(\langle u,v \rangle\),那么跑一遍 \(\rm spfa\) 后,一定满足 \(d_v\le d_u+c_{u,v}\). 增广的时候,我们挑选 \(d_v=d_u+c_{u,v}\) 的边组成的连接源汇的路径并流出流量,一直到不能增广为止。接着我们重新跑一遍最短路,处理出新的 \(d\) 数组,再继续增广。

而对于原始对偶算法,由于从汇点开始跑,在跑一遍 \(\rm spfa\) 后,一定满足 \(d_u\le d_v+c_{u,v}\). 增广的时候,还是挑选 \(d_u=d_v+c_{u,v}\) 的边组成的连接源汇的路径并流出流量,一直到不能增广为止。此时我们调整距离标号:记被 dfs() 到且满足 \(d_u=d_v+c_{u,v}\) 的点 \(u\) 的点集为 \(V\),给 \(\forall u\in V\)\(d\) 都增加一个 \(\delta\),我们希望操作之后对每条边仍满足 \(d_u\le d_v+c_{u,v}\),且 \(d_u= d_v+c_{u,v}\) 的边增加,从而拓展出新的增广路。

构造 \(\delta=\min\{d_v+c_{u,v}-d_u\}\ \ (u\in V,v\notin V)\)(我更愿意认为这是一个尝试后的结果),那么对于图上的所有边:

  • \(u\in V,v\in V\):毫无影响;
  • \(u\notin V,v\notin V\):毫无影响;
  • \(u\in V,v\notin V\):因为 \(d_u+(d_v+c_{u,v}-d_u)=d_v+c_{u,v}\),而 \(\delta\le d_v+c_{u,v}-d_u\),所以满足 \(d_u+\delta\le d_v+c_{u,v}\). 特别地,当 \(\delta= d_v+c_{u,v}-d_u\) 时,这条边被拓展成了最短路上的边;
  • \(u\notin V,v\in V\):显然满足,因为 \(\delta\ge 0\).

正确性意会即可(?

经过测试,原始对偶算法同样可以在包含负权边的图中运行,因此也可以求解最大费用最大流。

void spfa() {
    for(int i=0;i<=T;++i)
        dis[i]=infty, vis[i]=0;
	q.push_back(T), vis[T]=1, dis[T]=0;
    while(!q.empty()) {
        int u=q.front(); q.pop_front(); vis[u]=0;
        for(int i=head[u]; ~i; i=e[i].nxt) {
            int v=e[i].to;
            if(e[i^1].w && dis[v]>dis[u]-e[i].c) {
                dis[v] = dis[u]-e[i].c; 
                if(!vis[v]) {
					vis[v]=1;
					if(!q.empty() && dis[q.front()]>dis[v])
						q.push_front(v);
					else q.push_back(v);
				}
            }
        }
    }
}

int dfs(int u,int canFlow) {
    if(u==T) return canFlow;
    int sumFlow=0, d; vis[u]=1;
    for(int i=head[u]; ~i; i=e[i].nxt) {
        int v=e[i].to; 
        if(!vis[v] && e[i].w && dis[v]==dis[u]-e[i].c) {
            d = dfs(v,min(canFlow,e[i].w));
            ans += d*e[i].c; 
            canFlow -= d, sumFlow += d;
            e[i].w -= d, e[i^1].w += d;
            if(!canFlow) break;
        }
    }
    return sumFlow;
}

bool adjust() {
	int inc=infty;
	for(int i=0;i<=T;++i) if(vis[i]) 
		for(int j=head[i]; ~j; j=e[j].nxt) {
			int v=e[j].to;
			if(!vis[v] && e[j].w)
				inc = min(inc,dis[v]-dis[i]+e[j].c);
		}
	if(inc==infty) return false;
	for(int i=0;i<=T;++i)
		if(vis[i]) dis[i] += inc;
	return true;
}

void dinic() { 
	spfa();
	do 
		do memset(vis,0,sizeof(bool)*(T+3));
		while(dfs(S,infty));
	while(adjust());
}

3. 如何建图

戳我看题目合集~


  1. 从源点开始也是可行的,但是效率不如从汇点开始高,这是因为从汇点开始能提高 dfs() 的效率,省去到不了汇点的路径。并且需要注意的是,\(\rm spfa\) 使用 \(\rm SLF\) 优化能提高效率。 ↩︎

posted on 2021-11-09 19:50  Oxide  阅读(201)  评论(0编辑  收藏  举报