网络流学习笔记:各种各样的建模方式

关于网络流的各种题目笔记

前情提要:网络流的两种算法

EK算法:
#include <bits/stdc++.h>
using namespace std;
const int N=2e2+10;
const int M=1e4+10;
int n,m,s,t;

int head[N],ver[M],Next[M];  		 //链式前向星建图 
int tot=1;       					//但是tot=1方便建图时查找反向边
long long edge[M]; 
 
int f[N][N];                         //去重边必备矩阵存图 
int q[N*2],l,r;
long long dist[N],pre[N];                  //记录流量和路径 
long long ans=0;
bool vis[N];
inline int read()
{
	int x=0,f=1;
	char ch=getchar();
	while(!(ch>='0'&&ch<='9'))
	{
		if(ch=='-') f=-1;
		ch=getchar();
	}
	while(ch>='0'&&ch<='9')
	{
		x=x*10+(ch-'0');
		ch=getchar();
	}
	return x*f;
}
inline void add(int x,int y,long long z)
{
	++tot;
	ver[tot]=y;
	edge[tot]=z;
	Next[tot]=head[x];
	head[x]=tot;
}
inline void init()
{
	for(register int i=0;i<=n+1;++i)
	{
		head[i]=-1;
	}
}
bool bfs()
{
	for(register int i=0;i<=n+1;++i) vis[i]=0;
	l=0;r=0;
	//dist 和 pre 记录的内容都是可覆盖的 所以无需在每次bfs时初始化 
	q[r++]=s;
	vis[s]=1;
	dist[s]=1e18+1; //dist记录增广路的最小流量 每次初始化源点就行了 
	
	while(l<r)
	{
		int x=q[l++];
		for(register int i=head[x];i!=-1;i=Next[i])
		{
			int y=ver[i];
			long long z=edge[i];
			if(z==0) continue;  //流量为0的边没法走
			if(vis[y]==1) continue; //本次bfs遍历过的边不能走 防止死在环里面
			
			vis[y]=1;
			dist[y]=min(dist[x],z);  //更新整条路径的最小流量 
			pre[y]=i;                //记录路径编号 方便更新时查找 
			q[r++]=y; 
			
			if(y==t) return true;   //找到增广路 退出 
		}
	}
	return false;
}
void update()
{
	int x=t;
	while(x!=s)  //倒着跑回去 
	{
		int i=pre[x];  //取出每条边的编号
		edge[i]-=dist[t];
		edge[i^1]+=dist[t];
		//由于我们特殊的建图方式 i^1为该边的反向边的编号 
		//当前找到的增广路的流量为dist[t] 
		//我们让路径上所有的正向边减去这个流量 让反向边加上这个流量 (方便反悔) 
		x=ver[i^1];  //通过反向边往回走 
	}
	ans+=dist[t];  //记录答案 
	return ;
}
int main()
{
	n=read();
	m=read();
	s=read();
	t=read();
	init();
	for(register int i=1;i<=m;++i)
	{
		int x,y;
		long long z;
		x=read();
		y=read();
		z=(long long)read();
		if(f[x][y]==0) //处理重边的小技巧 
		{
			add(x,y,z);
			add(y,x,0);    //建图 反向边边权为0
			f[x][y]=tot; 
		}
		else 
		{
			edge[f[x][y]-1]+=z;  //重边 流量是要累加的 
		}
	}
	while(1) //不停地找增广路 每找到一条更新一下答案 找不到就退出 
	{
		if(bfs()==0) break;
		update();
	}
	printf("%lld\n",ans);
	return 0;
}
Dinic算法:
//网络流 Dinic 
#include <bits/stdc++.h>
using namespace std;
const int N=2e2+10;
const int M=1e4+10;
const int INF=0x3f3f3f3f;
int n,m,s,t;
int head[N],ver[M],Next[M],tot=1;
int f[N][N];
//链式前向星建图+处理重边
long long edge[M];
int dist[N],now[N];
int q[N*2],l,r;
long long ans=0;
void add(int x,int y,int z)
{
	++tot;
	ver[tot]=y;
	edge[tot]=z;
	Next[tot]=head[x];
	head[x]=tot;
	return ;
}
inline int read()
{
	int x=0,f=1;
	char ch=getchar();
	while(!(ch>='0'&&ch<='9'))
	{
		if(ch=='-') f=-1;
		ch=getchar();
	}
	while(ch>='0'&&ch<='9')
	{
		x=x*10+(ch-'0');
		ch=getchar();
	}
	return x*f;
}
inline bool bfs() //bfs找多条增广路 建分层图 
{
	memset(dist,0x3f3f3f3f,sizeof(dist));
	l=0;
	r=0;
	//dist表示每个点的深度 用于做分层图 (找多条增广路) 
	q[r++]=s;
	dist[s]=0;
	now[s]=head[s];  //当前弧优化 
	while(l<r)
	{
		int x=q[l++];
		for(int i=head[x];i!=-1;i=Next[i])
		{
			int y=ver[i];
			long long z=edge[i];
			if(z==0||dist[y]!=0x3f3f3f3f) continue;
			dist[y]=dist[x]+1;
			q[r++]=y;
			now[y]=head[y];  //当前弧优化 
			if(y==t) return true;
		}
	}
	return false;
}
long long dfs(int x,long long sum)  //sum为要更新的一条增广路的最小流量 
{
	if(x==t) return sum;  //更新完增广路 返回最小流量 
	long long k,res=0;
	for(int i=now[x];i!=-1&&sum!=0;i=Next[i])
	{
		now[x]=i;	//当前遍历到的边一定是被增广过了 下次遍历可以略过 
		int y=ver[i];
		long long z=edge[i];
		if(z!=0&&dist[y]==dist[x]+1)
		{
			k=dfs(y,min(z,sum));  //记录最小流量 
			if(k==0) dist[y]=-1;  //如果这个最小流量为0 则这个点及其以后的点都用不到了 
			edge[i]-=k;           //在回溯时进行常规的可反悔边权处理 
			edge[i^1]+=k;
			res+=k;				  //累计多条增广路的流量 
			sum-=k;			      //累计完之后让sum-=k 算剩余的流量 
		}
	}
	return res;
}
int main()
{
	memset(head,-1,sizeof(head));
	n=read();
	m=read();
	s=read();
	t=read();
	for(int i=1;i<=m;i++)
	{
		int x,y,z;
		x=read();
		y=read();
		z=read();
		if(f[x][y]==0)    
		{
			add(x,y,z);
			add(y,x,0);
			f[x][y]=tot;
		}
		else
		{
			edge[f[x][y]-1]+=z;
		}
	}
	while(bfs()!=0)    //找增广路 更新 整体思路同EK算法 
	{
		ans+=dfs(s,1e18+1);
	}
	printf("%lld\n",ans);
	return 0;
}

然后我们来看几道例题:

luogu P1361

链接在这里

题目描述

小 M 在 MC 里开辟了两块巨大的耕地 A 和 B(你可以认为容量是无穷),现在,小 P 有 n 种作物的种子,每种作物的种子有 1 个(就是可以种一棵作物),编号为 1 到 n。

现在,第 i 种作物种植在 A 中种植可以获得 a[i] 的收益,在 B 中种植可以获得 b[i] 的收益,而且,现在还有这么一种神奇的现象,就是某些作物共同种在一块耕地中可以获得额外的收益,小 M 找到了规则中共有 m 种作物组合,第 i 个组合中的作物共同种在 A 中可以获得 c1 的额外收益,共同种在 B 中可以获得 c2 的额外收益。

小 M 很快的算出了种植的最大收益,但是他想要考考你,你能回答他这个问题么?

简化题意

给定n个作物,可以种在耕地A或B中,获得不同收益。某些作物同时种在耕地A或B中可以获得额外收益。 求最大收益。

首先,这就是一个经典的二者选其一问题,很容易想到最小割模型

(为什么会想到最小割)

有 n 个物品和两个集合 A B ,如果一个物品没有放入 A 集合会花费 a[i],没有放入 B集合会花费 b[i];还有若干个形如 ui,vi,wi 限制条件,表示如果 ui 和 vi 同时不在一个集合会花费wi。每个物品必须且只能属于一个集合,求最小的代价。

这是一个经典的 二者选其一 的最小割题目。我们对于每个集合设置源点 s 和汇点 t,第 i 个点由 s 连一条容量为 a[i] 的边、向 t连一条容量为 b[i] 的边。对于限制条件 ui,vi,wi,我们在 ui,vi之间连容量为 wi 的双向边。

注意到当源点和汇点不相连时,代表这些点都选择了其中一个集合。如果将连向 s 或 t 的边割开,表示不放在 A 或 B 集合,如果把物品之间的边割开,表示这两个物品不放在同一个集合。

最小割就是最小花费。

(以上内容摘自OI wiki)

 所以要想到最小割模型

顺便一提最大流最小割定理

是不是还要说明一下什么是割  (其实就是一种点的划分方式,使得网络流中的源点和汇点无法相互到达)

对于这个定理的内容,不严谨且草率地说就是,最大流等于最小割

但实际上他们只有在数值上是相等的,这两者之间并无必然联系。我们在做题的时候,一般只是求一个最小割就可以了(应该吧),所以对于求最小割的问题我们还是跑一个最大流算法即可。

OK回到正题

思路

我们先不考虑这m种组合,如果只是将作物种在A或B中,那我们由上面的二者选其一例题不难想到,我们可以将耕地 A 与每一个作物建一条流量为 a[i] 的边,将耕地 B 与每一个作物建一条流量为  b[i] 的边,然后我们求该图的最小割(最大流)。显然答案并不是最小割,而是总的收益减去最小割的收益,因为一个作物只能种在耕地 A 或者 耕地 B 上,我们通过求最小割割去重复种植的作物(即最小费用),剩下的就是最大的收益了。

那考虑m种组合的情况呢?

其实思路并没有变,这个组合也必然要连向耕地 A 与耕地 B,不妨把每一个组合都看成是一个集合,我们将这个集合与耕地A、B相连,其流量为c1,c2。同时注意集合内部是一个统一的整体,不能被割掉,所以在建图时我们将每一个集合内部的点之间的边权设为正无穷(不会被割掉)。

那么这个题的大体思路就有了o(* ̄▽ ̄*)ブ

一些实现的细节:

对于每一个组合中的点所构成的集合,我们用两个虚拟点来表示,一个作为集合源点,一个作为合集汇点。最后我们将集合源点和汇点向耕地 A , B 连边

 举个例子,红色的点为虚拟源点和汇点,作物1,2在一个集合中,那么这个集合的图大概就这么建(边权都为正无穷,这个集合内部不能动) 

代码实现

最后给出AC代码(附带注释):

#include <bits/stdc++.h>
using namespace std;
const int N=1e7+10;  //怕RE所以数组开的特别大 (毫无意义) 
const int M=1e7+10;
int n,m,s,t,d;
int head[N],ver[M],edge[M],Next[M],tot=1;
int dist[N],pre[N],now[N];
int q[N],l,r;
int ans=0,cnt=0;
int a[N],b[N];
void adds(int x,int y,int z)
{
	++tot;
	ver[tot]=y;
	edge[tot]=z;
	Next[tot]=head[x];
	head[x]=tot;
	return ;
}
void add(int x,int y,int z)
{
	adds(x,y,z);
	adds(y,x,0);
	return ;
}
bool bfs()
{
	memset(dist,0x3f3f3f3f,sizeof(dist));
	l=0;r=0;
	dist[s]=0;
	q[r++]=s;
	now[s]=head[s];
	while(l<r)
	{
		int x=q[l++];
		for(int i=head[x];i!=-1;i=Next[i])
		{
			int y=ver[i];
			int z=edge[i];
			if(z==0||dist[y]!=0x3f3f3f3f) continue;
			q[r++]=y;
			dist[y]=dist[x]+1;
			now[y]=head[y];
			pre[y]=i;
			if(y==t) return true;
		}
	}
	return false;
}
int dfs(int x,int sum)
{
	if(x==t) return sum;
	int k,res=0;
	for(int i=now[x];i!=-1&&sum!=0;i=Next[i])
	{
		now[x]=i;
		int y=ver[i];
		int z=edge[i];
		if(z!=0&&dist[y]==dist[x]+1)
		{
			k=dfs(y,min(sum,z));
			if(k==0) dist[y]=-1;
			edge[i]-=k;
			edge[i^1]+=k;
			res+=k;
			sum-=k;
		}
	}
	return res;
}
int main()
{
	ios::sync_with_stdio(false);
	cin.tie(0);
	cout.tie(0);
	memset(head,-1,sizeof(head));
	cin>>n;
	
	//核心代码部分在主函数 
	/*
	cnt记录总收益
	d是用来做每个集合的虚拟点的(让虚拟点在下一层,总之不要和作物重复) 
	s,t源点汇点这不解释了 (就相当于文中说的耕地A和耕地B) 
	*/ 
	s=0;
	d=10000;
	t=d*5+1;
	for(int i=1;i<=n;i++)
	{
		cin>>a[i];
		cnt+=a[i];
		add(s,i,a[i]);  //作物与耕地A相连 
	} 
	for(int i=1;i<=n;i++)
	{
		cin>>b[i];
		cnt+=b[i];
		add(i,t,b[i]);  //作物与耕地B相连 
	} 
	cin>>m;
	for(int i=1;i<=m;i++)
	{
		int k,c1,c2,x;
		cin>>k>>c1>>c2;
		//先让虚拟点和真正的源点汇点(耕地AB)相连
		//(i+d表示每个集合的虚拟源点) 
		//(i+d*3表示每个集合的虚拟汇点)
		//不要问为什么,我就这么写的,反正你只要别让虚拟点和作物重复就行 
		add(s,i+d,c1);        
		add(i+d*3,t,c2);
		for(int j=1;j<=k;j++)
		{
			cin>>x;
			add(i+d,x,0x3f3f3f3f);
			add(x,i+d*3,0x3f3f3f3f);
			//集合内部 边权设置的大一些 
			/* 
			因为是额外收益 所以在集合内部 虚拟点需要连接到原来的作物上
			(而不是再建一个作物连到x+d*2或者什么奇奇怪怪的地方上)
			*/ 
		}
		cnt+=(c1+c2);
	}
	
	//Dinic 
	while(bfs()!=0)
	{
		ans+=dfs(s,0x3f3f3f3f);
	}
	
	cout<<cnt-ans<<'\n';  //最大收益为总收益减去最小割 
	return 0;
}

那么这一题就结束了φ(゜▽゜*)♪

luogu P2055

链接在这里

题目描述

学校放假了……有些同学回家了,而有些同学则有以前的好朋友来探访,那么住宿就是一个问题。

比如 A 和 B 都是学校的学生,A 要回家,而 C 来看B,C 与 A 不认识。我们假设每个人只能睡和自己直接认识的人的床。那么一个解决方案就是 B 睡 A 的床而 C 睡 B 的床。而实际情况可能非常复杂,有的人可能认识好多在校学生,在校学生之间也不一定都互相认识。

我们已知一共有 n 个人,并且知道其中每个人是不是本校学生,也知道每个本校学生是否回家。问是否存在一个方案使得所有不回家的本校学生和来看他们的其他人都有地方住。

简化题意

校园里有2种人,在校学生和外来人员,其中在校学生有床位,外来人员没有床位,现在有一部分在校学生回家了,询问是否存在一个方案使得所有在学校的人员都有床可睡。要求:每个人只能睡自己认识的人的床(即存在若干对认识关系)

思路

首先不难看出这是个图论题(这个真的能看出来吧)

我们希望每一个在学校的人都能有一个床位,即我们给每个在学校的人匹配一个床位。同时我们注意到相互每个人之间不存在边(关系是建立在人与床之间的),每张床之间也不存在边。

所以你想到了什么?

没错,二分图

这是一个比较典型的二分图匹配问题,但我们这里不用二分图匹配,我们可以拿网络流来做这道题。

那么怎么做呢?

其实很简单,我们将在学校的人与源点连边,将每张床与汇点连边,然后对于每两个认识的人,让其中一个人与他认识的人的床连边,再反过来连一条边,最后跑一个最大流,然后这题就结束啦!(*^▽^*)

给一张比较草率的图解

蓝色表示,绿色表示,这个图大概这样连(大概)

嗯哼~,当然实现起来还有一些细节,就放到代码里讲啦~

代码实现

#include <bits/stdc++.h>
using namespace std;
const int N=1e3;
const int M=1e4;
int n,s,t;
int head[N],ver[M],edge[M],Next[M],tot=1;
int dist[N];
int pre[N],now[N];
int q[N],l,r;
int ans=0;
bool a[N],b[N],c[N],f[N][N];
void add(int x,int y,int z)
{
	++tot;
	ver[tot]=y;
	edge[tot]=z;
	Next[tot]=head[x];
	head[x]=tot;
}
bool bfs()
{
	memset(dist,0x3f3f3f3f,sizeof(dist));
	l=0;r=0;
	dist[s]=0;
	q[r++]=s;
	now[s]=head[s];
	while(l<r)
	{
		int x=q[l++];
		for(int i=head[x];i!=-1;i=Next[i])
		{
			
			int y=ver[i];
			int z=edge[i];
			if(z==0||dist[y]!=0x3f3f3f3f) continue;
			dist[y]=dist[x]+1;
			pre[y]=i;
			now[y]=head[y];
			q[r++]=y;
			if(y==t) return true;
		}
	}
	return false;
}
int dfs(int x,int sum)
{
	if(x==t) return sum;
	int k,res=0;
	for(int i=now[x];i!=-1&&sum!=0;i=Next[i])
	{
		int y=ver[i];
		int z=edge[i];
		now[x]=i;
		if(z!=0&&dist[y]==dist[x]+1)
		{
			k=dfs(y,min(sum,z));
			if(k==0) dist[y]=-1;
			edge[i]-=k;
			edge[i^1]+=k;
			res+=k;
			sum-=k;
		}
	}
	return res;
}
int main()
{
	/*
	网络流的题目重点往往在于如何建模,跑网络流的内容就是一个板子
	所以基本上绝大部分的题目重点实现内容在主函数,网络流的部分就不再注释
	往后的题目我也不再强调这一点啦 
	*/
	ios::sync_with_stdio(false);
	cin.tie(0);
	cout.tie(0);
	int T;
	cin>>T;
	while(T--)
	{
		/*
		嗯~ 此题为多组输入,要做初始化....
		但其实也用不着把所有数组全初始化了 
		*/ 
		memset(head,-1,sizeof(head));
		memset(ver,0,sizeof(ver));
		memset(edge,0,sizeof(edge));
		memset(Next,0,sizeof(Next));
		memset(pre,0,sizeof(pre));
		memset(now,0,sizeof(now));
		memset(q,0,sizeof(q));
		l=0;r=0;
		tot=1;
		ans=0;
		int cnt=0;
		cin>>n;
		s=0;
		t=n*2+1;
		for(int i=1;i<=n;i++)
		{
			cin>>a[i];
		}
		for(int i=1;i<=n;i++)
		{
			cin>>b[i];
		}
		for(int i=1;i<=n;i++)
		{
			c[i]=((!a[i])||(!b[i]));
			/*	
			用来判断此人是否需要床位,写的稍微麻烦一点
			你可以改成 if( (a[i]==1&&b[i]==0) || a[i]==0) c[i]=1;
			*/ 
			if(c[i]==1) cnt++; //记录一下需要床位的人数 
		}
		//为防止人和床重复,我们令i表示第i个人,i+n表示第i张床 
		//人床连边的部分	
		for(int i=1;i<=n;++i)
		{
			for(int j=1;j<=n;++j)
			{
				cin>>f[i][j];
				if(i==j)  //此处见题目描述 特判一下自己 
				{
					add(i,i+n,1); //边权为1 
					add(i+n,i,0);
				}
				else if(f[i][j]==1)  
				{
					add(i,j+n,1); //边权为1
					add(j+n,i,0);
				}
			}
		}
		 
		for(int i=1;i<=n;i++)
		{
			if(c[i]==1) //如果这个人需要一张床,我们将他与源点连边 
			{
				add(s,i,1);
				add(i,s,0);
			}
			if(a[i]==1) //如果这个人有床,我们将这张床与汇点连边 
			{
				add(i+n,t,1);
				add(t,i+n,0);
			}
		}
		//Dinic部分 
		while(bfs()!=0)
		{
			ans+=dfs(s,0x3f3f3f3f);
		}
		
		if(ans>=cnt)
		{
			cout<<"^_^";
		}
		else
		{
			cout<<"T_T";
		}
		cout<<'\n';
	}
	return 0;
}

那么这个题目就结束啦(*^▽^*)

luogu P1231

链接在这里

题目描述

蒟蒻 HansBug 在一本语文书里面发现了一本答案,然而他却明明记得这书应该还包含一份练习题。然而出现在他眼前的书多得数不胜数,其中有书,有答案,有练习册。已知一个完整的书册均应该包含且仅包含一本书、一本练习册和一份答案,然而现在全都乱做了一团。许多书上面的字迹都已经模糊了,然而 HansBug 还是可以大致判断这是一本书还是练习册或答案,并且能够大致知道一本书和答案以及一本书和练习册的对应关系(即仅仅知道某书和某答案、某书和某练习册有可能相对应,除此以外的均不可能对应)。既然如此,HansBug 想知道在这样的情况下,最多可能同时组合成多少个完整的书册。

简化题意

有 n_{1} 本书,n_{2} 本练习册,n_{3}本答案,书与练习册存在 m_{1} 对可能的关系,书与答案存在 m_{2} 对可能的关系,求最多能组合出多少个完整的书册。

思路

我一眼就看出这个题是网络流

其实如果是在考场上可能第一时间想不到这是个网络流,但应该还是能看出这是个匹配问题(,总之我们大概能猜到这个题是网络流。

那我们就来考虑网络流吧(*^▽^*)

如果你很熟悉网络流,应该会想到从源点向练习册连边,然后练习册连向书,书连向答案(关系最多的放中间),答案连向汇点来建图,最后跑一个最大流,求出答案。很遗憾,这么做是错的。(QAQ)

为什么呢,其实画个草图分析一下就好了(又要画图!)

我们刚才建的图长这样:

(在图中我们用红色表示练习册,蓝色表示,绿色表示答案)

 当我们对这个图跑最大流时,我们会找到这两条增广路(下图粉色箭头)

 (不要在意源点没连上的细节)

从图中我们可以看出,第二本书被匹配了两次,被我们的最大流算法算作两对合法的匹配。

但由题意可得,每一本书只能被使用一次(匹配一次),所以这样求出来的答案是错的。

如何保证每一本书仅被使用一次

我们考虑拆点,把一本书拆成两个点,且连接这两个点的边权为1(只能使用一次)

就像这样

这样我们就保证了每本书只能被使用一次,这个题就搞定啦(*^▽^*)

 剩下的细节在代码实现里面讲

代码实现

#include <bits/stdc++.h>
using namespace std;
const int N=1e6+10;
const int M=1e6+10;
int n,s,t;
int head[N],ver[M],edge[M],Next[M],tot=1;
int dist[N];
int pre[N],now[N];
int q[N],l,r;
int ans=0;
void add(int x,int y,int z)
{
	++tot;
	ver[tot]=y;
	edge[tot]=z;
	Next[tot]=head[x];
	head[x]=tot;
}
bool bfs()
{
	memset(dist,0x3f3f3f3f,sizeof(dist));
	l=0;r=0;
	dist[s]=0;
	q[r++]=s;
	now[s]=head[s];
	while(l<r)
	{
		int x=q[l++];
		for(int i=head[x];i!=-1;i=Next[i])
		{
			
			int y=ver[i];
			int z=edge[i];
			if(z==0||dist[y]!=0x3f3f3f3f) continue;
			dist[y]=dist[x]+1;
			pre[y]=i;
			now[y]=head[y];
			q[r++]=y;
			if(y==t) return true;
		}
	}
	return false;
}
int dfs(int x,int sum)
{
	if(x==t) return sum;
	int k,res=0;
	for(int i=now[x];i!=-1&&sum!=0;i=Next[i])
	{
		int y=ver[i];
		int z=edge[i];
		now[x]=i;
		if(z!=0&&dist[y]==dist[x]+1)
		{
			k=dfs(y,min(sum,z));
			if(k==0) dist[y]=-1;
			edge[i]-=k;
			edge[i^1]+=k;
			res+=k;
			sum-=k;
		}
	}
	return res;
}
int main()
{
	memset(head,-1,sizeof(head));
	ios::sync_with_stdio(false);
	cin.tie(0);
	cout.tie(0);
	int n1,n2,n3,m1,m2;
	//这个n用于分层,防止重复 t设置的大一点也是为了防止重复 
	n=10000;
	s=0;
	t=n*4+1;
	cin>>n1>>n2>>n3;
	cin>>m1;
	/*
	第一层是练习册 i
	第二层是书 i+n
	第三层是书 i+n*2
	第四层是答案 i+n*3
	所有边的边权都为1 (计算最大匹配数) 
	*/ 
	for(int i=1;i<=m1;i++) 
	{
		int x,y;
		cin>>x>>y;
		add(y,x+n,1); //练习册向书建边 (注意输入和建边方向是反的) 
		add(x+n,y,0);
	}
	cin>>m2;
	for(int i=1;i<=m2;i++)
	{
		int x,y;
		cin>>x>>y;
		add(x+n*2,y+n*3,1); //书向答案建边 
		add(y+n*3,x+n*2,0);
	}
	for(int i=1;i<=n1;i++)  
	{
		add(i+n,i+n*2,1); //将书拆点 自己连向自己 
		add(i+n*2,i+n,0);
	}
	for(int i=1;i<=n2;i++)
	{
		add(s,i,1);    //源点向练习册建边 
		add(i,s,0);
	}
	for(int i=1;i<=n3;i++)
	{
		add(i+n*3,t,1); //答案向汇点建边 
		add(t,i+n*3,0);
	}
	
	//Dinic 
	while(bfs()!=0)
	{
		ans+=dfs(s,0x3f3f3f3f);
	}
	
	cout<<ans<<'\n';
	return 0;
}

此题完结

luogu P1251

链接在这里

题目描述

一个餐厅在相继的 N 天里,每天需用的餐巾数不尽相同。假设第 i 天需要 ri​ 块餐巾。餐厅可以购买新的餐巾,每块餐巾的费用为 p 分;或者把旧餐巾送到快洗部,洗一块需 m 天,其费用为 f 分;或者送到慢洗部,洗一块需 n 天(n>m),其费用为 s 分(s<f)。

每天结束时,餐厅必须决定将多少块脏的餐巾送到快洗部,多少块餐巾送到慢洗部,以及多少块保存起来延期送洗。但是每天洗好的餐巾和购买的新餐巾数之和,要满足当天的需求量。

试设计一个算法为餐厅合理地安排好 N 天中餐巾使用计划,使总的花费最小。编程找出一个最佳餐巾使用计划。

简化题意

我觉得题目挺简洁的

第 i 天需要 ri 块餐巾

餐巾有三种获得方式

1.m天前快洗部的旧餐巾(费用为f)

2.n天前慢洗部的旧餐巾(费用为s)

3.购买新餐巾(费用为p)

每天晚上餐厅会对餐巾进行处理

1.送到快洗部

2.送到慢洗部

3.不作处理

求N天内的最小花费

我觉得我写的更简洁

思路

因为此题为网络流与线性规划24题之一,不难看出这是一道网络流

所以这是一道最小费用最大流

首先对于最小费用最大流还是直接套模板就行,所以我们考虑如何建模

那如何建模呢

先回头看看我们的简要题意:

餐巾有三种获得方式

1.m天前快洗部的旧餐巾(费用为f)

2.n天前慢洗部的旧餐巾(费用为s)

3.购买新餐巾(费用为p)

每天晚上餐厅会对餐巾进行处理

1.送到快洗部

2.送到慢洗部

3.不作处理

你会发现餐厅每天要干两件事,一是获得餐巾,二是处理餐巾。

所以我们拆点,把餐厅的每一天分为两个状态:获取餐巾输送餐巾

我们再考虑如何将传输餐巾的过程建边

首先我们应该是从餐厅某一天的输送状态向餐厅某一天的获取状态连边

然后我们考虑具体实现

1.获取餐巾

餐厅当天只能收到来自m天或n天之前的餐巾,且数量不限(餐巾可以积累,这符合题意),所以对于餐厅的第 i 天,我们向 m 天后的每一天建一条流量为无穷大,费用为 f 的边;我们向 n 天后的每一天建一条流量为无穷大,费用为 s 的边(由第 i 天的输送状态连向 m 天或 n 天后的获取状态)。

或者餐厅直接去购买餐巾,因为购买行为可以发生在任意一天,且数量不限,所以我们不妨从源点向每一天连一条流量为无穷大,费用为 p 的边(连向每一天的获取状态)。

2.输送餐巾

输送操作 1 , 2 已经在获取餐巾的部分实现完毕(与获取状态建立了联系),我们考虑输送操作 3 。

那就是把餐巾拖到后一天了,对于餐厅的第 i 天,我们向第 i+1 天连一条流量为无穷大,费用为 0 的边(由第 i 天的输送状态连向第 i+1 天的输送状态,因为没有洗)。 

然后,传输餐巾的过程就写完了,但还有一点要注意的地方。

在前文中我们确定了一定是从餐厅某一天的输送状态向餐厅某一天的获取状态连边,所以我们的源点应该连向每一天的输送状态,每天的获取状态应该连向汇点,同时这些边的流量应为 r[i] (用来限制流量),费用应为0(符合题意)。如果我们反过来建图,你就会发现我们无法从源点走到汇点,也就没法跑网络流了。事实上,源点和汇点的连接应当看流的方向,保证从源点到汇点存在可达路径,而不是考虑我们认为的一般意义上的起点和终点。

最后,这个题就做完了。(*^▽^*)

总结一下建模的思路:(这个题相对比较麻烦)

1.第 i 天的输送状态向 m 天后的每一天的获取状态建一条流量为无穷大,费用为 f 的边

2.第 i 天的输送状态向 n 天后的每一天的获取状态建一条流量为无穷大,费用为 s 的边

3.第 i 天的输送状态向第 i+1 天的输送状态连一条流量为无穷大,费用为 0 的边

4.从源点向每一天的获取状态连一条流量为无穷大,费用为 p 的边

5.从源点向每一天的输送状态连一条流量为 r[i] ,费用为 0 的边

6.从每一天的获取状态向汇点连一条流量为 r[i] ,费用为 0 的边

剩余的其他细节处理我会在代码实现中注明

代码实现

下面给出AC代码 (这题折腾了好久)

#include <bits/stdc++.h>
using namespace std;
const int N=1e6+10;
const int M=1e6+10;
const long long inf=1e18+1;
int n,s,t;
int head[N],ver[M],Next[M],tot=1;
long long edge[M],value[M];
long long dist[N],flow[N];
int pre[N];
int q[N],l,r;
bool vis[N];
long long ans=0;
long long a[N];
void add(int x,int y,long long z,long long w)
{
	++tot;
	ver[tot]=y;
	edge[tot]=z;
	value[tot]=w;
	Next[tot]=head[x];
	head[x]=tot;
}
bool spfa()
{
	for(register int i=0;i<=n*3;++i) dist[i]=inf;
	memset(vis,0,sizeof(vis));
	l=0;r=0;
	q[r++]=s;
	vis[s]=1;
	dist[s]=0;
	flow[s]=0x3f3f3f3f;
	while(l<r)
	{
		int x=q[l++];
		vis[x]=0;
		for(int i=head[x];i!=-1;i=Next[i])
		{
			int y=ver[i];
			long long z=edge[i];
			long long w=value[i];
			if(z!=0&&dist[y]>dist[x]+w)
			{
				dist[y]=dist[x]+w;
				flow[y]=min(flow[x],z);
				pre[y]=i;
				if(vis[y]==0)
				{
					vis[y]=1;
					q[r++]=y;
				}	
			}
		}
	}
	if(dist[t]!=inf) return true;
	return false;
}
void update()
{
	int x=t;
	while(x!=s)
	{
		int i=pre[x];
		edge[i]-=flow[t];
		edge[i^1]+=flow[t];
		x=ver[i^1];
	}
	ans+=dist[t]*flow[t];
	return ;
}
int main()
{
	ios::sync_with_stdio(false);
	cin.tie(0);
	cout.tie(0);
	memset(head,-1,sizeof(head));
	long long p,d1,f1,d2,f2;
	cin>>n;
	for(int i=1;i<=n;i++)
	{
		cin>>a[i];
	}
	cin>>p>>d1>>f1>>d2>>f2;	
	/*
	再强调一遍
	
	1.第 i 天的输送状态向 m 天后的每一天的获取状态建一条流量为无穷大,费用为 f 的边

	2.第 i 天的输送状态向 n 天后的每一天的获取状态建一条流量为无穷大,费用为 s 的边

	3.第 i 天的输送状态向第 i+1 天的输送状态连一条流量为无穷大,费用为 0 的边

	4.从源点向每一天的获取状态连一条流量为无穷大,费用为 p 的边

	5.从源点向每一天的输送状态连一条流量为 r[i] ,费用为 0 的边

	6.从每一天的获取状态向汇点连一条流量为 r[i] ,费用为 0 的边 
	
	代码中 i+n 表示第 i 天的输送状态 
	代码中 i 表示第 i 天的获取状态 
	
	d1为快洗部洗一块的天数 m 
	d2为慢洗部洗一块的天数 n 
	f1为快洗部洗一块的费用 f 
	f2为快洗部洗一块的费用 s
	*/ 
	s=0;
	t=n*2+1;
	for(int i=1;i<=n;++i)
	{
		if(i+d1<=n)  //特判保证不越界 
		{
			add(i+n,i+d1,inf,f1);  //建图操作 1 
			add(i+d1,i+n,0,-f1);
		}
		if(i+d2<=n)  //特判保证不越界 
		{
			add(i+n,i+d2,inf,f2);  //建图操作 2 
			add(i+d2,i+n,0,-f2);
		}
		if(i+1<=n)   //特判保证不越界 
		{
			add(i+n,i+n+1,inf,0);  //建图操作 3
			add(i+n+1,i+n,0,0);	
		}
		add(s,i,inf,p);  //建图操作 4 
		add(i,s,0,-p);	
	}
	
	for(int i=1;i<=n;i++)
	{
		add(s,i+n,a[i],0);  //建图操作 5 
		add(i+n,s,0,0);
		add(i,t,a[i],0);    //建图操作 6
		add(t,i,0,0);
	}
	
	//EK+spfa求最小费用最大流 
	while(spfa()!=0)
	{
		update();
	}
	
	cout<<ans<<'\n';
	return 0;
}

那么这个题就做完啦!(T_T)

 

——To be continue

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值