见证者为见证而来,铭记者因铭记而生|

园龄:粉丝:关注:

网络流学习记录

跟开了森林书一样。

最大流

概念

有一个有源点 s 和有汇点 t 的有向图(网络),边上有【容量】。想象一下,我们从 s 灌进无限的水,水能从有向路径到达 t,而每条路径最多允许通过【容量】的水。最终从 t 流出的水即是最大流。

有点抽象,举个例子:


这里有一个源点为 1,汇点为 6 的网络。我们从 163 条路径可以走:1246,1356,1346

首先,我们走 1246 这条路径,可以通过 1 的水(即路径最小值)。
其次,我们走 1346 这条路径,可以通过 1 的水。(46 这条边已经有 1 的水流过了,还剩 1 的容量)。
最后,我们走 1356 这条路径,可以通过 2 的水。

这种方案是最优的(之一)。故该网络的最大流为 1+1+2=4

解法

Ford–Fulkerson 方法

这是【贪心】算法在网络流上的总称。
我们首先考虑以下贪心:有路径可流,就往下流。
但是这显然不正确。有图如下:

有源点为 1,汇点为 4 的网络。如果我们一开始就沿着路径 1324 流的话,边 2413 的容量被消耗完,不能再流,求得答案为 1
但是我们可以沿着路径 124134 流,答案为 2
这说明直接贪心是错误的。

我们考虑反悔贪心。
我们引入【退流】操作。对于每条边,建立反向边,初始容量为 0
当一条边(也可以是某条正向边的反向边)被流经时,若流了 f,那么该边剩余容量减 f,该边的反向边剩余容量加 f。剩下的交给正常贪心。
这样,我们就可以以【错误】的贪心顺序求得正确的答案。

我们考虑这样的图(来自 OI Wiki):

专家发现【退流】操作相当于反悔。这样做是对的。

如果直接这样做,时间复杂度如何?
我们先给出几个定义:

  • 剩余容量:一条边的容量减去实际流量。
  • 增广路:一条从 st 的路径,路径上边的剩余容量最小值大于 0,即对答案有贡献。
  • 残量网络:当前的图中,所有剩余容量大于 0 的边(包括正向边和反向边)和所有点的集合。

|V|=n,|E|=m
每轮增广的时间复杂度显然是 O(m) 的。计算时间复杂度只需要算出增广轮数即可。
极端情况下,有图:

可能以 13241234 两条路径反复增广约 2×109 次。
总复杂度是和值域 F 有关的,可能还要挂上一个 nm。总时间复杂度不敢想象,可能是 O(nmF) 的。

于是要优化。

Edmonds-Karp 算法

我们考虑用 BFS 实现 FF 方法。具体地,每轮用 BFS 搜出一条增广路,并将其加入答案。每轮 BFS 的时间复杂度是 O(m) 的,专家可以证明总的增广轮数是不超过 O(nm) 的,于是总复杂度 O(nm2)

Dinic 算法

我们专家思考 EK 算法的瓶颈。增广时,BFS 是乱搜的,如果我们以一定的顺序增广,可能获得更优的时间复杂度。
在增广前用一遍 BFS 建出(无需显式)最短路径图(即以 s 至每个结点的 dis 为键值分层,只保留是最短路径边的图)。我们在最短路径图上增广,每次(一般)用 DFS 搜出一个增广路,更新即可。

有两个优化需要注意:

  • 当前弧优化:每次我们维护搜到结点 u 的出边表中第一条【值得尝试】的边 curu。【值得尝试】指该边的剩余容量大于 0,且它没有被增广路走过(如果走过,那么它的下游仍没有机会增广)。这是保证 Dinic 复杂度的优化,让其不退化至 O(nm2)(?)。
    值得一提的是,我们的 cur 指针必须要指向第一个值得尝试的边,不能指向第二条或更后,否则复杂度会假。常见的错误是判 sum 写错位置,一定要在 cur 更新前判断。
  • 多路增广:我们在一轮增广时,可以不仅搜出一条增广路,可以在某处寻找一个岔路进行继续增广,不立即从头再来。这是 Dinic 的第一个常数优化。

总时间复杂度 O(n2m)

luogu P3376【模板】网络最大流

#include <bits/stdc++.h>
#define inf 0x3f3f3f3f
#define Linf 0x3f3f3f3f3f3f3f3f
#define pii pair<int, int> 
#define all(v) v.begin(), v.end()
#define int long long
using namespace std;

//#define filename "xxx" 
#define FileOperations() freopen(filename".in", "r", stdin), freopen(filename".out", "w", stdout)
//#define multi_cases 1

namespace Traveller {
	const int N = 202, M = 5002;
	
	struct Graph {
		int n, m, s, t, tot;
		int head[N], cur[N];
		struct edge {
			int v, w, next;
			edge() { }
			edge(int a, int b, int c) : v(a), w(b), next(c) { }
		} e[M << 1];
		void add_edge(int u, int v, int w) {
			e[tot] = edge(v, w, head[u]), head[u] = tot++;
			e[tot] = edge(u, 0, head[v]), head[v] = tot++;
		}
		
		void init() {
			cin >> n >> m >> s >> t;
			tot = 0;
			memset(head, -1, sizeof(head));	//如果tot=0
			for(int i = 1, u, v, w; i <= m; ++i) {
				cin >> u >> v >> w;
				add_edge(u, v, w);
			}
		}
		
		queue<int> q;
		int dis[N];
		int BFS() {	//在残量网络中打出层次图
			for(int i = 1; i <= n; ++i) dis[i] = Linf, cur[i] = head[i];
			queue<int>().swap(q);
			q.push(s);
			dis[s] = 0;
			while(!q.empty()) {
				int u = q.front();
				q.pop();
				for(int i = head[u]; ~i; i = e[i].next) {
					int v = e[i].v;
					if(e[i].w > 0 && dis[v] > 1e18) {
						q.push(v);
						dis[v] = dis[u] + 1;
						if(v == t) return 1;	//常数优化
					}
				}
			}
			return 0;
		}
		
		int DFS(int u, int sum = Linf) {	//sum:当前流量
			if(u == t) return sum;
			int res = 0;
			for(int i = cur[u]; ~i && sum > 0; i = e[i].next) {	//多路增广
				cur[u] = i;
				int v = e[i].v;
				if(e[i].w > 0 && dis[v] == dis[u] + 1) {
					int k = DFS(v, min(sum, e[i].w));
					if(k == 0) dis[v] = Linf;
					e[i].w -= k, e[i ^ 1].w += k;
					res += k, sum -= k;
				}
			}
			return res;
		}
		
		int Dinic() {
			int ans = 0;
			while(BFS()) ans += DFS(s);
			return ans;
		}
	} G;
	
	void main() {
		G.init();
		cout << G.Dinic();
	}
}

signed main() {
#ifdef filename
	FileOperations();
#endif
	
	signed _ = 1;
#ifdef multi_cases
	scanf("%d", &_);
#endif

	while(_--) Traveller::main();
	return 0;
}

例题

二分图最大匹配

我们新建立一个超级源点 s 和一个超级汇点 t。对于每个左边点 u,建容量为 1 的有向边 (s,u)。对于每个右边点 v,建容量为 1 的有向边 (v,t)。对于左边点 u 和右边点 v 之间的(无向)边,建成容量为 1 的有向边 (u,v)
这时,跑从 st 的最大流就是二分图的最大匹配。
luogu P3386 【模板】二分图最大匹配

#include <bits/stdc++.h>
#define inf 0x3f3f3f3f
#define Linf 0x3f3f3f3f3f3f3f3f
#define pii pair<int, int> 
#define all(v) v.begin(), v.end()
#define int long long
using namespace std;

//#define filename "xxx" 
#define FileOperations() freopen(filename".in", "r", stdin), freopen(filename".out", "w", stdout)
//#define multi_cases 1

namespace Traveller {
	const int N = 1005, M = 5e4+5;
	
	struct Graph {
		int n, n1, n2, m, s, t, tot;
		set<pii> mark;
		int head[N], cur[N];
		struct edge {
			int v, w, next;
			edge() { }
			edge(int a, int b, int c) : v(a), w(b), next(c) { }
		} e[M << 1];
		void add_edge(int u, int v, int w) {
			e[tot] = edge(v, w, head[u]), head[u] = tot++;
			e[tot] = edge(u, 0, head[v]), head[v] = tot++;
		}
		
		void init() {
			cin >> n1 >> n2 >> m;
			tot = 0;
			memset(head, -1, sizeof(head));
			for(int i = 1, u, v; i <= m; ++i) {
				cin >> u >> v;
				if(mark.count(pii(u, v))) continue;
				mark.insert(pii(u, v));
				add_edge(u, v+n1, 1);
			}
			s = n1+n2+1, t = n1+n2+2;
			for(int i = 1; i <= n1; ++i) add_edge(s, i, 1);
			for(int i = n1+1; i <= n1+n2; ++i) add_edge(i, t, 1);
			n = n1+n2+2;
		}
		
		queue<int> q;
		int dis[N];
		int BFS() {
			for(int i = 1; i <= n; ++i) dis[i] = Linf, cur[i] = head[i];
			queue<int>().swap(q);
			q.push(s);
			dis[s] = 0;
			while(!q.empty()) {
				int u = q.front();
				q.pop();
				for(int i = head[u]; ~i; i = e[i].next) {
					int v = e[i].v;
					if(e[i].w > 0 && dis[v] > 1e18) {
						q.push(v);
						dis[v] = dis[u] + 1;
						if(v == t) return 1;
					}
				}
			}
			return 0;
		}
		
		int DFS(int u, int sum = Linf) {
			if(u == t) return sum;
			int res = 0;
			for(int i = cur[u]; ~i && sum > 0; i = e[i].next) {
				cur[u] = i;
				int v = e[i].v;
				if(e[i].w > 0 && dis[v] == dis[u] + 1) {
					int k = DFS(v, min(sum, e[i].w));
					if(k == 0) dis[v] = Linf;
					e[i].w -= k, e[i ^ 1].w += k;
					res += k, sum -= k;
				}
			}
			return res;
		}
		
		int Dinic() {
			int ans = 0;
			while(BFS()) ans += DFS(s);
			return ans;
		}
	} G;
	
	void main() {
		G.init();
		cout << G.Dinic();
	}
}

signed main() {
#ifdef filename
	FileOperations();
#endif
	
	signed _ = 1;
#ifdef multi_cases
	scanf("%d", &_);
#endif

	while(_--) Traveller::main();
	return 0;
}

如果要输出匹配边,看看哪些左右点之间的边 (u,v) 满流了即可。

luogu P2763 试题库问题

若每个类型要出 x 道题,那么我们可以将这种类型拆成 x 个点,对于每个包含这种类型的试题都建点并向这 x 个点分别连容量为 1 的有向边。
超级源点连所有试题,所有拆点后的类型连超级汇点,容量均为 1
我们发现它实质上就是二分图最大匹配。
输出方案稍微处理一下即可。

#include <bits/stdc++.h>
#define inf 0x3f3f3f3f
#define Linf 0x3f3f3f3f3f3f3f3f
#define pii pair<int, int> 
#define all(v) v.begin(), v.end()
using namespace std;

//#define filename "xxx" 
#define FileOperations() freopen(filename".in", "r", stdin), freopen(filename".out", "w", stdout)
//#define multi_cases 1

namespace Traveller {
	const int N = 1e6+2, M = 1e6+5;
	
	struct Graph {
		int k, n, n1, m, s, t, tot;
		vector<int> vec[N];
		int head[N], cur[N];
		struct edge {
			int v, w, next;
			edge() { }
			edge(int a, int b, int c) : v(a), w(b), next(c) { }
		} e[M << 1];
		void add_edge(int u, int v, int w) {
			e[tot] = edge(v, w, head[u]), head[u] = tot++;
			e[tot] = edge(u, 0, head[v]), head[v] = tot++;
		}
		
		void init() {
			cin >> k >> n;
			n1 = n;
			tot = 0, memset(head, -1, sizeof(head));
			for(int i = 1, x; i <= k; ++i) {
				cin >> x;
				for(int j = m+1; j <= m+x; ++j) vec[i].push_back(j);
				m += x;
			}
			for(int i = 1, p; i <= n; ++i) {
				cin >> p;
				for(int j = 1, x; j <= p; ++j) {
					cin >> x;
					for(auto ele : vec[x]) add_edge(i, ele + n, 1);
				}
			}
			s = n+m+1, t = n+m+2;
			for(int i = 1; i <= n; ++i) add_edge(s, i, 1);
			for(int i = n+1; i <= n+m; ++i) add_edge(i, t, 1);
			n += m+2;
		}
		
		queue<int> q;
		int dis[N];
		int BFS() {
			for(int i = 1; i <= n; ++i) dis[i] = inf, cur[i] = head[i];
			queue<int>().swap(q);
			q.push(s);
			dis[s] = 0;
			while(!q.empty()) {
				int u = q.front();
				q.pop();
				for(int i = head[u]; ~i; i = e[i].next) {
					int v = e[i].v;
					if(e[i].w > 0 && dis[v] > 1e9) {
						q.push(v);
						dis[v] = dis[u] + 1;
						if(v == t) return 1;
					}
				}
			}
			return 0;
		}
		
		int DFS(int u, int sum = inf) {
			if(u == t) return sum;
			int res = 0;
			for(int i = cur[u]; ~i && sum > 0; i = e[i].next) {
				cur[u] = i;
				int v = e[i].v;
				if(e[i].w > 0 && dis[v] == dis[u] + 1) {
					int k = DFS(v, min(sum, e[i].w));
					if(k == 0) dis[v] = inf;
					e[i].w -= k, e[i ^ 1].w += k;
					res += k, sum -= k;
				}
			}
			return res;
		}
		
		int Dinic() {
			int ans = 0;
			while(BFS()) ans += DFS(s);
			return ans;
		}
		
		vector<int> ans[N];
		void solve() {
			int x = Dinic();
			if(x < m) return puts("No Solution!"), void();
			for(int i = 1; i <= n1; ++i)
				for(int j = head[i]; ~j; j = e[j].next)
					if(e[j].v != s && e[j].w == 0) ans[e[j].v].push_back(i);
			for(int i = 1; i <= k; ++i) {
				cout << i << ": ";
				for(auto j : vec[i])
					for(auto ele : ans[j + n1]) cout << ele << ' ';
				puts("");
			}
		}
	} G;
	
	void main() {
		G.init();
		G.solve();
	}
}

signed main() {
#ifdef filename
	FileOperations();
#endif
	
	signed _ = 1;
#ifdef multi_cases
	scanf("%d", &_);
#endif

	while(_--) Traveller::main();
	return 0;
}

luogu P3425 [POI 2005] KOS-Dicing

我们将源点 s 连向每个游戏,容量为 1,每次游戏连向参加的两个人,容量为 1,表示每个游戏只有一个胜者。每个人连向汇点 t,但是容量是多少呢?
我们思考这个容量的实际意义。该边的实际意义就是每个人最多有几个胜场。又注意到题目要求最大值最小,故考虑二分这个容量 mid
至于 check 怎么写,只需要看看最大流是不是(大于等于)等于总游戏次数 m。(最大流的上限就是 m,因为 s 总共才向外连了 m 条容量为 1 的边)
输出方案也很简单,看看每个比赛连哪个人的边满流即可。

#include <bits/stdc++.h>
#define inf 0x3f3f3f3f
#define Linf 0x3f3f3f3f3f3f3f3f
#define pii pair<int, int> 
#define all(v) v.begin(), v.end()
using namespace std;

//#define filename "xxx" 
#define FileOperations() freopen(filename".in", "r", stdin), freopen(filename".out", "w", stdout)
//#define multi_cases 1

namespace Traveller {
	const int N = 2e4+5, M = 4e4+5;
	
	struct game {
		int u, v, w;
		game() { }
		game(int u, int v) : u(u), v(v) { }
	} a[M];

	int n, m;
	
	struct Graph {
		int n, m, s, t, tot;
		int head[N], cur[N];
		struct edge {
			int v, w, next;
			edge() { }
			edge(int a, int b, int c) : v(a), w(b), next(c) { }
		} e[M << 1];
		void add_edge(int u, int v, int w) {
			e[tot] = edge(v, w, head[u]), head[u] = tot++;
			e[tot] = edge(u, 0, head[v]), head[v] = tot++;
		}
		
		void init(int n, int m, int l) {
			this->m = m;
			s = n+m+1, t = n+m+2;
			tot = 0, memset(head, -1, sizeof(head));
			for(int i = 1; i <= m; ++i) {
				add_edge(s, i, 1);
				add_edge(i, a[i].u + m, 1), add_edge(i, a[i].v + m, 1);
			}
			for(int i = 1; i <= n; ++i) add_edge(i + m, t, l);
			this->n = n+m+2;
		}
		
		queue<int> q;
		int dis[N];
		int BFS() {	//在残量网络中打出层次图
			for(int i = 1; i <= n; ++i) dis[i] = inf, cur[i] = head[i];
			queue<int>().swap(q);
			q.push(s);
			dis[s] = 0;
			while(!q.empty()) {
				int u = q.front();
				q.pop();
				for(int i = head[u]; ~i; i = e[i].next) {
					int v = e[i].v;
					if(e[i].w > 0 && dis[v] > 1e9) {
						q.push(v);
						dis[v] = dis[u] + 1;
						if(v == t) return 1;
					}
				}
			}
			return 0;
		}
		
		int DFS(int u, int sum = inf) {
			if(u == t) return sum;
			int res = 0;
			for(int i = cur[u]; ~i && sum > 0; i = e[i].next) {
				cur[u] = i;
				int v = e[i].v;
				if(e[i].w > 0 && dis[v] == dis[u] + 1) {
					int k = DFS(v, min(sum, e[i].w));
					if(k == 0) dis[v] = inf;
					e[i].w -= k, e[i ^ 1].w += k;
					res += k, sum -= k;
				}
			}
			return res;
		}
		
		int Dinic(int op = 0) {
			int ans = 0;
			while(BFS()) ans += DFS(s);
			if(op) {
				for(int i = 1; i <= m; ++i)
					cout << (e[head[i]].v == a[i].u ^ e[head[i]].w == 1) << '\n';
			}
			return ans;
		}
	} G;
	
	bool check(int l) {
		G.init(n, m, l);
		return G.Dinic() == m;
	}
	void print(int l) { G.init(n, m, l), G.Dinic(1); }
	
	void main() {
		cin >> n >> m;
		for(int i = 1; i <= m; ++i) cin >> a[i].u >> a[i].v;
		
		int L = 1, R = n;
		while(L < R) {
			int mid = L + R >> 1;
			if(check(mid)) R = mid;
			else L = mid+1;
		}
		cout << L << '\n';
		print(L);
	}
}

signed main() {
#ifdef filename
	FileOperations();
#endif
	
	signed _ = 1;
#ifdef multi_cases
	scanf("%d", &_);
#endif

	while(_--) Traveller::main();
	return 0;
}

luogu P2766 最长不下降子序列问题

第一问是 naive 的 DP,设 fiai 结尾的 LIS 即可。

第二问我们考虑怎么建图。所谓【取出】即【不相交】,有点类似匹配。
记 LIS 的长度为 mx
我们将源点 s 向每个 f 值为 1 的点连边,将每个 f 值为 mx 的点向汇点 t 连边,每个点向 f 值比其大 1 、编号在其后、a 值不比其小的点连边,容量均为 1,描述 DP 的状态转移过程。
输出最大流即可。
这么连边显然是正确的,每一条 st 的路径就表示一个【取出】的 LIS。由于容量的限制,每个点只会取一次。

第三问只需要在第二问的基础上把 s1 号点的边容量改为 n 号点至 t 的边容量改为 即可(如果有边)。
注意这一问要特判 n=1 的情况,输出 1但是输出无穷大也有道理?

套路

  • 转化成二分图匹配,或类似问题。
  • 保证一个点只经过一次,可以拆点。

最小割

概念

  • 割:一个源点为 s,汇点为 t 的网络 G=(V,E),将 V 划分成 ST 两部分,其中 sS,tT。那么 {S,T} 称为 G 的一个 st 割。
  • 割边:一条边 (u,v),满足 uS,vT ,则该边称为割边。
  • 割的容量(大小):即所有割边的容量之和。

解法

对于任意图,有最小割等于最大流。

  • 输出最小割的割边:我们从 s 开始 DFS,只走残量网络中的边,能到达的所有结点的集合就是 S。接下来在原图中枚举每个 S 中点的出边,到达的点不属于 S 的就是割边。
    • 一定不能直接输出所有满流边,因为有图:

      12,24 都是满流边,显然不对。

例题

luogu P5934 [清华集训 2012] 最小生成树

我们考虑最小生成树的过程,即 Kruskal。如果从小到大枚举边,当前加过的边构成的图中,u,v 不连通,那么当前边 (u,v) 才能加进来。
故权为 L 的边 (u,v) 在最小生成树中充要条件为所有小于 L 的边构成的“子图”中,(u,v) 不连通。于是割掉最少的边,使该图的 (u,v) 不连通,就是答案。这个明显等价于最小割。
于是我们在【小于 L】和【大于 L】的子图中分别跑最小割,相加即可。

Q:相加即是对的呢?
A:因为两个子图的边集没有交集,割掉的边不会有重复。

#include <bits/stdc++.h>
#define inf 0x3f3f3f3f
#define Linf 0x3f3f3f3f3f3f3f3f
#define pii pair<int, int> 
#define all(v) v.begin(), v.end()
using namespace std;

//#define filename "xxx" 
#define FileOperations() freopen(filename".in", "r", stdin), freopen(filename".out", "w", stdout)
//#define multi_cases 1

namespace Traveller {
	const int N = 3.2e5+2, M = 6.4e6+2;
	
	struct Graph {
		int n, s, t, tot;
		int head[N], cur[N];
		struct edge {
			int v, w, next;
			edge() { }
			edge(int a, int b, int c) : v(a), w(b), next(c) { }
		} e[M << 1];
		void add_edge(int u, int v, int w) {
			e[tot] = edge(v, w, head[u]), head[u] = tot++;
			e[tot] = edge(u, w, head[v]), head[v] = tot++;
		}
		
		void init(int n, int s, int t) {
			this->n = n, this->s = s, this->t = t;
			tot = 0, memset(head, -1, sizeof(head));
		}
		
		queue<int> q;
		int dis[N];
		int BFS() {
			for(int i = 1; i <= n; ++i) dis[i] = inf, cur[i] = head[i];
			queue<int>().swap(q);
			q.push(s);
			dis[s] = 0;
			while(!q.empty()) {
				int u = q.front();
				q.pop();
				for(int i = head[u]; ~i; i = e[i].next) {
					int v = e[i].v;
					if(e[i].w > 0 && dis[v] > 1e9) {
						q.push(v);
						dis[v] = dis[u] + 1;
						if(v == t) return 1;
					}
				}
			}
			return 0;
		}
		
		int DFS(int u, int sum = inf) {
			if(u == t) return sum;
			int res = 0;
			for(int i = cur[u]; ~i && sum > 0; i = e[i].next) {
				cur[u] = i;
				int v = e[i].v;
				if(e[i].w > 0 && dis[v] == dis[u] + 1) {
					int k = DFS(v, min(sum, e[i].w));
					if(k == 0) dis[v] = inf;
					e[i].w -= k, e[i ^ 1].w += k;
					res += k, sum -= k;
				}
			}
			return res;
		}
		
		int Dinic() {
			int ans = 0;
			while(BFS()) ans += DFS(s);
			return ans;
		}
	} G1, G2;
	
	int n, m;
	int u[N], v[N], w[N];
	int s, t, L;
	
	void main() {
		cin >> n >> m;
		for(int i = 1; i <= m; ++i) cin >> u[i] >> v[i] >> w[i];
		cin >> s >> t >> L;
		
		G1.init(n, s, t), G2.init(n, s, t);
		for(int i = 1; i <= m; ++i) {
			if(w[i] < L) G1.add_edge(u[i], v[i], 1);
			else if(w[i] > L) G2.add_edge(u[i], v[i], 1);
		}
		cout << G1.Dinic() + G2.Dinic();
	}
}

signed main() {
#ifdef filename
	FileOperations();
#endif
	
	signed _ = 1;
#ifdef multi_cases
	scanf("%d", &_);
#endif

	while(_--) Traveller::main();
	return 0;
}

Atcoder ABC239G Builder Takahashi

这题的最小割再显然不过了。但是这道题是点权。

有一个拆点的技巧:将每个点分为入点和出点。原图中的边正常连,权值设为 ,点权体现在入点至出点的边的边权。

#include <bits/stdc++.h>
#define inf 0x3f3f3f3f
#define Linf 0x3f3f3f3f3f3f3f3f
#define pii pair<int, int> 
#define int long long
#define all(v) v.begin(), v.end()
using namespace std;

//#define filename "xxx" 
#define FileOperations() freopen(filename".in", "r", stdin), freopen(filename".out", "w", stdout)
//#define multi_cases 1

namespace Traveller {
	const int N = 1002, M = 1e6+2;
	
	struct Graph {
		int n, m, s, t, tot;
		int head[N], cur[N];
		struct edge {
			int v, w, next;
			edge() { }
			edge(int a, int b, int c) : v(a), w(b), next(c) { }
		} e[M << 1];
		void add_edge(int u, int v, int w) {
			e[tot] = edge(v, w, head[u]), head[u] = tot++;
			e[tot] = edge(u, 0, head[v]), head[v] = tot++;
		}
		
		void init() {
			cin >> n >> m;
			s = n+1, t = n;
			tot = 0;
			memset(head, -1, sizeof(head));
			for(int i = 1, u, v; i <= m; ++i) {
				cin >> u >> v;
				add_edge(u+n, v, Linf), add_edge(v+n, u, Linf);
			}
			for(int i = 1, c; i <= n; ++i) {
				cin >> c;
				add_edge(i, i+n, c);
			}
			n *= 2;
		}
		
		queue<int> q;
		int dis[N];
		int BFS() {
			for(int i = 1; i <= n; ++i) dis[i] = Linf, cur[i] = head[i];
			queue<int>().swap(q);
			q.push(s);
			dis[s] = 0;
			while(!q.empty()) {
				int u = q.front();
				q.pop();
				for(int i = head[u]; ~i; i = e[i].next) {
					int v = e[i].v;
					if(e[i].w > 0 && dis[v] > 1e18) {
						q.push(v);
						dis[v] = dis[u] + 1;
						if(v == t) return 1;
					}
				}
			}
			return 0;
		}
		
		int DFS(int u, int sum = Linf) {
			if(u == t) return sum;
			int res = 0;
			for(int i = cur[u]; ~i && sum > 0; i = e[i].next) {
				cur[u] = i;
				int v = e[i].v;
				if(e[i].w > 0 && dis[v] == dis[u] + 1) {
					int k = DFS(v, min(sum, e[i].w));
					if(k == 0) dis[v] = Linf;
					e[i].w -= k, e[i ^ 1].w += k;
					res += k, sum -= k;
				}
			}
			return res;
		}
		
		int Dinic() {
			int ans = 0;
			while(BFS()) ans += DFS(s);
			return ans;
		}
		
		vector<int> ans;
		int vis[N];
		void mark(int u) {
			vis[u] = 1;
			for(int i = head[u]; ~i; i = e[i].next) {
				int v = e[i].v;
				if(!vis[v] && e[i].w) mark(v);
			}
		}
		void print() {
			mark(s);
			for(int i = 1; i <= n; ++i)
				if(i <= n/2 && vis[i] && !vis[i + n/2]) ans.push_back(i);
			cout << ans.size() << '\n';
			for(auto i : ans) cout << i << ' ';
		}
	} G;
	
	void main() {
		G.init();
		cout << G.Dinic() << '\n';
		G.print();
	}
}

signed main() {
#ifdef filename
	FileOperations();
#endif
	
	signed _ = 1;
#ifdef multi_cases
	scanf("%d", &_);
#endif

	while(_--) Traveller::main();
	return 0;
}

luogu P2774 方格取数问题

一个常用的策略是,最大收益等于总收益减去最小损失。
如果直接建无向网格图的话,既不能体现点权,也不能体现相邻不能同时选。

我们考虑如下建图:
记点 (i,j),若 2|(i+j),则其为白点。否则为黑点。
我们建出源点 s,连向所有白点,权值为白点的点权;白点连向各自相邻的黑点,权值为 ;所有黑点连向汇点 t,权值为黑点的点权。
最小割就是最小损失。

考虑这么做为什么是对的。
显然,因为是最小割,故不可能割掉白点与黑点之间的边。
割掉一条源点与白点之间的边就代表白点不选,黑点同理。
那么得到的最小割,就不存在 st 的路径,即满足了相邻黑白点不能同时选(画图易知)。

#include <bits/stdc++.h>
#define inf 0x3f3f3f3f
#define Linf 0x3f3f3f3f3f3f3f3f
#define pii pair<int, int> 
#define all(v) v.begin(), v.end()
#define int long long
using namespace std;

//#define filename "xxx" 
#define FileOperations() freopen(filename".in", "r", stdin), freopen(filename".out", "w", stdout)
//#define multi_cases 1

namespace Traveller {
	const int N = 1e4+5, M = 1e6+2;
	
	struct Graph {
		int n, m, s, t, tot, sum;
		const int dir[4][2] = {{0, 1}, {1, 0}, {0, -1}, {-1, 0}};
		
		int head[N], cur[N];
		struct edge {
			int v, w, next;
			edge() { }
			edge(int a, int b, int c) : v(a), w(b), next(c) { }
		} e[M << 1];
		void add_edge(int u, int v, int w) {
			e[tot] = edge(v, w, head[u]), head[u] = tot++;
			e[tot] = edge(u, 0, head[v]), head[v] = tot++;
		}
		
		void init() {
			cin >> n >> m;
			s = n*m+1, t = n*m+2;
			tot = 0;
			memset(head, -1, sizeof(head));
			for(int i = 1; i <= n; ++i) 
				for(int j = 1, a; j <= m; ++j) {
					scanf("%lld", &a), sum += a;
					if(i + j & 1) add_edge((i-1) * m + j, t, a);
					else {
						for(int o = 0; o < 4; ++o) {
							int nx = i + dir[o][0], ny = j + dir[o][1];
							if(nx < 1 || nx > n || ny < 1 || ny > m) continue;
							add_edge((i-1) * m + j, (nx-1) * m + ny, Linf);
						}
						add_edge(s, (i-1) * m + j, a);
					}
				}
			n = n * m + 2;
		}
		
		queue<int> q;
		int dis[N];
		int BFS() {
			for(int i = 1; i <= n; ++i) dis[i] = Linf, cur[i] = head[i];
			queue<int>().swap(q);
			q.push(s);
			dis[s] = 0;
			while(!q.empty()) {
				int u = q.front();
				q.pop();
				for(int i = head[u]; ~i; i = e[i].next) {
					int v = e[i].v;
					if(e[i].w > 0 && dis[v] > 1e18) {
						q.push(v);
						dis[v] = dis[u] + 1;
						if(v == t) return 1;
					}
				}
			}
			return 0;
		}
		
		int DFS(int u, int sum = Linf) {
			if(u == t) return sum;
			int res = 0;
			for(int i = cur[u]; ~i && sum > 0; i = e[i].next) {
				cur[u] = i;
				int v = e[i].v;
				if(e[i].w > 0 && dis[v] == dis[u] + 1) {
					int k = DFS(v, min(sum, e[i].w));
					if(k == 0) dis[v] = Linf;
					e[i].w -= k, e[i ^ 1].w += k;
					res += k, sum -= k;
				}
			}
			return res;
		}
		
		int Dinic() {
			int ans = 0;
			while(BFS()) ans += DFS(s);
			return ans;
		}
		int solve() { return sum - Dinic(); }
	} G;
	
	void main() {
		G.init();
		cout << G.solve();
	}
}

signed main() {
#ifdef filename
	FileOperations();
#endif
	
	signed _ = 1;
#ifdef multi_cases
	scanf("%d", &_);
#endif

	while(_--) Traveller::main();
	return 0;
}

luogu P1646 [国家集训队] happiness

仍然考虑最大收益为总收益减去最小损失。原因:我们直接做无法保证邻座同科的额外收益。

对于单人的文理选择,这是一个二者选其一,故我们直接考虑源点向当前点连容量为文科收益的边,当前点向汇点连容量为理科收益的边。
对于邻座同科的额外收益,我们发现:

  • 只要两个人有一个人选理科,那么邻座同文科的收益就要割掉。

记这两个人为 x,y。我们新建一个点 u,用 su 连容量为同是文科的收益,u 分别向 x,y 连容量是 的边。这样就可以保证上述条件。
同时理科同理,改成向汇点连边。

#include <bits/stdc++.h>
#define inf 0x3f3f3f3f
#define Linf 0x3f3f3f3f3f3f3f3f
#define pii pair<int, int> 
#define all(v) v.begin(), v.end()
#define int long long
using namespace std;

//#define filename "xxx" 
#define FileOperations() freopen(filename".in", "r", stdin), freopen(filename".out", "w", stdout)
//#define multi_cases 1

namespace Traveller {
	const int N = 1e6+2, M = 2e6+2;
	
	struct Graph {
		int n, m, s, t, tot, sum;
		int head[N], cur[N];
		struct edge {
			int v, w, next;
			edge() { }
			edge(int a, int b, int c) : v(a), w(b), next(c) { }
		} e[M << 1];
		void add_edge(int u, int v, int w) {
			e[tot] = edge(v, w, head[u]), head[u] = tot++;
			e[tot] = edge(u, 0, head[v]), head[v] = tot++;
		}
		
		void init() {
			cin >> n >> m;
			int vertices = n * m;
			s = ++vertices, t = ++vertices;
			tot = 0;
			memset(head, -1, sizeof(head));
			
			auto pos = [=] (int i, int j) { return (i-1) * n + j; };
			
			for(int i = 1; i <= n; ++i)
				for(int j = 1, x; j <= m; ++j) {
					cin >> x, sum += x;
					add_edge(s, pos(i, j), x);
				}
			for(int i = 1; i <= n; ++i)
				for(int j = 1, x; j <= m; ++j) {
					cin >> x, sum += x;
					add_edge(pos(i, j), t, x);
				}
			for(int i = 1; i < n; ++i)
				for(int j = 1, x; j <= m; ++j) {
					cin >> x, sum += x;
					add_edge(s, ++vertices, x);
					add_edge(vertices, pos(i, j), inf);
					add_edge(vertices, pos(i+1, j), inf);
				}
			for(int i = 1; i < n; ++i)
				for(int j = 1, x; j <= m; ++j) {
					cin >> x, sum += x;
					add_edge(++vertices, t, x);
					add_edge(pos(i, j), vertices, inf);
					add_edge(pos(i+1, j), vertices, inf);
				}
			for(int i = 1; i <= n; ++i)
				for(int j = 1, x; j < m; ++j) {
					cin >> x, sum += x;
					add_edge(s, ++vertices, x);
					add_edge(vertices, pos(i, j), inf);
					add_edge(vertices, pos(i, j+1), inf);
				}
			for(int i = 1; i <= n; ++i)
				for(int j = 1, x; j < m; ++j) {
					cin >> x, sum += x;
					add_edge(++vertices, t, x);
					add_edge(pos(i, j), vertices, inf);
					add_edge(pos(i, j+1), vertices, inf);
				}
			n = vertices;
		}
		
		queue<int> q;
		int dis[N];
		int BFS() {
			for(int i = 1; i <= n; ++i) dis[i] = Linf, cur[i] = head[i];
			queue<int>().swap(q);
			q.push(s);
			dis[s] = 0;
			while(!q.empty()) {
				int u = q.front();
				q.pop();
				for(int i = head[u]; ~i; i = e[i].next) {
					int v = e[i].v;
					if(e[i].w > 0 && dis[v] > 1e18) {
						q.push(v);
						dis[v] = dis[u] + 1;
						if(v == t) return 1;
					}
				}
			}
			return 0;
		}
		
		int DFS(int u, int sum = Linf) {
			if(u == t) return sum;
			int res = 0;
			for(int i = cur[u]; ~i && sum > 0; i = e[i].next) {
				cur[u] = i;
				int v = e[i].v;
				if(e[i].w > 0 && dis[v] == dis[u] + 1) {
					int k = DFS(v, min(sum, e[i].w));
					if(k == 0) dis[v] = Linf;
					e[i].w -= k, e[i ^ 1].w += k;
					res += k, sum -= k;
				}
			}
			return res;
		}
		
		int Dinic() {
			int ans = 0;
			while(BFS()) ans += DFS(s);
			return ans;
		}
		int solve() { return sum - Dinic(); }
	} G;
	
	void main() {
		G.init();
		cout << G.solve();
	}
}

signed main() {
#ifdef filename
	FileOperations();
#endif
	
	signed _ = 1;
#ifdef multi_cases
	scanf("%d", &_);
#endif

	while(_--) Traveller::main();
	return 0;
}

luogu P2762 太空飞行计划问题

本题仍然有【一个实验所需的仪器集合中有一个不用,那么实验的收益要割掉】的模型。
源点连向实验,代表实验的收益;实验连向仪器,表示依赖关系;仪器连向汇点,不割表示不选,割掉表示选。

总收益减最小割就是答案。
注意读入和方案构造。

#include <bits/stdc++.h>
#define inf 0x3f3f3f3f
#define Linf 0x3f3f3f3f3f3f3f3f
#define pii pair<int, int> 
#define all(v) v.begin(), v.end()
#define int long long
using namespace std;

//#define filename "xxx" 
#define FileOperations() freopen(filename".in", "r", stdin), freopen(filename".out", "w", stdout)
//#define multi_cases 1

namespace Traveller {
	const int N = 2002, M = 50002;
	
	struct Graph {
		int n, n0, m, s, t, tot, sum;
		int head[N], cur[N];
		struct edge {
			int v, w, next;
			edge() { }
			edge(int a, int b, int c) : v(a), w(b), next(c) { }
		} e[M << 1];
		void add_edge(int u, int v, int w) {
			e[tot] = edge(v, w, head[u]), head[u] = tot++;
			e[tot] = edge(u, 0, head[v]), head[v] = tot++;
		}
		
		void readtools(int i) {
			char tools[10000];
			memset(tools, 0, sizeof(tools));
			cin.getline(tools, 10000);
			int ulen = 0, tool;
			while(sscanf(tools + ulen, "%lld", &tool) == 1) {
				add_edge(i, tool + n, Linf);
				if(tool == 0) ++ulen;
				else while(tool) {
					tool /= 10;
					++ulen;
				}
				++ulen;
			}
		}
		
		void init() {
			cin >> n >> m;
			s = n + m + 1, t = n + m + 2;
			tot = 0, memset(head, -1, sizeof(head));
			for(int i = 1, v; i <= n; ++i) {
				scanf("%lld", &v), add_edge(s, i, v), sum += v;
				readtools(i);
			}
			for(int i = 1, price; i <= m; ++i) {
				cin >> price;
				add_edge(i + n, t, price);
			}
			n0 = n;
			n += m + 2;
		}
		
		queue<int> q;
		int dis[N];
		int BFS() {
			for(int i = 1; i <= n; ++i) dis[i] = Linf, cur[i] = head[i];
			queue<int>().swap(q);
			q.push(s);
			dis[s] = 0;
			while(!q.empty()) {
				int u = q.front();
				q.pop();
				for(int i = head[u]; ~i; i = e[i].next) {
					int v = e[i].v;
					if(e[i].w > 0 && dis[v] > 1e18) {
						q.push(v);
						dis[v] = dis[u] + 1;
						if(v == t) return 1;
					}
				}
			}
			return 0;
		}
		
		int DFS(int u, int sum = Linf) {
			if(u == t) return sum;
			int res = 0;
			for(int i = cur[u]; ~i && sum > 0; i = e[i].next) {
				cur[u] = i;
				int v = e[i].v;
				if(e[i].w > 0 && dis[v] == dis[u] + 1) {
					int k = DFS(v, min(sum, e[i].w));
					if(k == 0) dis[v] = Linf;
					e[i].w -= k, e[i ^ 1].w += k;
					res += k, sum -= k;
				}
			}
			return res;
		}
		
		int Dinic() {
			int ans = 0;
			while(BFS()) ans += DFS(s);
			return ans;
		}
		void solve() {
			int ans = sum - Dinic();
			for(int i = 1; i <= n0; ++i)
				if(dis[i] < 1e18) cout << i << ' ';
			cout << '\n';
			for(int i = n0+1; i <= n0+m; ++i)
				if(dis[i] < 1e18) cout << i - n0 << ' ';
			cout << '\n' << ans << '\n';
		}
	} G;
	
	void main() {
		G.init();
		G.solve();
	}
}

signed main() {
#ifdef filename
	FileOperations();
#endif
	
	signed _ = 1;
#ifdef multi_cases
	scanf("%d", &_);
#endif

	while(_--) Traveller::main();
	return 0;
}

套路

  • 看似是最大流但是不可做的题目,可以将最大收益转化成总收益减最小损失,用最小割建图处理。
  • 上下一定的依赖关系,可以有【下面有一个不选,上面的收益就要割掉】的建图。
  • 点权可以拆点转化成边权。

费用流

概念

网络的边不仅有容量,还有一个费用 cost,表示 1 单位流过所需费用。
我们主要研究最小费用最大流(Minimum Cost Maximum Flow),即在最大流的基础上费用要最小。最大费用最大流同理。

解法

以下陈述 MCMF 的解法。
我们在 Dinic 求最大流的时候,用 BFS 将当前残量网络处理出层次图,沿着最短路的更新方向增广。
现在我们需要在保证最大流的基础上,让费用最小。我们自然地想到用亖了的 SPFA 或 Bellman-Ford 处理出残量网络中 cost 的最短路层次图。
专家可以证明这是对的。
但是要注意,该算法不能用于求解 cost 有负环的网络,有负环时还需消圈。

Q:为什么要用 SPFA?
A:即使原图中 cost 没有负权边,但是建反边的时候要将 cost 赋为正边的相反数,以达到【退流】中反悔的目的。

luogu P3381 【模板】最小费用最大流

#include <bits/stdc++.h>
#define inf 0x3f3f3f3f
#define Linf 0x3f3f3f3f3f3f3f3f
#define pii pair<int, int> 
#define all(v) v.begin(), v.end()
#define int long long
using namespace std;

//#define filename "xxx" 
#define FileOperations() freopen(filename".in", "r", stdin), freopen(filename".out", "w", stdout)
//#define multi_cases 1

namespace Traveller {
	const int N = 5e3+2, M = 5e4+2;
	
	template<class T1, class T2>
	pair<T1, T2> operator + (pair<T1, T2> a, pair<T1, T2> b) { return pii(a.first + b.first, a.second + b.second); }
	
	struct Graph {
		int n, m, s, t;
		int head[N], tot, cur[N];
		struct edge {
			int v, w, cost, next;
			edge() { }
			edge(int a, int b, int c, int d) : v(a), w(b), cost(c), next(d) { }
		} e[M << 1];
		void add_edge(int u, int v, int w, int cost) {
			e[tot] = edge(v, w, cost, head[u]), head[u] = tot++;
			e[tot] = edge(u, 0, -cost, head[v]), head[v] = tot++;
		} 
		
		void init() {
			cin >> n >> m >> s >> t; 
			tot = 0, memset(head, -1, sizeof(head));
			for(int i = 1, u, v, w, cost; i <= m; ++i) {
				scanf("%lld%lld%lld%lld", &u, &v, &w, &cost);
				add_edge(u, v, w, cost);
			}
		}
		
		int dis[N], exist[N], vis[N];
		queue<int> q;
		bool SPFA() {
			for(int i = 1; i <= n; ++i) dis[i] = Linf, exist[i] = 0, cur[i] = head[i], vis[i] = 0;
			dis[s] = 0, exist[s] = 1;
			queue<int>().swap(q);
			q.push(s);
			while(!q.empty()) {
				int u = q.front();	q.pop();
				exist[u] = 0;
				for(int i = head[u]; ~i; i = e[i].next) {
					int v = e[i].v, cost = e[i].cost;
					if(e[i].w > 0 && dis[u] + cost < dis[v]) {
						dis[v] = dis[u] + cost;
						if(!exist[v]) exist[v] = 1, q.push(v);
					}
				}
			}
			return dis[t] < 1e18;
		}
		pii DFS(int u, int sum = Linf) {
			if(u == t) return pii(sum, 0);
			vis[u] = 1;
			int res = 0, c = 0;
			for(int i = cur[u]; ~i; i = cur[u] = e[i].next) {
				int v = e[i].v, cost = e[i].cost;
				if(vis[v]) continue;	//防止零环让 DFS 反复更新
				if(e[i].w > 0 && dis[v] == dis[u] + cost) {
					auto [a, b] = DFS(v, min(sum, e[i].w));
					if(a == 0) dis[v] = Linf;
					e[i].w -= a, e[i ^ 1].w += a;
					res += a, c += a * cost + b, sum -= a;
					if(sum == 0) break;
				} 
			}
			return pii(res, c);
		}
		pii Dinic() {
			pii ans = pii(0, 0);
			while(SPFA()) ans = ans + DFS(s);
			return ans;
		}
	} G;
	
	void main() {
		G.init();
		pii ans = G.Dinic();
		cout << ans.first << ' ' << ans.second;
	}
}

signed main() {
#ifdef filename
	FileOperations();
#endif
	
	signed _ = 1;
#ifdef multi_cases
	scanf("%d", &_);
#endif
	while(_--) Traveller::main();
	return 0;
}

最大费用最大流,将所有边的费用改为相反数,跑最小费用最大流之后费用取负即可。(前提是原图没有正环)

例题

luogu P4013 数字梯形问题

一堆路径从上面流到下面,一看就很最大流。
但是容量是什么呢?
我们发现,容量只能用来限制题目中的条件。于是,就可以考虑引入费用,计算最大收益。

对于第一问,每个【点】只能经过一次。经典地,将一个点拆成两个,中间连上容量为 1,费用为点权的边,称为点内边。
对于第二问,每条【边】只能经过一次。我们将第一问的点内边的容量改为 ,且最后一排点连到汇点的边的容量改为 即可。
对于第三问,除了源点连向第一排点的边,其他边容量全是

跑最大费用最大流即可。

#include <bits/stdc++.h>
#define inf 0x3f3f3f3f
#define Linf 0x3f3f3f3f3f3f3f3f
#define pii pair<int, int> 
#define all(v) v.begin(), v.end()
#define int long long
using namespace std;

//#define filename "xxx" 
#define FileOperations() freopen(filename".in", "r", stdin), freopen(filename".out", "w", stdout)
//#define multi_cases 1

namespace Traveller {
	const int N = 1402, M = 14002;
	
	template<class T1, class T2>
	pair<T1, T2> operator + (pair<T1, T2> a, pair<T1, T2> b) { return pii(a.first + b.first, a.second + b.second); }
	
	int n, m, a[42][42], pos[42][42], idx;
	
	struct Graph {
		int n, m, s, t;
		int head[N], tot, cur[N];
		struct edge {
			int v, w, cost, next;
			edge() { }
			edge(int a, int b, int c, int d) : v(a), w(b), cost(c), next(d) { }
		} e[M << 1];
		void add_edge(int u, int v, int w, int cost) {
			e[tot] = edge(v, w, cost, head[u]), head[u] = tot++;
			e[tot] = edge(u, 0, -cost, head[v]), head[v] = tot++;
		} 
		
		void init(int opt) {
			s = 2*idx + 1, t = 2*idx + 2;
			n = Traveller::n, m = Traveller::m;
			tot = 0, memset(head, -1, sizeof(head));
			for(int i = 1; i <= n; ++i)
				for(int j = 1; j <= m + i - 1; ++j) add_edge(pos[i][j], pos[i][j] + idx, opt == 1 ? 1 : Linf, -a[i][j]);
			for(int i = 1; i < n; ++i)
				for(int j = 1; j <= m + i - 1; ++j)
					add_edge(pos[i][j] + idx, pos[i+1][j], opt <= 2 ? 1 : Linf, 0),
					add_edge(pos[i][j] + idx, pos[i+1][j+1], opt <= 2 ? 1 : Linf, 0);
			for(int i = 1; i <= m; ++i) add_edge(s, pos[1][i], 1, 0);
			for(int i = 1; i <= n + m - 1; ++i) add_edge(pos[n][i] + idx, t, opt == 1 ? 1 : Linf, 0);
			n = 2*idx + 2;
		}
		
		int dis[N], exist[N], vis[N];
		queue<int> q;
		bool SPFA() {
			for(int i = 1; i <= n; ++i) dis[i] = Linf, exist[i] = 0, cur[i] = head[i], vis[i] = 0;
			dis[s] = 0, exist[s] = 1;
			queue<int>().swap(q);
			q.push(s);
			while(!q.empty()) {
				int u = q.front();	q.pop();
				exist[u] = 0;
				for(int i = head[u]; ~i; i = e[i].next) {
					int v = e[i].v, cost = e[i].cost;
					if(e[i].w > 0 && dis[u] + cost < dis[v]) {
						dis[v] = dis[u] + cost;
						if(!exist[v]) exist[v] = 1, q.push(v);
					}
				}
			}
			return dis[t] < 1e18;
		}
		pii DFS(int u, int sum = Linf) {
			if(u == t) return pii(sum, 0);
			vis[u] = 1;
			int res = 0, c = 0;
			for(int i = cur[u]; ~i; i = cur[u] = e[i].next) {
				int v = e[i].v, cost = e[i].cost;
				if(vis[v]) continue;
				if(e[i].w > 0 && dis[v] == dis[u] + cost) {
					auto [a, b] = DFS(v, min(sum, e[i].w));
					if(a == 0) dis[v] = Linf;
					e[i].w -= a, e[i ^ 1].w += a;
					res += a, c += a * cost + b, sum -= a;
					if(sum == 0) break;
				} 
			}
			return pii(res, c);
		}
		pii Dinic() {
			pii ans = pii(0, 0);
			while(SPFA()) ans = ans + DFS(s);
			return ans;
		}
		int solve() { return -Dinic().second; }
	} G;
	
	void main() {
		cin >> m >> n;
		for(int i = 1; i <= n; ++i)
			for(int j = 1; j <= m + i - 1; ++j) cin >> a[i][j];
		for(int i = 1; i <= n; ++i)
			for(int j = 1; j <= m + i - 1; ++j) pos[i][j] = ++idx;
		
		G.init(1), cout << G.solve() << '\n';
		G.init(2), cout << G.solve() << '\n';
		G.init(3), cout << G.solve() << '\n';
	}
}

signed main() {
#ifdef filename
	FileOperations();
#endif
	
	signed _ = 1;
#ifdef multi_cases
	scanf("%d", &_);
#endif
	while(_--) Traveller::main();
	return 0;
}

luogu P2604 [ZJOI2010] 网络扩容

第一问简单。记算出来的最大流为 f

第二问要求将最大流增加 k。现在的最大流就是 f+k,不能多不能少。
我们可以新建源点 s,向旧源点(1)连一条容量为 f+k,费用为 0 的边,达到限流目的。
我们将原图中的所有边分为【免费边】和【付费边】。【免费边】即为原来跑出最大流所需的,不用算在扩容费用中;【付费边】容量设为 ,表示想扩多少扩多少,但是要付费。跑一遍 MCMF 即可。

#include <bits/stdc++.h>
#define inf 0x3f3f3f3f
#define Linf 0x3f3f3f3f3f3f3f3f
#define pii pair<int, int> 
#define all(v) v.begin(), v.end()
#define int long long
using namespace std;

//#define filename "xxx" 
#define FileOperations() freopen(filename".in", "r", stdin), freopen(filename".out", "w", stdout)
//#define multi_cases 1

namespace Traveller {
	const int N = 5e3+2, M = 5e4+2;
	
	template<class T1, class T2>
	pair<T1, T2> operator + (pair<T1, T2> a, pair<T1, T2> b) { return pii(a.first + b.first, a.second + b.second); }
	
	struct node {
		int a, b, c, d;
		node() { }
		node(int a, int b, int c, int d) : a(a), b(b), c(c), d(d) { }
	};
	vector<node> vec;
	
	int ans;
	
	struct Graph {
		int n, m, k, s, t;
		int head[N], tot, cur[N];
		struct edge {
			int v, w, cost, next;
			edge() { }
			edge(int a, int b, int c, int d) : v(a), w(b), cost(c), next(d) { }
		} e[M << 1];
		void add_edge(int u, int v, int w, int cost) {
			e[tot] = edge(v, w, cost, head[u]), head[u] = tot++;
			e[tot] = edge(u, 0, -cost, head[v]), head[v] = tot++;
		} 
		
		void init() {
			cin >> n >> m >> k;
			s = 1, t = n; 
			tot = 0, memset(head, -1, sizeof(head));
			for(int i = 1, u, v, w, cost; i <= m; ++i) {
				scanf("%lld%lld%lld%lld", &u, &v, &w, &cost);
				add_edge(u, v, w, 0);
				vec.emplace_back(u, v, w, cost);
			}
		}
		void work() {
			s = n+1, t = n++;
			tot = 0, memset(head, -1, sizeof(head));
			add_edge(s, 1, ans+k, 0);
			for(auto [u, v, w, cost] : vec)
				add_edge(u, v, w, 0), add_edge(u, v, Linf, cost);
		}
		
		int dis[N], exist[N], vis[N];
		queue<int> q;
		bool SPFA() {
			for(int i = 1; i <= n; ++i) dis[i] = Linf, exist[i] = 0, cur[i] = head[i], vis[i] = 0;
			dis[s] = 0, exist[s] = 1;
			queue<int>().swap(q);
			q.push(s);
			while(!q.empty()) {
				int u = q.front();	q.pop();
				exist[u] = 0;
				for(int i = head[u]; ~i; i = e[i].next) {
					int v = e[i].v, cost = e[i].cost;
					if(e[i].w > 0 && dis[u] + cost < dis[v]) {
						dis[v] = dis[u] + cost;
						if(!exist[v]) exist[v] = 1, q.push(v);
					}
				}
			}
			return dis[t] < 1e18;
		}
		pii DFS(int u, int sum = Linf) {
			if(u == t) return pii(sum, 0);
			vis[u] = 1;
			int res = 0, c = 0;
			for(int i = cur[u]; ~i && sum > 0; i = cur[u] = e[i].next) {
				int v = e[i].v, cost = e[i].cost;
				if(vis[v]) continue;
				if(e[i].w > 0 && dis[v] == dis[u] + cost) {
					auto [a, b] = DFS(v, min(sum, e[i].w));
					if(a == 0) dis[v] = Linf;
					e[i].w -= a, e[i ^ 1].w += a;
					res += a, c += a * cost + b, sum -= a;
				} 
			}
			return pii(res, c);
		}
		pii Dinic() {
			pii ans = pii(0, 0);
			while(SPFA()) ans = ans + DFS(s);
			cerr << '\n';
			return ans;
		}
	} G;
	
	void main() {
		G.init();
		cout << (ans = G.Dinic().first) << ' ';
		G.work();
		cout << G.Dinic().second << '\n';
	}
}

signed main() {
#ifdef filename
	FileOperations();
#endif
	
	signed _ = 1;
#ifdef multi_cases
	scanf("%d", &_);
#endif
	while(_--) Traveller::main();
	return 0;
}
posted @   wfc284  阅读(10)  评论(0编辑  收藏  举报
点击右上角即可分享
微信分享提示
评论
收藏
关注
推荐
深色
回顶
收起