网络流算法与建模

网络流简介

网络流其实是图论问题。

对于一张有向图,每条边 \((u,v)\) 设一个容量限制 \(c(u,v)\)。再设源点 \(s\),汇点 \(t\)

对于每一条边 \((u,v)\),设流量 \(f(u,v)\),满足 \(0\leq f(u,v)\leq c(u,v)\)

定义点 \(u\) 的净流量 \(\displaystyle f(u)=\sum_{v\in V}f(u,v)-\sum_{v\in V}f(v,u)\)。显然,除 \(s,t\) 外,所有点的净流量均为 \(0\)

那么,网络流就可以处理一些流量的问题。

常见问题有:

  • 最大流。
  • 最小割。
  • 费用流。

网络流算法

最大流

luogu P3376 【模板】网络最大流

给出一个 \(n\) 个点 \(m\) 条边的网络图,以及其源点 \(s\) 和汇点 \(t\),求出其网络最大流。

\(1\leq n\leq200,1\leq m\leq5000,0\leq w<2^{31}\)

Ford–Fulkerson 增广

设剩余容量 \(c_f(u,v)=c(u,v)-f(u,v)\),将所有剩余容量大于 \(0\) 的边构成的子图称为残量网络 \(G_f\)

如果 \(G_f\) 上存在一条 \(s\)\(t\) 的路径,且路径上所有的剩余容量都大于 \(0\),那么就称这条路径为增广路

一种显然的算法是不断找增广路,并且将增广路上的每一条边都增加等量流量,这个过程称为增广

但是考虑到直接增广可能不是最优,因此引入反悔操作。对于每一条边 \((u,v)\) 建反边 \((v,u)\),钦定 \(f(v,u)=-f(u,v)\) 恒成立,更新 \(f(u,v)\) 同时更新 \(f(v,u)\)

引入反边后,实际上增广时可以通过反边来进行「退流」。

例如上图中所有边的容量限制均为 \(1\)。左图 \(u\rightarrow v\) 直接增广,没有利用 \(p,q\),最大流为 \(1\);但是中图 \(u\rightarrow q,p\rightarrow v\) 增广最大流为 \(2\)

本质上是因为左图没有利用 \(p,q\) 增广,而中图充分利用了。引入退流后,最右图 \(p\rightarrow v\rightarrow u\rightarrow q,u\rightarrow v\) 增广完全等价\(u\rightarrow q,p\rightarrow v\),最大流为 \(2\)

重复增广直到增广路不存在,即可得到最大流。

Ford-Fulkerson 增广有很多不同的实现,主流有 EK 和 Dinic。

正确性 & 时间复杂度

最大流最小割定理指出,当 \(G_f\) 上不再存在 \(s\)\(t\) 的路径时,取到最大流。即一直增广直到增广路不存在。

单轮增广时间复杂度为 \(\mathcal O(m)\),则 Ford-Fulkerson 增广的时间复杂度上界为 \(\mathcal O(m\vert f\vert)\)。当然,这只是一个基于答案分析得到的非常宽松的上界,实际上 EK、Dinic 并不会达到。

EK 算法

Edmonds-Karp 算法。

显然主要在于寻找 \(s\rightarrow t\) 的增广路,考虑在 \(G_f\) 上从 \(s\) 开始 BFS 即可。

对于找到的路径 \(p\),将 \(p\) 中的每一条边的流量和最大流增加 \(\displaystyle\min_{(u,v)\in p}c_f(u,v)\),并且同时退流。

直到不存在增广路,即得到了最大流。

时间复杂度 \(\mathcal O\left(nm^2\right)\)

实际实现上,可以直接维护每条边的剩余容量。找反边可以利用邻接表。

需要注意的是,即便初始时剩余容量在 int 范围内,实际运行过程中由于退流操作的存在,也可能会加至爆 int。需要注意数据范围。

参考代码
//#include<bits/stdc++.h>
#include<algorithm>
#include<iostream>
#include<cstring>
#include<iomanip>
#include<cstdio>
#include<string>
#include<vector>
#include<cmath>
#include<ctime>
#include<deque>
#include<queue>
#include<stack>
#include<list>
using namespace std;
typedef long long ll;
constexpr const int N=200,M=5000;
constexpr const ll inf=0x3f3f3f3f3f3f3f3f;
struct graph{
	struct edge{
		int v,r;
		ll w;
	}a[(M<<1)+1+1];
	int size=1,h[N+1];
	inline void create(int u,int v,int w){
		a[++size]={v,h[u],w};
		h[u]=size;
	}
	edge& operator [](int x){
		return a[x];
	}
}g;
int n,s,t,pre[N+1];
ll dis[N+1];
bool bfs(){
	static bool vis[N+1];
	memset(vis,0,sizeof(vis));
	queue<int>q;
	q.push(s);
	vis[s]=true;
	dis[s]=inf;
	while(q.size()){
		int x=q.front();q.pop();
		for(int i=g.h[x];i;i=g[i].r){
			auto [v,r,w]=g[i];
			if(w&&!vis[v]){
				q.push(v);
				pre[v]=i;
				dis[v]=min(dis[x],w);
				vis[v]=true;
				if(v==t){
					return true;
				}
			}
		}
	}
	return false;
}
ll EK(){
	ll ans=0;
	while(bfs()){
		int x=t;
		while(x!=s){
			g[pre[x]].w-=dis[t];
			g[pre[x]^1].w+=dis[t];
			x=g[pre[x]^1].v;
		}
		ans+=dis[t]; 
	}
	return ans;
}
int main(){
	/*freopen("test.in","r",stdin);
	freopen("test.out","w",stdout);*/
	
	ios::sync_with_stdio(false);
	cin.tie(0);cout.tie(0);
	
	int m;
	cin>>n>>m>>s>>t;
	for(int i=1;i<=m;i++){
		int u,v,w;
		cin>>u>>v>>w;
		g.create(u,v,w);
		g.create(v,u,0);
	}
	cout<<EK()<<'\n';
	
	cout.flush();
	
	/*fclose(stdin);
	fclose(stdout);*/
	return 0;
}

Dinic 算法

\(G_f\) 进行 BFS,按照深度分层。只保留每一个点到下一层的边,得到层次图 \(G_L\)

定义阻塞流 \(f_b\) 为一个不能继续增大的流。

Dinic 算法流程即重复执行:

  1. \(G_f\) 上 BFS,得到 \(G_L\)
  2. \(G_L\) 上 DFS,得到阻塞流 \(f_b\)
  3. 合并 \(f,f_b\),更新最大流。
  4. 重复执行,直到 \(G_L\) 上不存在 \(s\rightarrow t\) 的路径。

BFS 分层是简单的,考虑 DFS 求阻塞流 \(f_b\) 如何实现。

只需要在 DFS 到 \(x\) 的过程中维护 \(\textit{Min}\),表示 \(s\rightarrow x\) 的路径上最小的剩余容量为 \(\textit{Min}\)。DFS 结束 \(x\) 后,返回 \(\textit{ans}\) 表示流的增大量。

更新这条边的剩余容量和反边即可。

当前弧优化

考虑同一轮 BFS 后,$x$ 的下一层节点 $v_1,v_2,v_3,\cdots$。

如果上一次 DFS 遍历到 $v_1,v_2,\cdots,v_k$ 的时候,已经有 $\textit{Min}=0$,则下一次 DFS 到 $x$ 时,可以直接从 $v_k$ 开始遍历处理子节点。正确性是显然的,因为 $v_1\sim v_{k-1}$ 都已经在 $\textit{Min}>0$ 的时候处理掉了,利用了所有的剩余容量。

当前弧优化其实是 Dinic 时间复杂度的保证,不加当前弧优化的 Dinic 复杂度是错的。

Dinic 时间复杂度是 \(\mathcal O\left(n^2m\right)\),同时:

  • 在所有边的容量均为 \(1\) 的网络上,Dinic 复杂度为 \(\mathcal O\left(m\min\left(m^{\frac12},n^{\frac23}\right)\right)\)

  • 在所有边的容量均为 \(1\),且除 \(s,t\) 外均有入度为 \(1\) 或出度为 \(1\),则 Dinic 的时间复杂度为 \(\mathcal O\left(m\sqrt n\right)\)

    这可以应用到求解二分图最大匹配的 Hopcroft-Karp 算法上。

参考代码
//#include<bits/stdc++.h>
#include<algorithm>
#include<iostream>
#include<cstring>
#include<iomanip>
#include<cstdio>
#include<string>
#include<vector>
#include<cmath>
#include<ctime>
#include<deque>
#include<queue>
#include<stack>
#include<list>
using namespace std;
using ll=long long;
constexpr const int N=200,M=5000;
constexpr const ll inf=0x3f3f3f3f3f3f3f3f;
struct graph{
	struct edge{
		int v,r;
		ll w;
	}a[(M<<1)+1+1];
	int size=1,h[N+1];
	inline void create(int u,int v,int w){
		a[++size]={v,h[u],w};
		h[u]=size;
	}
	edge& operator [](int x){
		return a[x];
	}
}g;
int n,s,t,cur[N+1];
ll dis[N+1];
bool bfs(){
	memset(dis,0x3f,sizeof(dis));
	for(int i=1;i<=n;i++){
		cur[i]=g.h[i];
	}
	queue<int>q;
	q.push(s);
	dis[s]=0;
	while(q.size()){
		int x=q.front();q.pop();
		for(int i=g.h[x];i;i=g[i].r){
			auto [v,r,w]=g[i];
			if(w>0&&dis[v]==inf){
				q.push(v);
				dis[v]=dis[x]+1;
				if(v==t){
					return true;
				}
			}
		}
	}
	return false;
}
ll dfs(int x,ll Min){
	if(x==t){
		return Min;
	}
	ll ans=0;
	for(int i=cur[x];i&&Min;i=g[i].r){
		cur[x]=i;
		auto [v,r,w]=g[i];
		if(w&&dis[v]==dis[x]+1){
			ll pl=dfs(v,min(Min,w));
			if(!pl){
				dis[v]=inf;
			}
			g[i].w-=pl;
			g[i^1].w+=pl;
			ans+=pl;
			Min-=pl;
		}
	}
	return ans;
}
ll Dinic(){
	ll ans=0;
	while(bfs()){
		ans+=dfs(s,inf);
	}
	return ans;
} 
int main(){
	/*freopen("test.in","r",stdin);
	freopen("test.out","w",stdout);*/
	
	ios::sync_with_stdio(false);
	cin.tie(0);cout.tie(0);
	
	int m;
	cin>>n>>m>>s>>t;
	for(int i=1;i<=m;i++){
		int u,v,w;
		cin>>u>>v>>w;
		g.create(u,v,w);
		g.create(v,u,0);
	}
	cout<<Dinic()<<'\n';
	
	cout.flush();
	
	/*fclose(stdin);
	fclose(stdout);*/
	return 0;
}

最小割

对于一张网络 \(G=(V,E)\),定义割 \(\set{S,T}\) 为一种点的划分方式,满足 \(S\cap T=\varnothing,S\cup T=V\),且 \(s\in S,t\in T\)

定义割 \(\set{S,T}\) 的容量为 \(\vert\vert S,T\vert\vert=\displaystyle\sum_{u\in S}\sum_{v\in T}c(u,v)\)

最小割就是求得 \(\set{S,T}\),使得 \(\vert\vert S,T\vert\vert\) 最小。

Kőnig 定理是最大流最小割定理的特殊情形。

最大流最小割定理

最大流最小割定理指出,最大流 \(f\) 和最小割 \(\set{S,T}\) 总是满足 \(\vert f\vert=\vert\vert S,T\vert\vert\)

也就是说,最大流等于最小割。

证明

先考虑一个引理:对于网络 \(G=(V,E)\)\(\vert f\vert\leq\vert\vert S,T\vert\vert\),且当且仅当 \(\set{(u,v)\mid u\in S,v\in T}\) 满流,\(\set{(u,v)\mid u\in T,v\in S}\) 空流时取等。

\[\begin{aligned} \vert f\vert&=f(s)\\ &=\sum_{u\in S}f(u)\\ &=\sum_{u\in S}\left(\sum_{v\in V}f(u,v)-\sum_{v\in V}f(v,u)\right)\\ &=\sum_{u\in S}\left(\sum_{v\in T}f(u,v)+\sum_{v\in S}f(u,v)-\sum_{v\in T}f(v,u)-\sum_{v\in S}f(v,u)\right)\\ &=\sum_{u\in S}\left(\sum_{v\in T}f(u,v)-\sum_{v\in T}f(v,u)\right)+\sum_{u\in S}\sum_{v\in S}f(u,v)-\sum_{u\in S}\sum_{v\in S}f(v,u)\\ &=\sum_{u\in S}\sum_{v\in T}\left(f(u,v)-f(v,u)\right)\\ &\leq\sum_{u\in S}\sum_{v\in T}f(u,v)\\ &\leq\sum_{u\in S}\sum_{v\in T}c(u,v)\\ &=\vert\vert S,T\vert\vert \end{aligned} \]

\(\set{(u,v)\mid u\in T,v\in S}\) 空流时,取到 \(\displaystyle\vert f\vert=\sum_{u\in S}\sum_{v\in T}f(u,v)\)

\(\set{(u,v)\mid u\in S,v\in T}\) 满流时,取到 \(\displaystyle\vert f\vert=\sum_{u\in S}\sum_{v\in S}c(u,v)=\vert\vert S,T\vert\vert\) 最大。


假设 \(G=(V,E)\) 的某一轮增广后得到了流 \(f\),使得 \(G_f\) 上不存在增广路。记 \(s\) 出发可以到达的所有点构成的点集为 \(S\)\(T=V\setminus S\)

\(\set{S,T}\)\(G_f\) 的割,且 \(\displaystyle\vert\vert S,T\vert\vert=\sum_{u\in S}\sum_{v\in T}c_f(u,v)=0\)。(因为 \(\set{S,T}\)\(G_f\) 而不是 \(G\) 的割,因此统计的是 \(c_f\) 而不是 \(c\)。)

对于 \(u\in S,v\in T\) 的边 \((u,v)\) 进行分类讨论:

  • \((u,v)\in E\)\(c_f(u,v)=0\),则 \(f(u,v)=c(u,v)\),即 \(\set{(u,v)\mid u\in S,v\in T}\) 满流。
  • \((v,u)\in E\)\(c_f(u,v)=c(u,v)-f(u,v)=0-f(u,v)=0\)\(f(u,v)=0\),即 \(\set{(u,v)\mid u\in T,v\in S}\) 空流。

因此,总有 \(\set{(u,v)\mid u\in S,v\in T}\)\(\set{(u,v)\mid u\in T,v\in S}\) 空流时,有最大流 \(\vert f\vert\) 和最小割 \(\vert\vert S,T\vert\vert\) 满足 \(\vert f\vert=\vert\vert S,T\vert\vert\)

方案构造

求解最小割的大小是简单的,直接跑最大流即可。

那么 \(\set{S,T}\) 的具体构造,其实也就是最大流最小割定理中的 \(s\) 可以到达的所有点的点集 \(S\)\(T=V\setminus S\)

UVA10480 Sabotage

给定一张无向图,每条边有断边代价,断开一些边使得 \(1,2\) 不连通,求代价和最小的方案。

考虑 \(1\) 为源点,\(2\) 为汇点,那么原问题等价于最小割,对于 \(G=(V,E)\) 的最小割 \(\set{S,T}\)\(\set{(u,v)\in E\mid u\in S,v\in T}\) 即为所有断开的边。

可能特别需要考虑一下原图为无向图,那么将无向边转化为两条有向边,容量限制均为原来无向边的限制即可。

参考代码
//#include<bits/stdc++.h>
#include<algorithm>
#include<iostream>
#include<cstring>
#include<iomanip>
#include<cstdio>
#include<string>
#include<vector>
#include<cmath>
#include<ctime>
#include<deque>
#include<queue>
#include<stack>
#include<list>
using namespace std;
typedef long long ll;
constexpr const int N=50,M=500;
constexpr const ll inf=0x3f3f3f3f3f3f3f3f;
struct graph{
	struct edge{
		int v,r;
		ll w;
	}g[M<<2|1];
	
	int size=1,h[N+1];
	void create(int u,int v,int w){
		g[++size]={v,h[u],w};
		h[u]=size;
	}
	edge& operator [](int x){
		return g[x];
	}
	void clear(){
		size=1;
		memset(h,0,sizeof(h));
	} 
}g;
int n,s=1,t=2,cur[N+1];
ll dis[N+1];
bool bfs(){
	memset(dis,0x3f,sizeof(dis));
	for(int i=1;i<=n;i++){
		cur[i]=g.h[i];
	}
	queue<int>q;
	q.push(s);
	dis[s]=0;
	while(q.size()){
		int x=q.front();q.pop();
		for(int i=g.h[x];i;i=g[i].r){
			auto [v,r,w]=g[i];
			if(w>0&&dis[v]==inf){
				q.push(v);
				dis[v]=dis[x]+1;
				if(v==t){
					return true;
				}
			}
		}
	}
	return false;
}
ll dfs(int x,ll Min){
	if(x==t){
		return Min;
	}
	ll ans=0;
	for(int i=cur[x];i&&Min;i=g[i].r){
		cur[x]=i;
		auto [v,r,w]=g[i];
		if(w&&dis[v]==dis[x]+1){
			ll pl=dfs(v,min(Min,w));
			if(!pl){
				dis[v]=inf;
			}
			g[i].w-=pl;
			g[i^1].w+=pl;
			ans+=pl;
			Min-=pl;
		}
	}
	return ans;
}
ll Dinic(){
	ll ans=0;
	while(bfs()){
		ans+=dfs(s,inf);
	}
	return ans;
};
bool inS[N+1];
void dfs1(int x){
	if(inS[x]){
		return;
	}
	inS[x]=true;
	for(int i=g.h[x];i;i=g[i].r){
		auto [v,r,w]=g[i];
		if(w){
			dfs1(v);
		}
	}
}
int main(){
	/*freopen("test.in","r",stdin);
	freopen("test.out","w",stdout);*/
	
	ios::sync_with_stdio(false);
	cin.tie(0);cout.tie(0);
	
	while(true){
		int m;
		cin>>n>>m;
		if(!n&&!m){
			break; 
		}
		g.clear();
		while(m--){
			int u,v,w;
			cin>>u>>v>>w;
			g.create(u,v,w);
			g.create(v,u,w); 
		}
		Dinic();
		memset(inS,0,sizeof(inS));
		dfs1(s);
		for(int u=1;u<=n;u++){
			for(int i=g.h[u];i;i=g[i].r){
				auto [v,r,w]=g[i];
				if(inS[u]&&!inS[v]){
					cout<<u<<' '<<v<<'\n';
				}
			}
		}
		cout<<'\n';
		cout.flush();
	} 
	
	cout.flush();
	 
	/*fclose(stdin);
	fclose(stdout);*/
	return 0;
}

费用流

对于网络 \(G=(V,E)\),每条边除了容量限制 \(c(u,v)\),还有单位流量的费用 \(w(u,v)\)

流量为 \(f(u,v)\) 时,总费用为 \(f(u,v)\cdot w(u,v)\)

\(w\) 满足斜对称性,即 \(w(u,v)=-w(v,u)\)

求总花费最小的最大流称为最小费用最大流,即在最大化 \(\displaystyle\sum_{(u,v)\in E}f(u,v)\) 的情况下,最小化 \(\displaystyle\sum_{(u,v)\in E}f(u,v)\cdot w(u,v)\)

SSP 算法

每次寻找单位费用最小的增广路进行增广,直到图上不存在增广路为止。

实际上也属于 Ford-Fulkerson 增广,用 Bellman-Ford 求最短路,时间复杂度为 \(\mathcal O(nm\vert f\vert)\)

但是 Bellman-Ford 会跑满 \(\mathcal O(nm)\),一般写的都是 SPFA。

对于具体实现,以 Dinic 实现为例,SSP 就是把 BFS 分层,换成 SPFA 分层,每个点的 dis 从到 \(s\) 的距离,改为了到 \(s\) 的链上的单位费用和的最小值。

特别地,考虑单位费用可能为负,因此 Dinic 的 dfs 实现的时候需要记录 vis。不然可能会重复访问,然后无限递归。

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

给定一张网络的点数 \(n\),边数 \(m\),源点 \(s\),汇点 \(t\),和 \(m\)\(u\rightarrow v\) 的费用为 \(w\)、容量为 \(c\) 的有向边。

求最小费用最大流。

\(1\leq n\leq5\times10^3,1\leq m\leq5\times10^4\)\(0\leq w,c\leq10^3\),最大流、最小费用不超过 \(2^{31}-1\),数据随机生成。

数据随机生成,可以跑 SPFA,总时间复杂度 \(\mathcal O(m\vert f\vert)\)

参考代码
//#include<bits/stdc++.h>
#include<algorithm>
#include<iostream>
#include<cstring>
#include<iomanip>
#include<cstdio>
#include<string>
#include<vector>
#include<cmath>
#include<ctime>
#include<deque>
#include<queue>
#include<stack>
#include<list>
using namespace std;
constexpr const int N=5e3,M=5e4,inf=0x3f3f3f3f;
struct graph{
	struct edge{
		int v,r,w,c;
	}g[M<<2|1];
	
	int h[N+1],size=1;
	void create(int u,int v,int w,int c){
		g[++size]={v,h[u],w,c};
		h[u]=size;
	}
	
	edge& operator [](int x){
		return g[x];
	} 
}g;
int n,s,t,dis[N+1],cur[N+1];
bool SPFA(){
	static bool in[N+1];
	memset(in,0,sizeof(in));
	memset(dis,0x3f,sizeof(dis));
	for(int i=1;i<=n;i++){
		cur[i]=g.h[i];
	}
	queue<int>q;
	q.push(s);
	dis[s]=0;
	in[s]=true;
	while(q.size()){
		int x=q.front();q.pop();
		in[x]=false;
		for(int i=g.h[x];i;i=g[i].r){
			auto [v,r,w,c]=g[i];
			if(w&&dis[v]>dis[x]+c){
				dis[v]=dis[x]+c;
				if(!in[v]){
					q.push(v);
					in[v]=true;
				}
			}
		}
	}
	return dis[t]!=inf;
}
bool vis[N+1];
pair<int,int> dfs(int x,int Min){
	if(x==t){
		return {Min,0};
	}
	vis[x]=true;
	int ansW=0,ansC=0;
	for(int i=cur[x];i&&Min;i=g[i].r){
		cur[x]=i; 
		auto [v,r,w,c]=g[i];
		if(!vis[v]&&w&&dis[v]==dis[x]+c){
			auto [plW,plC]=dfs(v,min(w,Min));
			if(!plW){
				dis[v]=inf;
			}
			g[i].w-=plW;
			g[i^1].w+=plW;
			Min-=plW;
			ansW+=plW;
			ansC+=plW*c+plC;
		}
	}
	vis[x]=false;
	return {ansW,ansC};
}
pair<int,int> Dinic(){
	pair<int,int>ans;
	while(SPFA()){
		auto pl=dfs(s,inf);
		ans.first+=pl.first;
		ans.second+=pl.second;
	}
	return ans;
}
int main(){
	/*freopen("test.in","r",stdin);
	freopen("test.out","w",stdout);*/
	
	ios::sync_with_stdio(false);
	cin.tie(0);cout.tie(0);
	
	int m;
	cin>>n>>m>>s>>t;
	while(m--){
		int u,v,w,c;
		cin>>u>>v>>w>>c;
		g.create(u,v,w,c);
		g.create(v,u,0,-c);
	}
	auto [w,c]=Dinic();
	cout<<w<<' '<<c<<'\n';
	
	cout.flush();
	 
	/*fclose(stdin);
	fclose(stdout);*/
	return 0;
}

网络流建模

最小割除数值上与最大流相等外,和最大流的性质没有任何关系。

因此,最小割问题中边的容量,其实可以直接理解为普通权值。

二元选择

其实可以视为切糕模型的特殊形式。

\(n\) 个物品和两个集合 \(A,B\),如果物品 \(i\) 未放入 \(A\) 花费 \(a_i\),未放入 \(B\) 花费 \(b_i\)

同时给定 \(u_i,v_i,w_i\),若 \(u_i,v_i\) 不在同一集合内,花费 \(w_i\)

一个物品只能放入一个集合,求最小代价和。


二者选其一,考虑网络流建模。

设置源点 \(s\),代表集合 \(A\);设置汇点 \(t\) 代表集合 \(B\)

对于边 \(i\rightarrow j\),表示 \(i\) 要求 \(j\)\(i\) 处于一个集合。其容量 \(c(i,j)\) 表示 \(i,j\) 不在同一集合时的花费。

具体而言,连边 \(s\rightarrow i\) 容量为 \(a_i\)\(i\rightarrow t\) 容量为 \(b_i\)\(u_i\rightarrow v_i,v_i\rightarrow u_i\) 容量均为 \(w_i\)

对于冲突,考虑花费最小,因此割掉的边的容量之和要最小,即最小割。

求最小割即可。由最大流最小割定理,求最大流即可。


luogu P2057 [SHOI2007] 善意的投票 / [JLOI2010] 冠军调查

\(n\) 个人,放入 \(A,B\) 两个集合,\(A\) 表示反对,\(B\) 表示赞成。

设意愿 \(c_1,c_2,\cdots,c_n\),对于 \(i\),未放入 \(A\) 花费 \([c_i=0]\),未放入 \(B\) 花费 \([c_i=1]\)

给定 \(m\)\((i,j)\),如果 \(i,j\) 不在同一集合内,花费 \(1\)

\(2\leq n\leq300,1\leq m\leq\dfrac{n(n-1)}2\)

\(s=n+1,t=n+2\)

按照上述方式连边,求解即可。

参考代码
//#include<bits/stdc++.h>
#include<algorithm>
#include<iostream>
#include<cstring>
#include<iomanip>
#include<cstdio>
#include<string>
#include<vector>
#include<cmath>
#include<ctime>
#include<deque>
#include<queue>
#include<stack>
#include<list>
using namespace std;
constexpr const int N=300+2,M=N*(N-1)>>1,inf=0x3f3f3f3f;
struct graph{
	struct edge{
		int v,r,w;
	}g[M+N*2<<1|1];
	
	int h[N+1],size=1;
	void create(int u,int v,int w){
		g[++size]={v,h[u],w};
		h[u]=size;
	}
	edge& operator [](int x){
		return g[x];
	}
}g;
int n,s,t,c[N+1],cur[N+1],dis[N+1];
bool bfs(){
	memset(dis,0x3f,sizeof(dis));
	for(int i=1;i<=n;i++){
		cur[i]=g.h[i];
	}
	queue<int>q;
	q.push(s);
	dis[s]=0;
	while(q.size()){
		int x=q.front();q.pop();
		for(int i=g.h[x];i;i=g[i].r){
			auto [v,r,w]=g[i];
			if(w>0&&dis[v]==inf){
				q.push(v);
				dis[v]=dis[x]+1;
				if(v==t){
					return true;
				}
			}
		}
	}
	return false;
}
int dfs(int x,int Min){
	if(x==t){
		return Min;
	}
	int ans=0;
	for(int i=cur[x];i&&Min;i=g[i].r){
		cur[x]=i;
		auto [v,r,w]=g[i];
		if(w&&dis[v]==dis[x]+1){
			int pl=dfs(v,min(Min,w));
			if(!pl){
				dis[v]=inf;
			}
			g[i].w-=pl;
			g[i^1].w+=pl;
			ans+=pl;
			Min-=pl;
		}
	}
	return ans;
}
int Dinic(){
	int ans=0;
	while(bfs()){
		ans+=dfs(s,inf);
	}
	return ans;
}
int main(){
	/*freopen("test.in","r",stdin);
	freopen("test.out","w",stdout);*/
	
	ios::sync_with_stdio(false);
	cin.tie(0);cout.tie(0);
	
	int m;
	cin>>n>>m;
	for(int i=1;i<=n;i++){
		cin>>c[i];
	}
	s=n+1,t=n+2; 
	for(int i=1;i<=n;i++){
		g.create(s,i,c[i]==0);
		g.create(i,s,0); 
		g.create(i,t,c[i]==1);
		g.create(t,i,0);
	}
	n+=2;
	while(m--){
		int u,v;
		cin>>u>>v;
		g.create(u,v,1);
		g.create(v,u,1);
	}
	cout<<Dinic()<<'\n';
	
	cout.flush();
	 
	/*fclose(stdin);
	fclose(stdout);*/
	return 0;
}

最大权闭合子图

即给定一张有向图,每个点 \(i\) 都有一个权值 \(a_i\),你需要选择一个权值和最大的子图,使得子图中每个点在原图中的后继(有出边的点)都在子图中。

建立源点 \(s\) 和汇点 \(t\)。若 \(a_i>0\),连容量为 \(a_i\) 的边 \(s\rightarrow i\);若 \(a_i<0\),连容量为 \(-a_i\) 的边 \(i\rightarrow t\)

原图上所有边的容量为 \(+\infty\)

最大权值和 \(=\) 正权和 \(-\) 最小割。


对于我们选择的子图,权值和 \(=\) 所有正权值之和 \(-\) 未选择的正权点的权值和 \(+\) 选择的负权点的权值和。

考虑一个割对应一个子图。因为一个割把图分成两部分,有 \(s\) 的那一部分没有边指向另一部分,有 \(t\) 的部分是闭合子图;而且钦定原图中的边容量为 \(+\infty\),求最小割的时候不会割掉原图的边,破坏原图性质。

钦定不选择一个正权点 \(i\) 时,断开 \(s\rightarrow i\);选择一个负权点 \(i\) 时,断开 \(i\rightarrow t\)

断开边的权值和即为割的容量。

那么,这个割对应的子图的权值和 \(=\) 正权和 \(-\) 割的容量,则最大权值和 \(=\) 正权和 \(-\) 最小割。

对于最终方案, \(s\) 能到达的正权点是被选择的,不能到达 \(t\) 的负权点是被选择的。而不能到达 \(t\) 的负权点,就可以从 \(s\) 出发到达。

因此最终选择的点即从 \(s\) 出发的所有点。特别地,用 Dinic 求解时,可以直接判断求 \(G_L\) 之后有没有最后一次广搜得到的层数。


luogu P2762 太空飞行计划问题

\(n\) 个仪器,\(m\) 个实验,每个实验需要一些仪器,仪器可重复使用。

配置仪器 \(i\) 的成本为 \(c_i\),实验 \(j\) 的收益为 \(p_j\)

求净收益的最大值。

\(1\leq n,m\leq50,1\leq c,p<2^{31}\)

实验对应 \(1\sim m\),仪器对应 \(m+1\sim m+n\)

一个实验向其需要的仪器连有向边,表示该实验需要这些仪器。

之后就是选择一个闭合子图:仪器没有后继,而实验的后继必须全部选择。

净收益 \(=\) 实验收益 \(-\) 仪器成本。

\(p_j\) 为正权点,\(-c_i\) 为负权点,建图,即最大权闭合子图问题。钦定源点 \(s=m+n+1\),汇点 \(t=m+n+2\)

按照上述建图求解即可。

参考代码
//#include<bits/stdc++.h>
#include<algorithm>
#include<iostream>
#include<cstring>
#include<iomanip>
#include<cstdio>
#include<string>
#include<vector>
#include<cmath>
#include<ctime>
#include<deque>
#include<queue>
#include<stack>
#include<list>
using namespace std;
typedef long long ll;
constexpr const int N=102,M=N*N+1,inf=0x3f3f3f3f;
constexpr const ll lnf=0x3f3f3f3f3f3f3f3f;
struct graph{
	struct edge{
		int v,r;
		ll w;
	}g[M<<1|1];
	
	int h[N+1],size=1;
	void create(int u,int v,int w){
		g[++size]={v,h[u],w};
		h[u]=size;
	}
	
	edge& operator [](int x){
		return g[x];
	}
}g;
int n,s,t,dis[N+1];
int cur[N+1];
bool bfs(){
	memset(dis,0x3f,sizeof(dis));
	for(int i=1;i<=n;i++){
		cur[i]=g.h[i];
	}
	queue<int>q;
	dis[s]=0;
	q.push(s);
	while(q.size()){
		int x=q.front();q.pop();
		for(int i=g.h[x];i;i=g[i].r){
			auto [v,r,w]=g[i];
			if(w>0&&dis[v]==inf){
				dis[v]=dis[x]+1;
				q.push(v);
				if(v==t){
					return true;
				}
			}
		}
	}
	return false;
}
ll dfs(int x,ll Min){
	if(x==t){
		return Min;
	}
	ll ans=0;
	for(int i=cur[x];i&&Min;i=g[i].r){
		cur[x]=i;
		auto [v,r,w]=g[i];
		if(w&&dis[v]==dis[x]+1){
			ll pl=dfs(v,min(Min,w));
			if(!pl){
				dis[v]=inf;
			}
			g[i].w-=pl;
			g[i^1].w+=pl;
			ans+=pl;
			Min-=pl;
		}
	}
	return ans;
}
ll Dinic(){
	ll ans=0;
	while(bfs()){
		ans+=dfs(s,lnf);
	}
	return ans;
}
int main(){
	/*freopen("test.in","r",stdin);
	freopen("test.out","w",stdout);*/
	
	ios::sync_with_stdio(false);
	cin.tie(0);cout.tie(0);
	
	int n,m;
	cin>>m>>n;
	s=n+m+1,t=n+m+2;
	::n=t;
	ll ans=0;
	for(int i=1;i<=m;i++){
		int p;
		cin>>p;
		g.create(s,i,p);
		g.create(i,s,0); 
		ans+=p;
		string str;
		getline(cin,str);
		stringstream ss(str);
		int x;
		while(ss>>x){
			g.create(i,x+m,inf);
			g.create(x+m,i,0); 
		}
	}
	for(int i=1;i<=n;i++){
		int c;
		cin>>c;
		g.create(m+i,t,c);
		g.create(t,m+i,0);
	}
	ans-=Dinic();
	for(int i=1;i<=m;i++){
		if(dis[i]!=inf){
			cout<<i<<' ';
		}
	}
	cout<<'\n';
	for(int i=1;i<=n;i++){
		if(dis[i+m]!=inf){
			cout<<i<<' ';
		}
	}
	cout<<'\n'<<ans<<'\n';
	
	cout.flush();
	 
	/*fclose(stdin);
	fclose(stdout);*/
	return 0;
}

切糕模型

luogu P3227 [HNOI2013] 切糕

给定代价函数 \(v(x,y,z),x\in[1,P],y\in[1,Q],z\in[1,R]\)

构造函数 \(f(x,y),x\in[1,P],y\in[1,Q]\),满足 \(f(x,y)\in[1,R]\),且 \(\forall1\leq x,x'\leq P,1\leq y,y'\leq Q\),满足 \(\vert x-x'\vert+\vert y-y'\vert=1\),有 \(\vert f(x,y)-f(x',y')\vert\leq D\)

\(\displaystyle\sum_{x=1}^P\sum_{y=1}^Qv(x,y,f(x,y))\) 最小值。

\(1\leq P,Q,R\leq40\)\(0\leq D\leq R\)

考虑对于确定的 \(f(x_0,y_0)\),其需要从 \(1,2,3,\cdots,R\) 中选择一个值,等价于从长度为 \(z\) 的链上选择至少一条边删除。

具体而言,定义节点 \(V_{x_0,y_0,z}\)\(z=0,1,\cdots,R\)。钦定 \(s=V_{x_0,y_0,0}\) 为源点,\(t=V_{x_0,y_0,R}\) 为汇点。特别地,\(\forall x,y\)\(s=V_{x,y,0}\)同一个点\(t=V_{x,y,R}\) 也是同一个点。

对于 \(i=0,1,\cdots,R-1\),构造一条 \(V_{x_0,y_0,i}\rightarrow V_{x_0,y_0,i+1}\) 的容量为 \(v(x_0,y_0,i+1)\) 的边。

此时选择 \(f(x_0,y_0)\),等价于割掉 \(V_{x_0,y_0,f(x_0,y_0)-1}\rightarrow V_{x_0,y_0,f(x_0,y_0)}\) 的边,割的容量增大 \(v(x_0,y_0,f(x_0,y_0))\)

最终得到的就是一个图的割。

如果不考虑限制 \(\vert f(x,y)-f(x',y')\vert\leq D\),只需要求最小割即可。

现在考虑限制 \(\vert f(x,y)-f(x',y')\vert\leq D\),根据对称性可以拆成 \(f(x,y)-f(x',y')\leq D\)

\(f(x,y)\leq f(x',y')+D\),令 \(z\leq f(x,y)\),则有 \(f(x',y')\geq z-D\)。因此当 \(f(x,y)\geq z\)\(f(x',y')<z-D\) 时,选择不成立。

\(R=5,D=2\) 为例(省略了其他支路):

001

\(z=5\),对于 \(f(x,y)>5\) 的部分,应在 \(f(x',y')<3\) 时不成立。也就是说,\(f(x,y)\geq5\)\(f(x',y')\leq2\) 的割是无效的。

为了让这个割不是最小割,可以有:

002

此时,所有 \(f(x,y)\geq 5\)\(f(x',y')\leq 2\) 的割不再是割。如果割掉红边,需要 \(+\infty\) 的代价,一定不为最小割。这样,求最小割的过程中就自然避免了这种不合法的方案。

003

连出所有红边后,跑最小割即可。

具体而言,我们对于每一个 \(z\),连容量为 \(+\infty\) 的边 \(V_{x,y,z-1}\rightarrow V_{x',y',z-D-1}\),代表 \(f(x,y)\geq z,f(x',y')\leq z-D-1\) 不能同时成立。关于 \(z\) 的范围,因为 \(1\leq z-1,z-D-1\leq R-1\),所以 \(D+2\leq z\leq R\)

参考代码
//#include<bits/stdc++.h>
#include<algorithm>
#include<iostream>
#include<cstring>
#include<iomanip>
#include<cstdio>
#include<string>
#include<vector>
#include<cmath>
#include<ctime>
#include<deque>
#include<queue>
#include<stack>
#include<list>
using namespace std;
typedef long long ll;
constexpr const int P=40,Q=40,R=40,D=40,N=P*Q*R,M=100*N+1,inf=0x3f3f3f3f;
constexpr const int f[4][2]={{-1,0},{0,-1},{1,0},{0,1}};
constexpr const ll lnf=0x3f3f3f3f3f3f3f3f;
int p,q,r,d,s,t,v[P+1][Q+1][R+1];
struct graph{
	struct edge{
		int v,r;
		ll w;
	}g[M<<1|1];
	
	int h[N+1],size=1;
	void create(int u,int v,int w){
		g[++size]={v,h[u],w};
		h[u]=size;
	}
	edge& operator [](int x){
		return g[x];
	}
}g;
int V(int x,int y,int z){
	if(z==0){
		return s;
	}else if(z==r){
		return t;
	}else{
		return ((x-1)*q+y-1)*(r-1)+z;
	}
}
int cur[N+1],dis[N+1];
bool bfs(){
	memset(dis,0x3f,sizeof(dis));
	for(int i=0;i<=t;i++){
		cur[i]=g.h[i];
	}
	queue<int>q;
	dis[s]=0;
	q.push(s);
	while(q.size()){
		int x=q.front();q.pop();
		for(int i=g.h[x];i;i=g[i].r){
			auto [v,r,w]=g[i]; 
			if(w&&dis[v]==inf){
				dis[v]=dis[x]+1;
				q.push(v);
				if(v==t){
					return true;
				}
			}
		}
	}
	return false;
}
ll dfs(int x,ll Min){
	if(x==t){
		return Min;
	}
	ll ans=0;
	for(int i=cur[x];i&&Min;i=g[i].r){
		cur[x]=i;
		auto [v,r,w]=g[i];
		if(w&&dis[v]==dis[x]+1){
			ll pl=dfs(v,min(w,Min));
			if(!pl){
				dis[v]=inf;
			}
			g[i].w-=pl;
			g[i^1].w+=pl;
			ans+=pl;
			Min-=pl;
		}
	}
	return ans;
}
ll Dinic(){
	ll ans=0;
	while(bfs()){
		ans+=dfs(s,lnf);
	}
	return ans;
}
int main(){
	/*freopen("test.in","r",stdin);
	freopen("test.out","w",stdout);*/
	
	ios::sync_with_stdio(false);
	cin.tie(0);cout.tie(0);
	
	cin>>p>>q>>r>>d;
	for(int z=1;z<=r;z++){
		for(int i=1;i<=p;i++){
			for(int j=1;j<=q;j++){
				cin>>v[i][j][z];
			}
		}
	}
	s=0;
	t=p*q*(r-1)+1;
	for(int x=1;x<=p;x++){
		for(int y=1;y<=q;y++){
			for(int i=0;i<r;i++){
				g.create(V(x,y,i),V(x,y,i+1),v[x][y][i+1]);
				g.create(V(x,y,i+1),V(x,y,i),0);
			} 
		}
	}
	for(int x=1;x<=p;x++){
		for(int y=1;y<=q;y++){
			for(int i=0;i<4;i++){
				int xx=x+f[i][0],yy=y+f[i][1];
				if(1<=xx&&xx<=p&&1<=yy&&yy<=q){
					for(int z=d+2;z<=r;z++){
						g.create(V(x,y,z-1),V(xx,yy,z-d-1),inf);
						g.create(V(xx,yy,z-d-1),V(x,y,z-1),0);
					}
				}
			}
		}
	}
	cout<<Dinic()<<'\n';
	
	cout.flush();
	 
	/*fclose(stdin);
	fclose(stdout);*/
	return 0;
}
/*
2 2 2
0
5 1
5 1
2 5
2 5
*/

有很多网络流问题都可以化归成这题的模型,即构造函数 \(f:S\rightarrow[1,R]\),例如比较经典的 \(0,1\) 二元选择问题。

由于「切糕」这个描述十分形象,所以这一类问题就被称为切糕模型。

即将对于函数 \(f\) 的值的选择变成对于链上的割的选择,将额外的代价/限制变成点之间的连边。 如果存在收益,那么将其提前加入答案中,然后变成不满足条件的代价。

网络流与线性规划 24 题

24 年 NOIP 集训时,某个人推荐给我的题单。虽然我现在才开始学网络流。

网络流与线性规划 24 题其实是一个中文互联网上古早的网络流题单。

餐巾计划问题

记原题面中的 \(N,p,m,f,n,s\) 分别为 \(n,p,a,\textit{ca},b,\textit{cb}\)

这还是非常早期的一次模拟赛的第四题,应该是我第一次遇到网络流问题。

首先考虑如果第 \(i\) 天的可用餐巾的数量大于 \(r_i\),一定不优。因为延期送洗没有额外花费,而买了多的新的餐巾也可以之后再买。因此钦定第 \(i\) 天的餐巾为 \(r_i\) 条。

考虑把一天拆成早晚,因为早上会得到可用餐巾,晚上会得到脏餐巾并处理。

定义节点 \(V_{i,0/1}\) 分别表示第 \(i\) 天的早上、晚上对应的节点,\(s\) 为源点,\(t\) 为汇点。

考虑把「餐巾的操作」(购买、清洗、使用)视为「流」,在网络里流动起来,然后求最小费用最大流。

因而可以得到以下操作建模:

  • 送到快洗部:连容量为 \(+\infty\)、费用为 \(\textit{ca}\) 的边 \(V_{i,1}\rightarrow V_{i+a,0}\)
  • 送到慢洗部:连容量为 \(+\infty\)、费用为 \(\textit{cb}\) 的边 \(V_{i,1}\rightarrow V_{i+b,0}\)
  • 延期送洗:连容量为 \(+\infty\)、费用为 \(0\) 的边 \(V_{i,1}\rightarrow V_{i+1,1}\)

我们还需要的操作就是购买餐巾,和限制每天早晚用到的餐巾为 \(r_i\) 条。

<<1145141919810>>

限制 \(V_{i,1}\) 流出的餐巾为 \(r_i\) 条。

[CTSC1999] 家园 / 星际转移问题

飞行员配对方案问题

给定一个二分图,左部 \(1\sim m\) 编号,右部 \(m+1\sim n\) 编号,和左部至右部的若干条有向边,求其最大匹配并输出方案。

其实是二分图最大匹配问题。建源点、汇点,跑 Dinic 即可。时间复杂度 \(\mathcal O\left(m\sqrt n\right)\)

输出方案就判断都是 \(1\sim n\) 的点,且流量为 \(1\)(剩余容量为 \(0\))。

参考代码
//#include<bits/stdc++.h>
#include<algorithm>
#include<iostream>
#include<cstring>
#include<iomanip>
#include<cstdio>
#include<string>
#include<vector>
#include<cmath>
#include<ctime>
#include<deque>
#include<queue>
#include<stack>
#include<list>
using namespace std;
constexpr const int N=100,M=(N+2)*(N+2);
constexpr const int inf=0x3f3f3f3f;
struct graph{
	struct edge{
		int v,r,w;
	}g[M<<1|1];
	int h[N+1],size=1;
	void create(int u,int v,int w){
		g[++size]={v,h[u],w};
		h[u]=size;
	}
	edge& operator [](int x){
		return g[x];
	}
}g;
int m,n,s,t,dis[N+1],cur[N+1];
bool bfs(){
	memset(dis,0x3f,sizeof(dis));
	for(int i=1;i<=n+2;i++){
		cur[i]=g.h[i];
	}
	queue<int>q;
	q.push(s);
	dis[s]=0;
	while(q.size()){
		int x=q.front();q.pop();
		for(int i=g.h[x];i;i=g[i].r){
			auto [v,r,w]=g[i];
			if(w>0&&dis[v]==inf){
				dis[v]=dis[x]+1;
				q.push(v);
				if(v==t){
					return true;
				}
			}
		}
	}
	return false;
}
int dfs(int x,int Min){
	if(x==t){
		return Min;
	}
	int ans=0;
	for(int i=cur[x];i&&Min;i=g[i].r){
		cur[x]=i;
		auto [v,r,w]=g[i];
		if(w&&dis[v]==dis[x]+1){
			int pl=dfs(v,min(Min,w));
			if(!pl){
				dis[v]=inf;
			}
			g[i].w-=pl;
			g[i^1].w+=pl;
			ans+=pl;
			Min-=pl;
		}
	}
	return ans;
} 
int Dinic(){
	int ans=0;
	while(bfs()){
		ans+=dfs(s,inf);
	}
	return ans;
}
int main(){
	/*freopen("test.in","r",stdin);
	freopen("test.out","w",stdout);*/
	
	ios::sync_with_stdio(false);
	cin.tie(0);cout.tie(0);
	
	cin>>m>>n;
	s=n+1,t=n+2;
	while(true){
		int u,v;
		cin>>u>>v;
		if(u==-1){
			break;
		}
		g.create(u,v,1);
		g.create(v,u,0);
	}
	for(int i=1;i<=m;i++){
		g.create(s,i,1);
		g.create(i,s,0);
	}
	for(int i=m+1;i<=n;i++){
		g.create(i,t,1);
		g.create(t,i,0);
	}
	cout<<Dinic()<<'\n';
	for(int u=1;u<=m;u++){
		for(int i=g.h[u];i;i=g[i].r){
			auto [v,r,w]=g[i];
			if(v!=s&&v!=t&&!w){
				cout<<u<<' '<<v<<'\n';
			}
		}
	}
	
	cout.flush();
	 
	/*fclose(stdin);
	fclose(stdout);*/
	return 0;
}

软件补丁问题

太空飞行计划问题

实验对应 \(1\sim m\),仪器对应 \(m+1\sim m+n\)

一个实验向其需要的仪器连有向边,表示该实验需要这些仪器。

之后就是选择一个闭合子图:仪器没有后继,而实验的后继必须全部选择。

净收益 \(=\) 实验收益 \(-\) 仪器成本。

\(p_j\) 为正权点,\(-c_i\) 为负权点,建图,即最大权闭合子图问题。钦定源点 \(s=m+n+1\),汇点 \(t=m+n+2\)

按照上述建图求解即可。

参考代码
//#include<bits/stdc++.h>
#include<algorithm>
#include<iostream>
#include<cstring>
#include<iomanip>
#include<cstdio>
#include<string>
#include<vector>
#include<cmath>
#include<ctime>
#include<deque>
#include<queue>
#include<stack>
#include<list>
using namespace std;
typedef long long ll;
constexpr const int N=102,M=N*N+1,inf=0x3f3f3f3f;
constexpr const ll lnf=0x3f3f3f3f3f3f3f3f;
struct graph{
	struct edge{
		int v,r;
		ll w;
	}g[M<<1|1];
	
	int h[N+1],size=1;
	void create(int u,int v,int w){
		g[++size]={v,h[u],w};
		h[u]=size;
	}
	
	edge& operator [](int x){
		return g[x];
	}
}g;
int n,s,t,dis[N+1];
int cur[N+1];
bool bfs(){
	memset(dis,0x3f,sizeof(dis));
	for(int i=1;i<=n;i++){
		cur[i]=g.h[i];
	}
	queue<int>q;
	dis[s]=0;
	q.push(s);
	while(q.size()){
		int x=q.front();q.pop();
		for(int i=g.h[x];i;i=g[i].r){
			auto [v,r,w]=g[i];
			if(w>0&&dis[v]==inf){
				dis[v]=dis[x]+1;
				q.push(v);
				if(v==t){
					return true;
				}
			}
		}
	}
	return false;
}
ll dfs(int x,ll Min){
	if(x==t){
		return Min;
	}
	ll ans=0;
	for(int i=cur[x];i&&Min;i=g[i].r){
		cur[x]=i;
		auto [v,r,w]=g[i];
		if(w&&dis[v]==dis[x]+1){
			ll pl=dfs(v,min(Min,w));
			if(!pl){
				dis[v]=inf;
			}
			g[i].w-=pl;
			g[i^1].w+=pl;
			ans+=pl;
			Min-=pl;
		}
	}
	return ans;
}
ll Dinic(){
	ll ans=0;
	while(bfs()){
		ans+=dfs(s,lnf);
	}
	return ans;
}
int main(){
	/*freopen("test.in","r",stdin);
	freopen("test.out","w",stdout);*/
	
	ios::sync_with_stdio(false);
	cin.tie(0);cout.tie(0);
	
	int n,m;
	cin>>m>>n;
	s=n+m+1,t=n+m+2;
	::n=t;
	ll ans=0;
	for(int i=1;i<=m;i++){
		int p;
		cin>>p;
		g.create(s,i,p);
		g.create(i,s,0); 
		ans+=p;
		string str;
		getline(cin,str);
		stringstream ss(str);
		int x;
		while(ss>>x){
			g.create(i,x+m,inf);
			g.create(x+m,i,0); 
		}
	}
	for(int i=1;i<=n;i++){
		int c;
		cin>>c;
		g.create(m+i,t,c);
		g.create(t,m+i,0);
	}
	ans-=Dinic();
	for(int i=1;i<=m;i++){
		if(dis[i]!=inf){
			cout<<i<<' ';
		}
	}
	cout<<'\n';
	for(int i=1;i<=n;i++){
		if(dis[i+m]!=inf){
			cout<<i<<' ';
		}
	}
	cout<<'\n'<<ans<<'\n';
	
	cout.flush();
	 
	/*fclose(stdin);
	fclose(stdout);*/
	return 0;
}

试题库问题

最小路径覆盖问题

魔术球问题

最长不下降子序列问题

航空路线问题

方格取数问题

机器人路径规划问题(疑似错题)

圆桌问题

骑士共存问题

火星探险问题

最长k可重线段集问题

最长 k 可重区间集问题

孤岛营救问题

深海机器人问题

数字梯形问题

分配问题

运输问题

负载平衡问题

备忘录

在最大流的代码中,w 表示剩余流量,对应 \(c_f\)

在费用流的代码中,w 表示剩余流量,c 表示单位费用,与 \(w,c_f\) 相反。

posted @ 2026-06-28 12:49  TH911  阅读(3)  评论(0)    收藏  举报