ybtAu「图论」第4章 费用流

A. 【例题1】订货问题

由源点向每个月份连边,容量 \(+\infin\),费用 \(d_i\);由每个月份向汇点连边,容量 \(U_i\),费用 \(0\);由每个月份向下个月连边,容量 \(+\infin\),费用 \(m\),跑最小费用最大流即可。

#include <iostream>
#include <cstring>
#define int long long
#define N 100005
int hed[N],tal[N],flw[N],wt[N],nxt[N],cnte,n,m,S,T,s;
void adde(int u,int v,int w,int c) {tal[++cnte]=v,flw[cnte]=w,wt[cnte]=c,nxt[cnte]=hed[u],hed[u]=cnte;}
void de(int u,int v,int w,int c) {adde(u,v,w,c),adde(v,u,0,-c);}
namespace MF
{
	int cur[N],dep[N],q[N],vis[N],ans;
	bool spfa()
	{
		memset(dep,0x3f,sizeof dep),memset(vis,0,sizeof vis);
		int inf=dep[0];
		dep[q[1]=S]=0;
		int hd=1,tl=1;
		while(hd<=tl)
		{
			int u=q[hd++];
			vis[u]=0;
			for(int i=hed[u];i;i=nxt[i]) if(flw[i]&&dep[tal[i]]>dep[u]+wt[i])
			{
				dep[tal[i]]=dep[u]+wt[i];
				if(!vis[tal[i]]) vis[tal[i]]=1,q[++tl]=tal[i];
			}
		}
		return dep[T]<inf;
	}
	int dfs(int x,int fl)
	{
		vis[x]=1;
		if(x==T||!fl) return ans+=dep[T]*fl,fl;
		int ret=0;
		for(int &i=cur[x];i;i=nxt[i]) if(flw[i]&&!vis[tal[i]]&&dep[tal[i]]==dep[x]+wt[i])
		{
			int d=dfs(tal[i],std::min(fl-ret,flw[i]));
			ret+=d,flw[i]-=d,flw[i^1]+=d;
			if(ret==fl) break;
		}
		if(ret==fl) vis[x]=0;
		return ret;
	}
	int dinic()
	{
		ans=0;
		while(spfa()) memcpy(cur,hed,sizeof cur),dfs(S,1e16);
		return ans;
	}
};
signed main()
{
	std::ios::sync_with_stdio(0);
	std::cin.tie(0),std::cout.tie(0);
	std::cin>>n>>m>>s;
	S=n+1,T=n+2,cnte=1;
	for(int i=1,x;i<=n;i++) std::cin>>x,de(i,T,x,0);
	for(int i=1,x;i<=n;i++) std::cin>>x,de(S,i,1e16,x);
	for(int i=1;i<n;i++) de(i,i+1,s,m);
	std::cout<<MF::dinic();
}

B. 【例题2】工作安排

由源点向每类产品连边,容量 \(C_i\),费用 \(0\);由每类产品向能制造它的的员工连边,容量 \(+\infin\),费用 \(0\);由每个员工向汇点连 \(S_i+1\) 条边,容量 \(T_{i,j}-T_{i,j-1}\),费用 \(W_{i,j}\),跑费用流即可。
由于 \(W_{i,j}<W_{i,j+1}\),当一个员工制造的商品个数较少时,对应制造了更多商品的那些边上不会有流量。

#include <iostream>
#include <cstring>
#include <queue>
#define N 2000005
#define M 505
#define int long long
int hed[M],tal[N],flw[N],wt[N],nxt[N],cnte,n,m,S,T;
bool a[M][M];
int t[M];
void adde(int u,int v,int w,int c) {tal[++cnte]=v,flw[cnte]=w,wt[cnte]=c,nxt[cnte]=hed[u],hed[u]=cnte;}
void de(int u,int v,int w,int c) {adde(u,v,w,c),adde(v,u,0,-c);}
namespace MF
{
	int cur[M],dep[M],vis[M],ans;
	std::queue<int> q;
	bool spfa()
	{
		for(int i=1;i<=n+m+2;i++) dep[i]=1e18,vis[i]=0;
		int inf=1e18;
		dep[S]=0;
		q.push(S);
		while(!q.empty())
		{
			int u=q.front();
			vis[u]=0,q.pop();
			for(int i=hed[u];i;i=nxt[i]) if(flw[i]&&dep[tal[i]]>dep[u]+wt[i])
			{
				dep[tal[i]]=dep[u]+wt[i];
				if(!vis[tal[i]]) vis[tal[i]]=1,q.push(tal[i]);
			}
		}
		return dep[T]<1e16;
	}
	int dfs(int x,int fl)
	{
		if(x==T||!fl) return ans+=dep[T]*fl,fl;
		vis[x]=1;
		int ret=0;
		for(int &i=cur[x];i;i=nxt[i]) if(flw[i]&&!vis[tal[i]]&&dep[tal[i]]==dep[x]+wt[i])
		{
			int d=dfs(tal[i],std::min(fl-ret,flw[i]));
			ret+=d,flw[i]-=d,flw[i^1]+=d;
			if(ret==fl) break;
		}
		if(ret==fl) vis[x]=0;
		return ret;
	}
	int dinic()
	{
		ans=0;
		while(spfa())
		{
			for(int i=1;i<=n+m+2;i++) cur[i]=hed[i];
			dfs(S,1e18);
		}
		return ans;
	}
};
signed main()
{
	std::ios::sync_with_stdio(0);
	std::cin.tie(0),std::cout.tie(0);
	std::cin>>m>>n,cnte=1,S=n+m+1,T=n+m+2;
	int x;
	for(int i=1;i<=n;i++) std::cin>>x,de(S,i,x,0);
	for(int i=1;i<=m;i++) for(int j=1;j<=n;j++)
	{
		std::cin>>x;
		if(x) de(j,i+n,1e18,0);
	}
	for(int i=1;i<=m;i++)
	{
		int len;
		std::cin>>len;
		t[0]=0;
		for(int j=1;j<=len;j++) std::cin>>t[j];
		t[len+1]=1e18;
		for(int j=1,y;j<=len+1;j++) std::cin>>y,de(i+n,T,t[j]-t[j-1],y);
	}
	std::cout<<MF::dinic();
}

C. 软件开发

直接做是不好做的,所以考虑拆点。
具体地,把每天拆成“白天”和“晚上”,“白天”产生未消毒的毛巾,“晚上”处理未消毒的毛巾。
由于无论怎样,处理和购买的毛巾的总和不变,产生未消毒的毛巾的总量也不变。
知道了这一点,可以令源点源源不断地产生未消毒的毛巾,汇点接受未消毒的毛巾。
由源点向每个晚上连边,容量 \(n_i\),费用 \(0\),表示这一天产生了 \(n_i\) 条未消毒的毛巾;
由每个白天向汇点连边,容量 \(n_i\),费用 \(0\),含义同上;
由第 \(i\) 个晚上向第 \(i+a+1\) 个白天连边,容量 \(+\infin\),费用 \(f_a\),表示第 \(i\) 个晚上处理的毛巾第 \(i+a+1\) 个白天能用,\(b\) 类消毒同理;
由每个白天向第二天白天连边,容量 \(+\infin\),费用 \(0\),表示这天收到的毛巾可以留到以后再用;
由每个晚上向第二天晚上连边,容量 \(+\infin\),费用 \(0\),表示这天收到的未消毒的毛巾可以留到以后再消毒;
由源点向每个白天连边,容量 \(+\infin\),费用 \(f\),表示可以购买毛巾。
连的边看起来有点多。

#include <iostream>
#include <cstring>
#define int long long
#define N 100005
int hed[N],tal[N],flw[N],wt[N],nxt[N],cnte,n,a,b,f,fa,fb,S,T;
void adde(int u,int v,int w,int c) {tal[++cnte]=v,flw[cnte]=w,wt[cnte]=c,nxt[cnte]=hed[u],hed[u]=cnte;}
void de(int u,int v,int w,int c) {adde(u,v,w,c),adde(v,u,0,-c);}
namespace MF
{
	int cur[N],dep[N],q[N],vis[N],ans;
	bool spfa()
	{
		memset(dep,0x3f,sizeof dep),memset(vis,0,sizeof vis);
		int inf=dep[0];
		dep[q[1]=S]=0;
		int hd=1,tl=1;
		while(hd<=tl)
		{
			int u=q[hd++];
			vis[u]=0;
			for(int i=hed[u];i;i=nxt[i]) if(flw[i]&&dep[tal[i]]>dep[u]+wt[i])
			{
				dep[tal[i]]=dep[u]+wt[i];
				if(!vis[tal[i]]) vis[tal[i]]=1,q[++tl]=tal[i];
			}
		}
		return dep[T]<inf;
	}
	int dfs(int x,int fl)
	{
		if(x==T||!fl) return ans+=dep[T]*fl,fl;
		vis[x]=1;
		int ret=0;
		for(int &i=cur[x];i;i=nxt[i]) if(flw[i]&&!vis[tal[i]]&&dep[tal[i]]==dep[x]+wt[i])
		{
			int d=dfs(tal[i],std::min(fl-ret,flw[i]));
			ret+=d,flw[i]-=d,flw[i^1]+=d;
			if(ret==fl) break;
		}
		if(ret==fl) vis[x]=0;
		return ret;
	}
	int dinic()
	{
		ans=0;
		while(spfa()) memcpy(cur,hed,sizeof cur),dfs(S,1e16);
		return ans;
	}
};
signed main()
{
	std::ios::sync_with_stdio(0);
	std::cin.tie(0),std::cout.tie(0),cnte=1;
	std::cin>>n>>a>>b>>f>>fa>>fb;
	S=n*2+1,T=n*2+2;
	for(int i=1,x;i<=n;i++)
	{
		std::cin>>x,de(S,i+n,x,0),de(i,T,x,0),de(S,i,1e16,f);
		if(i+a+1<=n) de(i+n,i+a+1,1e16,fa);
		if(i+b+1<=n) de(i+n,i+b+1,1e16,fb);
		if(i+1<=n) de(i,i+1,1e16,0),de(i+n,i+n+1,1e16,0);
	}
	std::cout<<MF::dinic();
}

D. 放置棋子

一种非常巧妙的建图方法,巧妙到 neatisaac 已经不记得了。
考虑把棋盘放满然后拿走棋子。
由于第二条限制难以直接处理,所以直接枚举每一行最多放的棋子个数,求出一种方案之后再判断是否合法即可。
设当前枚举到每一行最多放 \(x\) 个棋子。
由源点向每一行连边,容量为该行的空格数,费用 \(0\),表示这一行的棋子个数;
由每一列向汇点连边,容量为该列的空格数,费用 \(0\),表示这一列的棋子个数;
由每行向每列连边,容量 \(x\),费用 \(0\),表示该行列最多放下的棋子个数;
对每个空格,由所在行向所在列连边,容量 \(1\),费用 \(1\),表示拿走这枚棋子。
于是,我们建立了这样一个网络:流量表示处理的棋子个数(决定一个棋子是拿走还是放下),费用表示拿走的棋子个数,跑最小费用最大流,如果不能满流,那么一定是不合法的,因为有棋子没有被处理到。经过同行列连边的流量表示在这放下棋子,经过有费用的边的流量表示拿走棋子,得到的最小费用就是拿走的棋子个数。
最后判断放下的棋子个数是否合法并更新最大值即可。

#include <iostream>
#include <cstring>
#define int long long
#define N 100005
int hed[N],tal[N],flw[N],wt[N],nxt[N],cnte,n,S,T,A,B,cc,cs,an;
std::string mp[105];
void adde(int u,int v,int w,int c) {tal[++cnte]=v,flw[cnte]=w,wt[cnte]=c,nxt[cnte]=hed[u],hed[u]=cnte;}
void de(int u,int v,int w,int c) {adde(u,v,w,c),adde(v,u,0,-c);}
namespace MF
{
	int cur[N],dep[N],q[N],vis[N],ans,sna;
	bool spfa()
	{
		memset(dep,0x3f,sizeof dep),memset(vis,0,sizeof vis);
		int inf=dep[0];
		dep[q[1]=S]=0;
		int hd=1,tl=1;
		while(hd<=tl)
		{
			int u=q[hd++];
			vis[u]=0;
			for(int i=hed[u];i;i=nxt[i]) if(flw[i]&&dep[tal[i]]>dep[u]+wt[i])
			{
				dep[tal[i]]=dep[u]+wt[i];
				if(!vis[tal[i]]) vis[tal[i]]=1,q[++tl]=tal[i];
			}
		}
		return dep[T]<inf;
	}
	int dfs(int x,int fl)
	{
		if(x==T||!fl) return ans+=dep[T]*fl,fl;
		vis[x]=1;
		int ret=0;
		for(int &i=cur[x];i;i=nxt[i]) if(flw[i]&&!vis[tal[i]]&&dep[tal[i]]==dep[x]+wt[i])
		{
			int d=dfs(tal[i],std::min(fl-ret,flw[i]));
			ret+=d,flw[i]-=d,flw[i^1]+=d;
			if(ret==fl) break;
		}
		if(ret==fl) vis[x]=0;
		return ret;
	}
	int dinic()
	{
		ans=sna=0;
		while(spfa()) memcpy(cur,hed,sizeof cur),sna+=dfs(S,1e16);
		return ans;
	}
};
void check(int x)
{
	memset(hed,0,sizeof hed),memset(nxt,0,sizeof nxt);
	cnte=1;
	for(int i=1;i<=n;i++)
	{
		de(i,i+n,x,0);
		int tc=0;
		for(int j=1;j<=n;j++) if(mp[i][j-1]!='/') tc++;
		de(S,i,tc,0);
		tc=0;
		for(int j=1;j<=n;j++) if(mp[j][i-1]!='/') tc++;
		de(i+n,T,tc,0);
	}
	for(int i=1;i<=n;i++) for(int j=1;j<=n;j++) if(mp[i][j-1]=='.') de(i,j+n,1,1);
	int d=MF::dinic();
	if(MF::sna!=cs) return;
	if(x*B<=(cs-d)*A) an=std::max(an,cs-d-cc);
}
signed main()
{
	std::ios::sync_with_stdio(0);
	std::cin.tie(0),std::cout.tie(0);
	for(int case1=1;;case1++)
	{
		std::cin>>n>>A>>B;
		if(!n) break;
		cc=0,cs=0,S=2*n+1,T=2*n+2;
		for(int i=1;i<=n;i++)
		{
			std::cin>>mp[i];
			for(int j=1;j<=n;j++)
			{
				if(mp[i][j-1]=='C') cc++;
				if(mp[i][j-1]!='/') cs++;
			}
		}
		an=-1;
		for(int i=0;i<=n;i++) check(i);
		std::cout<<"Case "<<case1<<": ";
		if(an==-1) std::cout<<"impossible\n";
		else std::cout<<an<<'\n';
	}
}

E. 订单处理

考虑一个处理过的订单对该机器的影响。
简单推导可知,令第 \(j\) 台机器处理 \(S\) 个任务,则第 \(i\) 个订单在第 \(j\) 台机器上第 \(k\) 个处理会使该机器的总时间增加 \((S-k+1)Z_{i,j}\)
因此,由源点向每个订单连边,容量 \(1\),费用 \(0\);把每台机器拆成 \(n\) 个点,每个点对应第 \(S-k+1\) 个处理的订单,向汇点连边,容量 \(1\),费用 \(0\);由每个订单向每台机器的 \(n\) 个点连边,容量 \(1\),费用 \(kZ_{i,j}\),跑费用流即可。

#include <iostream>
#include <cstring>
#define int long long
#define N 5005
#define M 2000005
int hed[N],tal[M],flw[M],wt[M],nxt[M],cnte,n,m,S,T,a[105][105],id[105][105];
void adde(int u,int v,int w,int c) {tal[++cnte]=v,flw[cnte]=w,wt[cnte]=c,nxt[cnte]=hed[u],hed[u]=cnte;}
void de(int u,int v,int w,int c) {adde(u,v,w,c),adde(v,u,0,-c);}
namespace MF
{
	int cur[N],dep[N],q[M],ans,vis[N];
	bool spfa()
	{
		memset(dep,0x3f,sizeof dep),memset(vis,0,sizeof vis);
		int inf=dep[0];
		dep[q[1]=S]=0;
		int hd=1,tl=1;
		while(hd<=tl)
		{
			int u=q[hd++];
			vis[u]=0;
			for(int i=hed[u];i;i=nxt[i]) if(flw[i]&&dep[tal[i]]>dep[u]+wt[i])
			{
				dep[tal[i]]=dep[u]+wt[i];
				if(!vis[tal[i]]) vis[tal[i]]=1,q[++tl]=tal[i];
			}
		}
		return dep[T]<inf;
	}
	int dfs(int x,int fl)
	{
		if(x==T||!fl) return ans+=dep[T]*fl,fl;
		vis[x]=1;
		int ret=0;
		for(int &i=cur[x];i;i=nxt[i]) if(!vis[tal[i]]&&flw[i]&&dep[tal[i]]==dep[x]+wt[i])
		{
			int d=dfs(tal[i],std::min(fl-ret,flw[i]));
			ret+=d,flw[i]-=d,flw[i^1]+=d;
			if(ret==fl) break;
		}
		if(ret==fl) vis[x]=0;
		return ret;
	}
	int dinic()
	{
		ans=0;
		while(spfa()) memcpy(cur,hed,sizeof cur),dfs(S,1e16);
		return ans;
	}
};
signed main()
{
	std::ios::sync_with_stdio(0);
	std::cin.tie(0),std::cout.tie(0);
	int Tt;
	std::cin>>Tt;
	while(Tt--)
	{
		cnte=1,memset(hed,0,sizeof hed),memset(nxt,0,sizeof nxt);
		std::cin>>n>>m;
		int cn=n+2;
		S=n+1,T=n+2;
		for(int i=1;i<=n;i++) de(S,i,1,0);
		for(int i=1;i<=n;i++) for(int j=1;j<=m;j++) std::cin>>a[i][j],de(id[j][i]=++cn,T,1,0);
		for(int i=1;i<=n;i++) for(int j=1;j<=m;j++)
			for(int k=1;k<=n;k++) de(i,id[j][k],1,k*a[i][j]);
		printf("%.6lf\n",MF::dinic()*1.0/n);
	}
}

F. 数字配对

\(a_i=\prod p^k\),发现两个数 \(x\)\(y\) 能配对,当且仅当 \(\sum k_i=\sum k_j+1\)\(y\mid x\) 或相反。于是可以根据 \(\sum k\) 的奇偶性把图变成二分图。
由源点向左部点连边,容量 \(b_i\),费用 \(0\);由右部点向汇点连边,容量 \(b_i\),费用 \(0\);由左部点向能配对的右部点连边,容量 \(+\infin\),费用 \(c_ic_j\)。最费用最大流即答案。
注意是最大费用,所以增广的方式有所变化,细节见代码。

#include <iostream>
#include <cstring>
#define int long long
#define N 5005
#define M 2000005
int hed[N],tal[M],flw[M],wt[M],nxt[M],cnte,n,S,T,a[N],b[N],c[N],f[N],ce[M];
void adde(int u,int v,int w,int c) {tal[++cnte]=v,ce[cnte]=u,flw[cnte]=w,wt[cnte]=c,nxt[cnte]=hed[u],hed[u]=cnte;}
void de(int u,int v,int w,int c) {adde(u,v,w,c),adde(v,u,0,-c);}
namespace MF
{
	int cur[N],dep[N],q[M],ans,vis[N],in[N],sum;
	bool spfa()
	{
		memset(dep,-0x3f,sizeof dep),memset(vis,0,sizeof vis);
		int inf=dep[0];
		dep[q[1]=S]=0;
		int hd=1,tl=1;
		while(hd<=tl)
		{
			int u=q[hd++];
			vis[u]=0;
			for(int i=hed[u];i;i=nxt[i]) if(flw[i]&&dep[tal[i]]<dep[u]+wt[i])
			{
				dep[tal[i]]=dep[u]+wt[i],in[tal[i]]=i;
				if(!vis[tal[i]]) vis[tal[i]]=1,q[++tl]=tal[i];
			}
		}
		return dep[T]>inf;
	}
	bool dfs()
	{
		int fl=1e16;
		for(int t=in[T];t;t=in[ce[t]]) fl=std::min(fl,flw[t]);
		if(sum+fl*dep[T]>=0)
		{
			sum+=fl*dep[T],ans+=fl;
			for(int t=in[T];t;t=in[ce[t]]) flw[t]-=fl,flw[t^1]+=fl;
			return 1;
		}
		else return ans+=sum/(-dep[T]),0;
	}
	int dinic()
	{
		ans=sum=0;
		while(spfa()&&dfs());
		return ans;
	}
};
signed main()
{
	std::ios::sync_with_stdio(0);
	std::cin.tie(0),std::cout.tie(0);
	std::cin>>n,cnte=1;
	S=n+1,T=n+2;
	for(int i=1,x;i<=n;i++)
	{
		std::cin>>x,a[i]=x;
		for(int j=2;x>1;j++) while(x%j==0) x/=j,f[i]++;
	}
	for(int i=1;i<=n;i++) std::cin>>b[i];
	for(int i=1;i<=n;i++) std::cin>>c[i];
	for(int i=1;i<=n;i++) (f[i]&1)?de(S,i,b[i],0):de(i,T,b[i],0);
	for(int i=1;i<=n;i++) if(f[i]&1) for(int j=1;j<=n;j++)
		if(abs(f[i]-f[j])==1&&(a[i]%a[j]==0||a[j]%a[i]==0)) de(i,j,1e16,c[i]*c[j]);
	std::cout<<MF::dinic();
}

G. 避难方案

posted @ 2025-06-23 16:47  整齐的艾萨克  阅读(7)  评论(0)    收藏  举报