网络流学习记录

跟开了森林书一样。

最大流

概念

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

有点抽象,举个例子:


这里有一个源点为 \(1\),汇点为 \(6\) 的网络。我们从 \(1\)\(6\)\(3\) 条路径可以走:\(1 \to 2 \to 4 \to 6,1 \to 3 \to 5 \to 6,1 \to 3 \to 4 \to 6\)

首先,我们走 \(1 \to 2 \to 4 \to 6\) 这条路径,可以通过 \(1\) 的水(即路径最小值)。
其次,我们走 \(1 \to 3 \to 4 \to 6\) 这条路径,可以通过 \(1\) 的水。(\(4 \to 6\) 这条边已经有 \(1\) 的水流过了,还剩 \(1\) 的容量)。
最后,我们走 \(1 \to 3 \to 5 \to 6\) 这条路径,可以通过 \(2\) 的水。

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

解法

Ford–Fulkerson 方法

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

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

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

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

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

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

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

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

可能以 \(1 \to 3 \to 2 \to 4\)\(1 \to 2 \to 3 \to 4\) 两条路径反复增广约 \(2 \times 10 ^9\) 次。
总复杂度是和值域 \(F\) 有关的,可能还要挂上一个 \(n\)\(m\)。总时间复杂度不敢想象,可能是 \(O(nmF)\) 的。

于是要优化。

Edmonds-Karp 算法

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

Dinic 算法

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

有两个优化需要注意:

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

总时间复杂度 \(O(n^2m)\)

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)\)
这时,跑从 \(s\)\(t\) 的最大流就是二分图的最大匹配。
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,设 \(f_i\)\(a_i\) 结尾的 LIS 即可。

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

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

混合图欧拉回路判定

即,混合图中的无向边定向之后有欧拉回路。
定向之后判断是很容易的,即每个点的入度都等于度数 \(d_i\) 的一半。

考虑用 flow 刻画定向的过程。
原图中边集中的元素 \(e_i=(u_i, v_i)\) 抽象成二分图的左部点,点集中元素抽象成右部点。

定向相当于,源点 \(s\)\(e_i\) 连容量为 \(1\) 的边,卡流量!
\(e_i\)\(u_i\)\(v_i\) 连边,意义是提供入度(无向就连两个,有向就连一个)。
右部点向汇点连边,容量为 \(d_i/2\)

判定就是是否满流。

DAG 最小路径覆盖

用最少的路径(不在点和边处相交)覆盖全图。

相当于找尽可能多的点有前驱。
每个点的前驱不能相同!想到拆点。拆成 \(u, u'\)

然后对于原图中的 \((u, v)\),在新的图上连边 \(u\to v'\)
发现最多有前驱的点的个数就是这个二分图的最大匹配。

于是最小路径覆盖等于 \(n\) 减去二分图最大匹配。
构造就按照匹配来。

问题:如果允许一个点被覆盖多次呢?

这个问题里,默认 \(n,m\) 同阶。

也就是,一条路径可以在中间断开。
一种很唐的思路是,跑一遍传递闭包,然后做上面的过程。
但这时候边数是 \(O(n^2)\) 级别的。太大。

然后看看传递闭包到底慢在哪。
它把一个 \(O(m)\) 能描述的结构扩成了 \(O(n^2)\) 的结构。

仍然拆点。对于每一条边,考虑这样一个结构:

在它上面跑 flow。

这时候一个增广路的含义就相当于二分图的一条匹配边(设为 \(s\to v_1\to v_2\to\dots\to v_k\to t\),那么相当于匹配上了 \(v_1\to v_k\))!并且自动涵盖了可达关系。

构造不会。咕咕咕。(参见习题 [QOJ 6239](https://qoj.ac/problem/6329)

套路

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

最小割

概念

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

解法

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

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

      \(1 \to 2 , 2 \to 4\) 都是满流边,显然不对。

例题

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

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

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

#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\),连向所有白点,权值为白点的点权;白点连向各自相邻的黑点,权值为 \(\infty\);所有黑点连向汇点 \(t\),权值为黑点的点权。
最小割就是最小损失。

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

#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\),用 \(s\)\(u\) 连容量为同是文科的收益,\(u\) 分别向 \(x, y\) 连容量是 \(\infty\) 的边。这样就可以保证上述条件。
同时理科同理,改成向汇点连边。

#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\),费用为点权的边,称为点内边。
对于第二问,每条【边】只能经过一次。我们将第一问的点内边的容量改为 \(\infty\),且最后一排点连到汇点的边的容量改为 \(\infty\) 即可。
对于第三问,除了源点连向第一排点的边,其他边容量全是 \(\infty\)

跑最大费用最大流即可。

#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\) 的边,达到限流目的。
我们将原图中的所有边分为【免费边】和【付费边】。【免费边】即为原来跑出最大流所需的,不用算在扩容费用中;【付费边】容量设为 \(\infty\),表示想扩多少扩多少,但是要付费。跑一遍 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;
}

luogu P1251 餐巾计划问题

看起来很傻。源点连向每一天的开头(表示餐巾是干净的,记作 \(i_1\))。每一天结束(表示餐巾是脏的,记作 \(i_2\))连向汇点。容量均为无穷,费用均为 \(p\)

然后快洗就是 \(i_2 \to (i+m)_1\) 连费用是 \(f\) 的边。
每天的需求量就可以表征为,\(i_1 \to i_2\)至少流当天需求量(记为 \(a_i\))的流。这里有点困难。

改造这个图。
\(t\) 先连向 \(s\),容量无穷,费用 \(0\)。这样就转化为了一个“上下界最小可行流”。

然后显然不愿意跑这个。原来不是 \(i_1 \to i_2\) 吗,现在改一下,建一个超级源点 \(s'\),一个超级汇点 \(t'\),将上面这条边拆掉,改为 \(i_1 \to t',s'\to i_2\)。容量均为 \(a_i\),费用为 \(0\)

这样跑 MCMF 就行了。

这样转化“至少边”的妙处在于,最大流一定能满流,满流就一定能达到每天 \(a_i\) 的下界限制。

luogu P3980 志愿者招募

难点仍然是如何控制“至少边”。

解决方法很简单。源点连向第一天,容量为 \(M\)(一个大于总人数的值),费用为 \(0\)。“至少边”套路地转化为保证满流的结构。

具体地,第 \(i\) 天指向第 \(i+1\) 天,第 \(n+1\) 天为汇点。容量为 \(M-a_i\),然后对于所有的 \((s,t,c)\) 限制,将第 \(s\) 天连向第 \(t\) 天,容量为无穷,费用为 \(c\)。这个结构可以保证 \(s\to t\) 这条边满流!

跑 MCMF 即可。

posted @ 2025-01-27 22:44  Water_M  阅读(21)  评论(0)    收藏  举报