上下界网络流学习笔记

[学习笔记] 有上下界的网络流

upd: 好像这篇博客漏洞百出, 有些定义都是错的, 什么时候有时间了再修锅吧

​ 为什么刚学网络流不久就要学这种东西啊, 我网络流二十四题还没写完呢...

​ 本文是在liu_runda的学习笔记的帮助下写出来的, 在这里先提前声明一下(本文中有很多引用)...

​ 在多篇别人的学习笔记的帮助和我自己的学习意愿下(其实是考试遇到这种题爆零后才去学习的), 我接触了一下有上下界的网络流, 略微对这种流法有所了解, 它大约分为以下几种:

​ 1.无源汇有上下界的网络流:

​ 由于这个部分是下面其他部分的基础, 所以在这里会啰嗦一点... 先看一下模版题的题面(进去有喜), 题面是这样说的: 给你n个点, m条边, 每条边有一个流量下界和一个流量上界, 求一个可行方案使得在所有点满足流量平衡的条件下, 所有边满足流量限制. 我们设每条边(i)的上限和下限分别为(lower(i)), (upper(i))

​ 首先我们知道, 每条边(i)最少都要流(lower(i))这么多, 所以我们可以先让每条边流这么多, 但是, 这样可能不满足网络流定律中的流守恒, 所以我们需要进行某种操作来将它变为可行流. 不妨设流完下界的一个流为初始流, 那么我们可以通过调整初始流使得它变为一个可行流. 这个初始流不一定满足流量守恒,因此最终的可行流一定是在这个初始流的基础上增大了一些边的流量使得所有点满足流量守恒.我们考虑在残量网络中求出一个附加流, 使得初始流与这个附加流合并之后变成可行流. 首先我们确定, 某一条边在初始流和附加流的合并网络中一定是满足流守恒的, 那么我们可以得到以下几个要素:

​ ①: 若当前点在初始流中满足流守恒, 那么他在附加流中一定也满足流守恒(加起来要满足嘛);

​ ②: 若当前点在初始流中流入的流量比流出的流量多(f), 那么在附加流中这个点流出的流量比流入的流量多(f);

​ ③: 若当前点在初始流中流出的流量比流入的流量多(f), 那么在附加流中这个点流入的流量比流出的流量多(f);

​ 其次, 我们需要将这些原图中已经跑完了初始流的边加入到附加流中, 容量为(upper[i] - lower[i]), 我们可以这样认为:在附加流中(u)(v)的一个流量可以看做在原图中的(u -> v)这条边的流量加一. 那么既然我们已经知道了上面三点, 有什么方法可以记录下每个点的流入与流出的流量差呢? 我们可以拿一个数组(F[])来存下每个点流量的差, 对于一条边, 它的起点为(u), 终点为(v), 流量下界为(down), 流量上界为(up), 那么我们就可以让(F[u])-=(down), 让(F[v]) += (down), 意思是在(u)点流出了一个流量为(down)的流, 在(v)点流入了一个流量为(down)的流. 那么对每一条边进行这个操作之后, 我们就可以得出每个点的流入与流出的差. 现在我们要通过这个(F[])数组来操作, 我们知道, dinic的前提条件是有源汇, 但是这个图中并没有源汇, 我们就制造一个源汇, 对于某个点(i), 如果(F[i] < 0), 我们就加入一条从新建的原点s到i的边, 容量为(-F[i]), 如果(F[i] > 0), 我们就加入一条从i​到汇点t的边(我的习惯是原来网络流中的源点和汇点设为S, T, 设s, t是为了与原网络流区分), 为什么要这样加呢???

​ 我们都知道, 对于一个点, 若他在初始流中的流入量大于流出量, 它在附加流中必然会流出量大于流入量, 那么这个点多余的流出的量需要找到一个地方流出去, 所以我们将它向汇点连去, 所以就是(F[i] > 0)时, 从i向t连一条边, 同理, 对于一个点, 如果他在初始流中的流入量小于流出量, 它在附加流中必然会流出量小于流入量, 那么这个点多余的流入的流量需要从一个地方得到, 所以我们将源点与他相连, 所以就是(F[i] < 0)时, 从s向i连一条边, 这样的话, 如果我们能够找到一个流满足附加流满流的话, 就说明这个流是可行的(若未满流, 则通向s或者是通向t的边中至少有一条不是满流, 那么这条边所连的这个除了s或t另外的一个端点i在原图中不满足流守恒, 只有满流了它才能与(F[i])相抵消(互为相反数, 看上面边的容量的定义), 而这里没有满流, 所以这个点不满足流守恒), 也就是说这个图在这个附加流中的流与初始流合并后满足这个图上每一个点都是流守恒的, 那么每一条边的流量便是它的流量下界加上他在dinic中的反向边的流量(即这个点在附加流中的流量).

​ 贴一个模版的代码吧

具体代码

#include <iostream>
#include <cstring>
#include <cstdio>
#include <queue>
#include <cmath>
#define INF 1e9
using namespace std;

int n, m, sum, head[1005], cnt = 1, S, T, d[5005], cur[5005], cha[5005], rem[105000];
struct Edge
{
	int from, to, down, up; 
} e[105kexingliu000];
struct node
{
	int to, flow, next; 
} edge[100005]; 

inline int read()
{
	int x = 0, w = 1;
	char c = getchar();
	while(c < '0' || c > '9') { if (c == '-') w = -1; c = getchar(); }
	while(c >= '0' && c <= '9') { x = x * 10 + c - '0'; c = getchar(); }
	return x * w;
}

inline void add(int u, int v, int w)
{
	edge[++cnt] = { v, w, head[u] }; head[u] = cnt;
	edge[++cnt] = { u, 0, head[v] }; head[v] = cnt; 
}

bool bfs()
{
	memset(d, 0, sizeof(d)); queue<int> q;
	q.push(S); d[S] = 1;
	while(!q.empty())
	{
		int u = q.front(); q.pop();
		for(int i = head[u]; i; i = edge[i].next)
		{
			int v = edge[i].to;
			if(!d[v] && edge[i].flow > 0) { d[v] = d[u] + 1; q.push(v); }
		}
	}
	return d[T]; 
}

int dfs(int u, int a)
{
	if(u == T || !a) return a;
	int flow = 0;
	for(int &i = cur[u]; i; i = edge[i].next)
	{
		int v = edge[i].to;
		if(d[v] == d[u] + 1 && edge[i].flow > 0)
		{
			int f = dfs(v, min(a, edge[i].flow));
			a -= f; flow += f; edge[i].flow -= f; edge[i ^ 1].flow += f; 
		}
		if(!a) break; 
	}
	if(a) d[u] = -1;
	return flow; 
}

int dinic()
{
	int flow = 0;
	while(bfs())
	{
		memcpy(cur, head, sizeof(head));
		flow += dfs(S, INF); 
	}
	return flow; 
}

int main()
{
	n = read(); m = read(); S = n + 1; T = n + 2; 
	for(int i = 1; i <= m; i++)
	{
		int u = read(), v = read(), down = read(), up = read();
		e[i] = { u, v, down, up }; cha[u] -= down; cha[v] += down; 
	}
	int sum = 0; 
	for(int i = 1; i <= m; i++)
	{
		add(e[i].from, e[i].to, e[i].up - e[i].down);
		rem[i] = cnt; 
	}
	for(int i = 1; i <= n; i++)
	{
		if(cha[i] < 0) { add(i, T, -cha[i]); }
		if(cha[i] > 0) { sum += cha[i]; add(S, i, cha[i]); } 
	}
	if(dinic() != sum) puts("NO");
	else
	{
		puts("YES");
		for(int i = 1; i <= m; i++) printf("%d
", e[i].down + edge[rem[i]].flow); 
	}
	return 0;
}

​ 2.有源汇有上下界的可行流:

​ 似乎没找到题目, 讲一下题面吧: 给定一个网络, 网络有一个源点(S), 一个汇点(T), 除源点和汇点不满足流量守恒外, 其他的点都满足流量守恒并且每条边的流量要大于等于它的上界, 小于等于它的下界, 问是否能找到一个满足条件的可行流.

​ 源点和汇点不满足流守恒, 我们想办法让他满足流守恒, 我们知道这样一个等式, 源点流出的流量等于汇点流入的流量, 所以, 我们可以从汇点向源点连一条容量为INF的边, 这样就从有源汇的转化为了无源汇的上下界网络流, 这样就与上面相同了, 拆掉从汇点向源点的边之后就变成了有源汇的可行流.

​ 在这里, 最后得到的流量其实就是从源点流出的流量, 也就是从汇点流入的流量, 也就是汇点向源点连的那一条边的流量, 所以我们最后得到的有上下界可行流的流量就是这个了.

​ 这个是下面有源汇上下界最大流和最小流的基础.

具体代码

#include <iostream>
#include <cstring>
#include <cstdio>
#include <queue>
#include <cmath>
#define INF 1e9
using namespace std;

int n, m, sum, head[1005], cnt = 1, S, T, d[5005], cur[5005], cha[5005], rem[105000];
struct Edge
{
	int from, to, down, up; 
} e[105kexingliu000];
struct node
{
	int to, flow, next; 
} edge[100005]; 

inline int read()
{
	int x = 0, w = 1;
	char c = getchar();
	while(c < '0' || c > '9') { if (c == '-') w = -1; c = getchar(); }
	while(c >= '0' && c <= '9') { x = x * 10 + c - '0'; c = getchar(); }
	return x * w;
}

inline void add(int u, int v, int w)
{
	edge[++cnt] = { v, w, head[u] }; head[u] = cnt;
	edge[++cnt] = { u, 0, head[v] }; head[v] = cnt; 
}

bool bfs()
{
	memset(d, 0, sizeof(d)); queue<int> q;
	q.push(S); d[S] = 1;
	while(!q.empty())
	{
		int u = q.front(); q.pop();
		for(int i = head[u]; i; i = edge[i].next)
		{
			int v = edge[i].to;
			if(!d[v] && edge[i].flow > 0) { d[v] = d[u] + 1; q.push(v); }
		}
	}
	return d[T]; 
}

int dfs(int u, int a)
{
	if(u == T || !a) return a;
	int flow = 0;
	for(int &i = cur[u]; i; i = edge[i].next)
	{
		int v = edge[i].to;
		if(d[v] == d[u] + 1 && edge[i].flow > 0)
		{
			int f = dfs(v, min(a, edge[i].flow));
			a -= f; flow += f; edge[i].flow -= f; edge[i ^ 1].flow += f; 
		}
		if(!a) break; 
	}
	if(a) d[u] = -1;
	return flow; 
}

int dinic()
{
	int flow = 0;
	while(bfs())
	{
		memcpy(cur, head, sizeof(head));
		flow += dfs(S, INF); 
	}
	return flow; 
}

int main()
{
	n = read(); m = read(); S = n + 1; T = n + 2; 
	for(int i = 1; i <= m; i++)
	{
		int u = read(), v = read(), down = read(), up = read();
		e[i] = { u, v, down, up }; cha[u] -= down; cha[v] += down; 
	}
	int sum = 0; 
	for(int i = 1; i <= m; i++)
	{
		add(e[i].from, e[i].to, e[i].up - e[i].down);
		rem[i] = cnt; 
	}
	for(int i = 1; i <= n; i++)
	{
		if(cha[i] < 0) { add(i, T, -cha[i]); }
		if(cha[i] > 0) { sum += cha[i]; add(S, i, cha[i]); } 
	}
    add(T, S, INF); 
	if(dinic() != sum) puts("NO");
	else
	{
		puts("YES");
		for(int i = 1; i <= m; i++) printf("%d
", e[i].down + edge[rem[i]].flow); 
	}
	return 0;
}

有没有发现就是从上面蒯下来的加了句话...

​ 3.有源汇上下界最大流:

​ 直接给题面吧, 我实在是不想写这些叽叽喳喳的东西了. 首先跑一遍有源汇的可行流, 再在残量网络上跑一遍s - t最大流即可, 此时的流量为可行流流量加上残量网络上的流量.

具体代码

#include <iostream>
#include <cstring>
#include <cstdio>
#include <queue>
#define N 505
#define INF 1e9
using namespace std;

int n, m, s, t, S, T, head[N], cnt = 1, F[N], sum, u[20005], v[20005], low[20005], up[20005], cur[N], d[N]; 
struct node
{
	int from, to, flow, next; 
} edge[100005]; 

inline int read()
{
	int x = 0, w = 1;
	char c = getchar();
	while(c < '0' || c > '9') { if (c == '-') w = -1; c = getchar(); }
	while(c >= '0' && c <= '9') { x = x * 10 + c - '0'; c = getchar(); }
	return x * w;
}

inline void add(int u, int v, int w)
{
	edge[++cnt] = { u, v, w, head[u] }; head[u] = cnt;
	edge[++cnt] = { v, u, 0, head[v] }; head[v] = cnt; 
}

bool bfs(int S, int T)
{
	memset(d, 0, sizeof(d)); d[S] = 1;
	queue<int> q; q.push(S);
	while(!q.empty())
	{
		int u = q.front(); q.pop();
		for(int i = head[u]; i; i = edge[i].next)
		{
			int v = edge[i].to;
			if(!d[v] && edge[i].flow > 0) { d[v] = d[u] + 1; q.push(v); }
		}
	}
	return d[T]; 
}

int dfs(int u, int S, int T, int a)
{
	if(u == T || !a) return a;
	int flow = 0;
	for(int &i = cur[u]; i; i = edge[i].next)
	{
		int v = edge[i].to;
		if(d[v] == d[u] + 1 && edge[i].flow > 0)
		{
			int f = dfs(v, S, T, min(a, edge[i].flow));
			a -= f; flow += f; edge[i].flow -= f; edge[i ^ 1].flow += f; 
		}
		if(!a) break; 
	}
	if(a) d[u] = -1;
	return flow; 
}

int dinic(int S, int T)
{
	int flow = 0;
	while(bfs(S, T))
	{
		memcpy(cur, head, sizeof(head));
		flow += dfs(S, S, T, INF); 
	}
	return flow; 
}

int main()
{
	n = read(); m = read(); s = read(); t = read(); 
	for(int i = 1; i <= m; i++)
	{
		u[i] = read(), v[i] = read(), low[i] = read(), up[i] = read();
		add(u[i], v[i], up[i] - low[i]); F[u[i]] -= low[i]; F[v[i]] += low[i]; 
	}
	S = n + 1; T = S + 1;
	for(int i = 1; i <= n; i++)
	{
		if(F[i] < 0) add(i, T, -F[i]); 
		if(F[i] > 0) { sum += F[i]; add(S, i, F[i]); }
	}
	add(t, s, INF); 
	if(dinic(S, T) != sum) { puts("please go home to sleep"); return 0; }
	printf("%d
", dinic(s, t)); 
	return 0;
}

我没有删掉t - s那条边是因为它的反向边走了之后的最大流其实就相当于记录下这条边的流量并断掉这条边之后的最大流与这条边的流量之和, 其实就相当于s - t可以直接走这么多.

​ 4.有源汇上下界最小流:

题面, 似乎并没有已知的方法满足求最小流啊, 我们还是先将一个可行流跑出来, 要使流量最小又满足要求, 我们可以逐渐减少某些点的流量, 但又使他满足流守恒, 这样就可以减少流量, 但是好像也没有方法可以这样做.

​ 我们需要知道一个东西, 反向边建出来肯定是有它的作用的, 在dinic中它用来'反悔', 而在这里, 我们知道, 正向边流量减少, 也就意味着正向边残量增多, 也就是就意味着反向边残量减少, 所以我们只要使得反向边的残量减少, 对于某条边的残量减少, 你想到了什么呢, 没错, 正是最大流, 从s - t跑最大流意味的是正向边残量减少, 反向边残量增多, 那么从t - s跑最大流就是相反的, '正向边'(也就是原图中真正的反向边)残量减少, '反向边'残量增多, 这样就可以达到一个退流的效果, 也就是说, 我们将可行流的流量减去t - s最大流的流量就是最小流的流量, 有人会这样想, 有没有可能不满足流量下界呢, 答案是否定的, 我们在建图前就将所有边的下界作为初始流, 之后建图的时候我们建的是上界减下界为容量的网络图, 即流满下界后还需要流多少流量, 前提条件是下界已经满了, 也就是说, 只有当流量为负时, 他才有可能影响到原来的下界的流量, 而这是不可能的. 至于每个点是否满足流量平衡, 这是肯定满足的, 如果你这个点的流入量减少了, 它的流出量也必然会减少, 不然流入提供不了流出这么多, 所以流出和流入的差其实是不会变的, 因为他们的变化量相同, 减去就抵消了.

具体代码

#include <iostream>
#include <cstring>
#include <cstdio>
#include <queue>
#define N 60005
#define INF 1e12
#define int long long
using namespace std;

int n, m, s, t, S, T, head[N], cnt = 1, F[N], sum, u[130005], v[130005], low[130005], up[130005], cur[N], d[N]; 
struct node
{
	int from, to, flow, next; 
} edge[1000005]; 

inline int read()
{
	int x = 0, w = 1;
	char c = getchar();
	while(c < '0' || c > '9') { if (c == '-') w = -1; c = getchar(); }
	while(c >= '0' && c <= '9') { x = x * 10 + c - '0'; c = getchar(); }
	return x * w;
}

inline void add(int u, int v, int w)
{
	edge[++cnt] = { u, v, w, head[u] }; head[u] = cnt;
	edge[++cnt] = { v, u, 0, head[v] }; head[v] = cnt; 
}

bool bfs(int S, int T)
{
	memset(d, 0, sizeof(d)); d[S] = 1;
	queue<int> q; q.push(S);
	while(!q.empty())
	{
		int u = q.front(); q.pop();
		for(int i = head[u]; i; i = edge[i].next)
		{
			int v = edge[i].to;
			if(!d[v] && edge[i].flow > 0) { d[v] = d[u] + 1; q.push(v); }
		}
	}
	return d[T]; 
}

int dfs(int u, int S, int T, int a)
{
	if(u == T || !a) return a;
	int flow = 0;
	for(int &i = cur[u]; i; i = edge[i].next)
	{
		int v = edge[i].to;
		if(d[v] == d[u] + 1 && edge[i].flow > 0)
		{
			int f = dfs(v, S, T, min(a, edge[i].flow));
			a -= f; flow += f; edge[i].flow -= f; edge[i ^ 1].flow += f; 
		}
		if(!a) break; 
	}
	if(a) d[u] = -1;
	return flow; 
}

int dinic(int S, int T)
{
	int flow = 0;
	while(bfs(S, T))
	{
		memcpy(cur, head, sizeof(head));
		flow += dfs(S, S, T, INF); 
	}
	return flow; 
}

signed main()
{
	n = read(); m = read(); s = read(); t = read(); 
	for(int i = 1; i <= m; i++)
	{
		u[i] = read(), v[i] = read(), low[i] = read(), up[i] = read();
		add(u[i], v[i], up[i] - low[i]); F[u[i]] -= low[i]; F[v[i]] += low[i]; 
	}
	S = n + 1; T = S + 1;
	for(int i = 1; i <= n; i++)
	{
		if(F[i] < 0) add(i, T, -F[i]); 
		if(F[i] > 0) { sum += F[i]; add(S, i, F[i]); }
	}
	add(t, s, INF); 
	if(dinic(S, T) != sum) { puts("please go home to sleep"); return 0; }
	sum = edge[cnt].flow; edge[cnt].flow = edge[cnt ^ 1].flow = 0; 
	printf("%lld
", sum - dinic(t, s)); 
	return 0;
}

又是把上面的蒯下来了改一改哈哈...

​ 5.有源汇上下界最小 / 最大费用可行流:

​ 在满足上下界限制的同时使得费用最小, 即在附加流上满足满流又费用最小, 已知附加流有源点和汇点, 所以, 在附加流上跑一遍最小 / 最大费用最大流, 最后答案再加上每条边的下界乘上当前边费用即可.

​ 这里提供某谷4043支线剧情的代码(我才不会告诉你们这就是我考试爆零的那道题), 这个是有源汇最小费用可行流, 最大费用的话初始化改一下就好了...

具体代码

#include <iostream>
#include <cstring>
#include <cstdio>
#include <queue>
#define INF 1e9
#define N 505
#define int long long
using namespace std;

int n, s, t, S, T, cha[N], head[N], cnt = 1, p[N], vis[N];
struct node
{
	int from, to, next;
	long long flow, cost; 
} edge[1000005]; 
long long ans = 0, a[N], d[N]; 

inline int read()
{
	int x = 0, w = 1;
	char c = getchar();
	while(c < '0' || c > '9') { if (c == '-') w = -1; c = getchar(); }
	while(c >= '0' && c <= '9') { x = x * 10 + c - '0'; c = getchar(); }
	return x * w;
}

inline void add(int u, int v, long long flow, long long w)
{
	edge[++cnt] = { u, v, head[u], flow, w }; head[u] = cnt;
	edge[++cnt] = { v, u, head[v], 0, -w }; head[v] = cnt; 
}

int SPFA(long long &flow, long long &cost)
{
	memset(d, 0x3f, sizeof(d)); d[S] = 0; vis[S] = 1; queue<int> q; q.push(S);
	memset(a, 0x3f, sizeof(a)); memset(p, 0, sizeof(p)); memset(vis, 0, sizeof(vis));
	while(!q.empty())
	{
		int u = q.front(); q.pop(); vis[u] = 0;
		for(int i = head[u]; i; i = edge[i].next)
		{
			int v = edge[i].to;
			if(d[v] > d[u] + edge[i].cost && edge[i].flow > 0)
			{
				d[v] = d[u] + edge[i].cost; a[v] = min(a[u], edge[i].flow);
				p[v] = i; if(!vis[v]) { vis[v] = 1; q.push(v); }
			}
		}
	}
	if(d[T] == d[0]) return 0;
	flow += a[T]; cost += (a[T] * d[T]);
	for(int i = T; i != S; i = edge[p[i]].from)
	{
		edge[p[i]].flow -= a[T]; edge[p[i] ^ 1].flow += a[T]; 
	}
	return 1; 
}

signed main()
{
	n = read();
	t = n + 1; s = 1; S = t + 1; T = t + 2; 
	for(int u = 1; u <= n; u++)
	{
		int p = read();
		for(int j = 1; j <= p; j++)
		{
			int v = read(), w = read();
			cha[u]--; cha[v]++; ans += w; 
			add(u, v, INF, w); 
		}
	}
	for(int i = 2; i <= n; i++) add(i, t, INF, 0);
	add(t, s, INF, 0); 
	for(int i = 1; i <= n; i++)
	{
		if(cha[i] > 0) add(S, i, cha[i], 0); 
		if(cha[i] < 0) add(i, T, -cha[i], 0); 
	}
	long long flow = 0, cost = 0;
	while(SPFA(flow, cost));
	printf("%lld
", 1ll * (ans + cost)); 
	return 0;
}

​ 6.有源汇上下界最小 / 最大费用最大流:

​ 首先跑出有源汇上下界最小 / 最大费用可行流, 然后在残量网络上跑出s - t的最小 / 最大费用最大流即可.

​ 有源汇的上面有代码, 最小 / 最大费用最大流都是板子, 自己去敲吧...

​ 7.有源汇上下界最小 / 最大费用最小流:

​ 还是用可行流减去t - s的最大流, 对于最小费用最小流, 首先跑出有源汇上下界最小费用可行流, 再跑出t - s最大费用最大流即可, 正确性在上面已经证明过, 这里只是加了个费用而已, t - s的费用肯定小于可行流的最小费用, 因为他是在可行流的基础上进行的, 所以最小费用是一个已经求出的已知常量, 减去可行的最大费用, 也就是t - s的最大费用最大流的费用就可以使得最终费用最小了; 对于最大费用最小流, 跑出最大费用可行流, 减去t - s最小费用最大流即可.

​ 都是板子, 跟上面一样(其实是我不想写了)

​ 好像还有一些无源汇的费用流, 大家可以自己去推一下, 反正都是在可行流的基础上进行一些其他的操作, 应该还是不难的.

差不多完了, 可以按下键盘上的Ctrl + w自行退出了

原文地址:https://www.cnblogs.com/ztlztl/p/10565775.html