P4722 【模板】最大流加强版 / 预流推进

15 篇文章 0 订阅
10 篇文章 0 订阅

本题解侧重解释 HLPP \text{HLPP} HLPP 细节部分,而对于 HLPP \text{HLPP} HLPP 的详细思路等不会过多阐述。

HLPP \text{HLPP} HLPP 思路

按照众多大佬所说的那样, HLPP \text{HLPP} HLPP 就是将水流自源点一步一步地推到其他中转点,最后推向汇点,汇点累计的流量就是该网络的最大流。

注意,在此算法中,我们的目标是图中所有中转点最后存储的流量为 0。

在此过程中,为了防止 TLE \text{TLE} TLE,也就是避免两个点互相不停地将水流推来推去的情况,我们给每一个节点一个高度,像我们在 dinic \text{dinic} dinic 算法中所做的那样,每次保证是从当前层推向下一层。同时,也要保证每次从高度最高的节点向低处节点推流,节省时间(这也是使用优先队列的原因)。

那为什么它会比其他解决最大流问题的算法快呢?

首先,在这里我们只用运行一次 bfs \text{bfs} bfs,在给每一个节点初始了一个高度之后,后面如果它这个点储存的流传不出去了,也只需要针对它单独更改其高度即可。

其次,因为它是一步一步将流向下推,所以不用多次 dfs \text{dfs} dfs 去寻找增广路径。

代码实现细节解释

bfs \text{bfs} bfs 初始高度

相比先给每一个节点初始高度为 0,用 dinic \text{dinic} dinic 中的手段给每个节点的高度初始为它距离终点的最短距离,明显会大大地减少后期更新其高度的次数。

具体代码如下:

inline bool bfs()
{
	rep(i, 1, n) h[i] = inf;
	h[t] = 0, q.push(t);
	while(!q.empty())
	{
		int u = q.front();
		q.pop();
		for(int i = hd[u], v; i; i = e[i].nxt)
			if(e[i ^ 1].f and h[v = e[i].to] > h[u] + 1)
				h[v] = h[u] + 1, q.push(v);
	}
	return h[s] == inf ? 0 : 1;
}
  1. 为什么是 if(e[i ^ 1].f) 而不是 if(e[i].f) 呢?

    要注意因为我们是从终点反向去给每个点初始高度的,所以这时候我们走的边都是反向边。而这里是判断该两点之间正向边的流量是否为 0。

  2. 为什么是 if(h[v] > h[u] + 1) 而不是 if(h[v] > h[u] - 1) 呢?

    原因同上,因为我们在走反向边,此时 v v v 更靠近 s s s,高度更高。

HLPP \text{HLPP} HLPP 函数

inline int hlpp()
{
	if(!bfs()) return 0;
	h[s] = n;
	for(int i = hd[s], v, d; i; i = e[i].nxt)
	{
		if(d = e[i].f)
		{
			p[v = e[i].to] += d, p[s] -= d;
			e[i].f -= d, e[i ^ 1].f += d;
			if(v != s and v != t and !inq[v]) pq.push(v), inq[v] = 1;
		}
	}
	rep(i, 1, n) if(h[i] != inf) gap[h[i]] += 1;
	while(!pq.empty())
	{
		int u = pq.top();
		pq.pop(), inq[u] = 0, push(u);
		if(!p[u]) continue;
		if(!--gap[h[u]])
		{
			rep(i, 1, n) 
				if(i != s and i != t and h[i] > h[u] and h[i] < n + 1) 
					h[i] = n + 1;
		}
		updt(u), gap[h[u]] += 1, pq.push(u), inq[u] = 1;
	}
	return p[t];
}

注: push \text{push} push 函数的作用是将当前点存储的流尽可能地推到下一层的节点。

  1. if(!bfs()) return 0; 原因?

    回看 bfs \text{bfs} bfs 函数代码,若最后 s s s 没有高度,就意味着无法从 t t t 走到 s s s,那么自然也就不存在最大流一说了。也因此要把 h[s] = n; 放在判断的后面了。

  2. h[s] = n;

    因为 s s s 是源点,对于它来说有无尽的流,目前暂时不存在有流要回到 s s s,只有自 s s s 将流往下推到其他点,所以目前它的高度要是最高的。

  3. 为什么在 HLPP \text{HLPP} HLPP 中还需要 g a p [ ] gap[] gap[] 数组?

    这个数组用来统计每一层当前节点数量。此时它的作用是判断当此层为空时的情况。若此层为空,意味着那些在这层以上的节点无法将流送到终点,所以要将这些流送回源点。

    因为流是自高往低才能流,所以要将这些点的高度设在源点高度之上。

    相对应的就是后面的这段代码了:

    	if(!--gap[h[u]])
    	{
    		rep(i, 1, n) 
    			if(i != s and i != t and h[i] > h[u] and h[i] < n + 1) 
    				h[i] = n + 1;
    	}
    
  4. updt(u), gap[h[u]] += 1, pq.push(u), inq[u] = 1;

    当节点 u u u 存储的流在尽可能推给下一层节点之后还有流剩时,要向将流送完,只有更改节点高度,才可能将所有流推出去。所以就有了以上的操作。

push \text{push} push 函数

inline void push(int nw)
{
	for(int i = hd[nw], v; i; i = e[i].nxt)
	{
		if(!p[nw]) return;
		if(h[v = e[i].to] + 1 != h[nw] or !e[i].f) continue;
		int d = min(p[nw], e[i].f);
		p[nw] -= d, p[v] += d, e[i].f -= d, e[i ^ 1].f += d;
		if(v != s and v != t and !inq[v]) 
			pq.push(v), inq[v] = 1;
	}
}

它的作用就是将当前节点的流尽可能地推向下一层的节点。

  1. if(v != s and v != t and !inq[v]) pq.push(v), inq[v] = 1;

    注意优先队列里,存放的是存储了若干流的中转点,所以每次推流之后都要让推向的那个节点在有限队列里。

updt \text{updt} updt 函数

inline void updt(int u)
{
	h[u] = inf;
	for(int i = hd[u], v; i; i = e[i].nxt)
	{
		if(e[i].f and h[v = e[i].to] + 1 < h[u])
			h[u] = h[v] + 1;
	}
}

它的作用是更改当前节点的高度。

  1. 为什么要取最小的高度?

    我们之所以会更改它的高度,是因为它有剩余的一些流无法推出去了。所以将它的高度设为最低可能的高度,以保证它还能将剩余流推出去。

AC Code

整体代码:

#include<bits/stdc++.h>
using std::min;
using std::vector;
using std::queue;
using std::priority_queue;

#define maxn 2005
#define maxm 200005
#define inf 2147483647
#define rep(i, a, b) for(register int i = a; i <= b; ++i)
int n, m, s, t;
int cnt = 1, hd[maxn];
struct node{
	int to, nxt, f;
}e[maxm << 1];
int p[maxn], h[maxn], gap[maxm];
bool inq[maxn];
queue <int> q;
struct cmp{
	inline bool operator () (int a, int b) const//重写仿函数 
	{
		return h[a] < h[b];
	}
};
priority_queue <int, vector<int>, cmp> pq;
int u, v, w;

inline int read()
{
	int x = 1, s = 0;
	char ch = getchar();
	while(ch < '0' or ch > '9') {if(ch == '-') x = -1; ch = getchar();}
	while(ch >= '0' and ch <= '9') s = s * 10 + ch - '0', ch = getchar();
	return x * s;
}

inline void add(int u, int v, int w)
{
	e[++cnt] = (node){v, hd[u], w};
	hd[u] = cnt;
	e[++cnt] = (node){u, hd[v], 0};
	hd[v] = cnt;
}

inline bool bfs()
{
	rep(i, 1, n) h[i] = inf;
	h[t] = 0, q.push(t);
	while(!q.empty())
	{
		int u = q.front();
		q.pop();
		for(int i = hd[u], v; i; i = e[i].nxt)
			if(e[i ^ 1].f and h[v = e[i].to] > h[u] + 1)
				h[v] = h[u] + 1, q.push(v);
	}
	return h[s] == inf ? 0 : 1;
}

inline void push(int nw)
{
	for(int i = hd[nw], v; i; i = e[i].nxt)
	{
		if(!p[nw]) return;
		if(h[v = e[i].to] + 1 != h[nw] or !e[i].f) continue;
		int d = min(p[nw], e[i].f);
		p[nw] -= d, p[v] += d, e[i].f -= d, e[i ^ 1].f += d;
		if(v != s and v != t and !inq[v]) 
			pq.push(v), inq[v] = 1;
	}
}

inline void updt(int u)
{
	h[u] = inf;
	for(int i = hd[u], v; i; i = e[i].nxt)
	{
		if(e[i].f and h[v = e[i].to] + 1 < h[u])
			h[u] = h[v] + 1;
	}
}

inline int hlpp()
{
	if(!bfs()) return 0;
	h[s] = n;
	for(int i = hd[s], v, d; i; i = e[i].nxt)
	{
		if(d = e[i].f)
		{
			p[v = e[i].to] += d, p[s] -= d;
			e[i].f -= d, e[i ^ 1].f += d;
			if(v != s and v != t and !inq[v]) pq.push(v), inq[v] = 1;
		}
	}
	rep(i, 1, n) if(h[i] != inf) gap[h[i]] += 1;
	while(!pq.empty())
	{
		int u = pq.top();
		pq.pop(), inq[u] = 0, push(u);
		if(!p[u]) continue;
		if(!--gap[h[u]])
		{
			rep(i, 1, n) 
				if(i != s and i != t and h[i] > h[u] and h[i] < n + 1) 
					h[i] = n + 1;
		}
		updt(u), gap[h[u]] += 1, pq.push(u), inq[u] = 1;
	}
	return p[t];
}

int main()
{
	n = read(), m = read(), s = read(), t = read();
	rep(i, 1, m)
		u = read(), v = read(), w = read(), add(u, v, w);
	printf("%d\n", hlpp());
	return 0;
}

感谢阅读。

辛苦管理员审核,若有问题烦请指出。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值