网络流总结

引出:

求出一个二分图的最大匹配。

二分图匹配怎么求?

可以用匈牙利算法:遍历每个点,查找增广路,若找到增广路,则修改匹配集合和匹配数;否则终止算法,返回最大匹配数。这样时间复杂度是 \(\mathcal{O}(nm)\) 的。代码实现:

int con[N * N];
bool vis[N * N];

bool Hungary(int u)
{
	for (int i = head[u]; i; i = e[i].nxt)
	{
		int v = e[i].to;
		if (vis[v]) continue;
		vis[v] = 1;
		if (!con[v] || Hungary(con[v]))
		{
			con[v] = u;
			return 1;
		}
	}
	return 0;
}

二分图有很多优秀的性质:

最大匹配数=最小点覆盖数,最大独立集与最大匹配数互补,DAG最小路径覆盖=DAG原图节点数-对应二分图最大匹配。

也就是说用简简单单小 DFS 就能快速地求出这么多问题!但是这不够优秀,我们用网络最大流解决二分图一类的问题可以达到 \(\mathcal{O}(m\sqrt{n})\)。所以在这里介绍一下网络流的解法之类。

正文:

当然网络流不仅局限于二分图的问题,它还比匈牙利更加灵活。

在开始之前,先了解关于网络流的一些基本概念吧。

基本概念:

网络流图:

  1. 有且仅有一个入度为 \(0\) 的源点 \(S\)
  2. 有且仅有一个出度为 \(0\) 的汇点 \(T\)
  3. 每条有向边都有流量上限。

可行流:

  1. 每个点(\(S,T\) 除外)满足流入等于流出,每条边满足流入等于流出且不超过流量上限。
    1.从源点出发的流全经过网络到汇点。

饱和弧:

饱和弧是指在一个可行流中,一条通过流量等于流量上限的边。若是小于,这条边就叫非饱和弧。

零弧:

零弧是指在一个可行流中,一条通过流量为零的边。

前向弧和后向弧:

将网络流图看作是无向图的情况下,前向弧是指一条方向是和源点流向汇点方向一致的边,后向弧则反之。

后向边:

和有向图的有向边方向相反的边。

增广路径:

一段路径满足:

  1. 所有前向弧都是非饱和弧。
  2. 所有后向弧都是非零弧。

残量网络:

对于某一条边,还能再有多少流量经过。

最大流:

每次用 BFS 找一条最短的增广路径,然后沿着这条路径修改流量值(实际修改的是残量网络的边权)。当没有增广路时,算法停止,此时的流就是最大流。

然后这个过程可以用 Dinic 优化:

  1. 分层图优化:Dinic 算法首先对图进行一次 BFS,然后在其生成的分层图中多次 DFS。这样就切断了原有图中的许多不必要连接。这样我们可以进行多路增广,减少了调用增广函数的次数。
  2. 当前弧优化:每次 DFS 完,找到路径中容量最小的一条。在这条边之前的路径的容量大于等于这条边的容量。那么从这条边之前的点,可能引发出别的增广路径。
  3. -1 优化:在同一次 DFS 中,如果从一个点引发不出任何增广路径,就让它在层次图里抹去。

代码:

int n, m, s, t, tot;
struct edge
{
	int y, w, op, next;
} e[M];
int head[N];

void Add(int x, int y, int w)
{
	e[++tot] = (edge){y, w, tot + 1, head[x]}; 
	head[x] = tot;
	e[++tot] = (edge){x, 0, tot - 1, head[y]}; 
	head[y] = tot; 
}

int dis[N];
queue <int> que;

bool bfs()
{
	while(!que.empty())que.pop();
	memset(dis, 60, sizeof(dis));
	dis[s] = 0;
	que.push(s);
	while(!que.empty())
	{
		int x = que.front();que.pop();
		for (int i = head[x]; i; i = e[i].next)
		{
			int y = e[i].y;
			if(dis[y] >= dis[x] + 1 && e[i].w)
			{
				dis[y] = dis[x] + 1;
				if(y == t) return 1;
				que.push(y);
			}
		}
	}
	return 0;
}

ll dfs(int x, ll f)
{
	if(x == t) return f;
	ll sum = 0;
	for (int i = head[x]; i; i = e[i].next)
	{
		int y = e[i].y;
		if(dis[y] == dis[x] + 1 && e[i].w)
		{
			ll f2 = dfs(y, min(e[i].w * 1ll, f - sum));
			if (!f2) dis[y] = -1;         //-1优化
			e[i].w -= f2; 
			e[e[i].op].w += f2;
			sum += f2;
			if (sum == f) break;
		}
	}
	return sum;
}

ll dinic()
{
	ll sum = 0;
	while(bfs()){sum += dfs(s, 1010580540);}
	return sum;
}

最小割:

首先,割是一组边,如果去掉割边,\(S,T\) 就不会联通,那么最小割就是这组边的最小价值。

其次,最小割等于最大流,不会证明。

费用流:

BFS 换成 SPFA。


int n, m, s, t, tot;
struct edge
{
	int y, w, z, op, next;
} e[M];
int head[N];

void Add(int x, int y, int w, int z)
{
	e[++tot] = (edge){y, w, z, tot + 1, head[x]}; 
	head[x] = tot;
	e[++tot] = (edge){x, 0, -z, tot - 1, head[y]}; 
	head[y] = tot; 
}

int dis[N], incf[N], pre[N];
bool vis[N];
queue <int> que;

bool spfa()
{
	while(!que.empty())que.pop();
	memset(dis, 60, sizeof(dis));
	memset(vis, 0, sizeof(vis));
	dis[s] = 0;
	que.push(s);
	vis[s] = 1;
	incf[s] = 1 << 30;
	while(!que.empty())
	{
		int x = que.front();que.pop();vis[x] = 0;
		for (int i = head[x]; i; i = e[i].next)
		{
			int y = e[i].y;
			if(dis[y] > dis[x] + e[i].z && e[i].w)
			{
				dis[y] = dis[x] + e[i].z;
				incf[y] = min(incf[x], e[i].w);
				pre[y] = i;
				if(!vis[y]) vis[y] = 1, que.push(y);
			}
		}
	}
	if (dis[t] == 1010580540)
		return 0;
	else
		return 1;
}

int maxflow, mincost;

void MCMF()
{
	while(spfa())
	{
		int x = t;
		maxflow += incf[t];
		mincost += dis[t] * incf[t];
		int i;
		while(x != s)
		{
			i = pre[x];
			e[i].w -= incf[t];
			e[e[i].op].w += incf[t];
			x = e[e[i].op].y;
		}
	}
}

posted @ 2019-12-27 22:28  Jayun  阅读(215)  评论(0编辑  收藏  举报