网络流学习笔记(持续更新)

什么是网络流?

顾名思义,网络流就是通过网络传递数据的流
开个玩笑,OI怎么会考到网络工程呢?(其实我一开始也是这么被骗进来的)
网络流,顾名思义就是从一个源点流向一个汇点的流
打个比方
有一家水厂,他们发扬着“我们不运输水,我们只做纯净水的生产者”的理念,为mygr市提供着源源不断的纯净水
问题来了,对于一家饮料厂急需大量的水来生产可口可乐
水厂和饮料厂之间通过许多分叉口(节点)和管道(边)来运输纯净水
每一条管道有自己的容量,这表示同一时间内这条管道能流过的最大的水量
大致长这个样子(一般而言s代表源点,t代表汇点)

求饮料厂一天内最多能收到多少纯净水来生产可口可乐
某一个人很急,因为她已经半个钟头没喝到可乐了!纯净水运得越多,可乐生产越快
可是某一个人不喝到可乐就不会开始工作,所以他把这个重任托付给了你
这可是事关某一个人性命的大事!请您尽快解决!

———————————————分割线———————————————————

dinic算法

此时我们引入一个概念:增广路
增广路,就是选择这条路径后能使最大流(到达汇点的最大的流量)更大的路径
网络流中找最大流的过程实际上就是不断寻找增广路直到不存在增广路的过程
一个很暴力的思想是:每次都dfs一遍,如果到达汇点则加上流量,如果无法到达则统计答案
可这个做法有大问题
还是刚才那个图,如果一开始我们选择的是s-2-1-t这条路径
那么就不存在增广路了,答案为1
可是我们发现,选择s-2-t 和s-1-t这两条道路可以使答案更优,答案为2
这该怎么办呢?
一个绝妙的方法:建立反边
即在减去流量时,在对应方向相反的边上加上这个流量
刚才那个例子:我们走过s-2-1-t这条路径后,将s-2,2-1,1-t这三条边的容量减去这条路径的流量
再建立反边t-1,1-2,2-s,他们的容量设为这条路径的流量
此时再跑dfs,我们会发现多出了一条路径1-2,最终答案也变为了2
为什么正确呢?
因为我们建反边后,再次走上这条路径时相当于把原先这条路径上的流量又退了回去,相当于这条边又回到了没用过的状态,所以最终答案没有变(感性理解即可)
接下来引入一个算法:dinic算法
算法思想:每次dfs前先跑一次bfs,记录下可到达点的深度,接下来跑dfs时只能跑深度为当前点的深度+1的点,这样每次剩余下来的流量又可以继续递归直至没有流量了为止
洛谷模板题
代码:(建议手敲)

#include<bits/stdc++.h>
#define int long long
using namespace std;
const int Max=2e4+5,inf=0x7fffffff;
int n,m,s,t;
struct edge{
	int to,w,from,next;
}p[Max];
int head[Max],last[Max],idx=0,dep[Max];
void add(int u,int v,int w)
{
	if(head[u])
		p[last[u]].next=++idx;
	else
		head[u]=++idx;
	last[u]=idx;
	p[idx].to=v;
	p[idx].from=idx+1;
	p[idx].w=w;
	
	if(head[v])
		p[last[v]].next=++idx;
	else
		head[v]=++idx;
	last[v]=idx;
	p[idx].to=u;
	p[idx].from=idx-1;
	p[idx].w=0;
}
bool bfs()
{
	queue<int> in;
	for(int i=1;i<=n;i++)
		dep[i]=inf;
	in.push(s);
	dep[s]=0;
	while(!in.empty())
	{
		int now=in.front();
		in.pop();
		for(int i=head[now];p[i].to;i=p[i].next)
		{
			if(p[i].w==0 or dep[p[i].to]!=inf)
				continue;
			dep[p[i].to]=dep[now]+1;
			in.push(p[i].to);
		}
	}
	if(dep[t]!=inf)
		return true;
	return false;
}
int dfs(int now,int minn)
{
	if(now==t)
		return minn;
	int tmp=0,tot=0;
	for(int i=head[now];p[i].to!=0 and minn>0;i=p[i].next)
	{
		if(p[i].w==0)
			continue;
		if(dep[p[i].to]==dep[now]+1 and (tmp=dfs(p[i].to,min(minn,p[i].w))))
		{
			p[i].w-=tmp;
			if(p[i].from == i-1)
				p[i-1].w+=tmp;
			else
				p[i+1].w+=tmp;
			tot+=tmp;
			minn-=tmp;
			if(minn==0)
				break;
		}
	}
	return tot;
}
signed main()
{
	scanf("%lld%lld%lld%lld",&n,&m,&s,&t);
	int x,y,w;
	for(int i=1;i<=m;i++)
	{
		scanf("%lld%lld%lld",&x,&y,&w);
		add(x,y,w);
	}
	int ans=0;
	while(bfs())
	{
		ans+=dfs(s,inf);
	}
	printf("%lld",ans);
}

然而当你兴致冲冲的把代码交上去的时候,你会发现:
“嗯?怎么有个点超时了???”
你被骗了
在这里插入图片描述

优化

“你被骗啦”
清纯小dinic算法可是会超时的哟~
想让dinic变成大人,还需要一番磨砺才行哟~

算法改进:
当前弧优化:我们考虑到每当我们再次访问到一个点时,从前访问过的与这个点的连边一定已经流量为0或无法走通
所以我们记录一个Now数组,记录下以前访问到的边的编号,避免重复计数
这样优化的dinic算法跑得飞快,彻底变成了能独当一面的大人哟~
代码:(小改一下而已)

#include<bits/stdc++.h>
#define int long long
using namespace std;
const int Max=2e4+5,inf=0x7fffffff;
int n,m,s,t;
struct edge{
	int to,w,from,next;
}p[Max];
int head[Max],last[Max],idx=0,dep[Max];
int Now[Max];
void add(int u,int v,int w)
{
	if(head[u])
		p[last[u]].next=++idx;
	else
		head[u]=++idx;
	last[u]=idx;
	p[idx].to=v;
	p[idx].from=idx+1;
	p[idx].w=w;
	
	if(head[v])
		p[last[v]].next=++idx;
	else
		head[v]=++idx;
	last[v]=idx;
	p[idx].to=u;
	p[idx].from=idx-1;
	p[idx].w=0;
}
bool bfs()
{
	queue<int> in;
	for(int i=1;i<=n;i++)
	{
		dep[i]=inf;
		Now[i]=head[i];
	}
		
	in.push(s);
	dep[s]=0;
	while(!in.empty())
	{
		int now=in.front();
		in.pop();
		for(int i=head[now];p[i].to;i=p[i].next)
		{
			if(p[i].w==0 or dep[p[i].to]!=inf)
				continue;
			dep[p[i].to]=dep[now]+1;
			in.push(p[i].to);
		}
	}
	if(dep[t]!=inf)
		return true;
	return false;
}
int dfs(int now,int minn)
{
	if(now==t)
		return minn;
	int tmp=0,tot=0;
	for(int i=Now[now];p[i].to!=0 and minn>0;i=p[i].next)
	{
		Now[now]=i;
		if(p[i].w==0)
			continue;
		if(dep[p[i].to]==dep[now]+1)
		{
			tmp=dfs(p[i].to,min(minn,p[i].w));
			if(tmp==0)
			{
				dep[p[i].to]=inf;
			}
			p[i].w-=tmp;
			if(p[i].from == i-1)
				p[i-1].w+=tmp;
			else
				p[i+1].w+=tmp;
			tot+=tmp;
			minn-=tmp;
			if(minn==0)
				break;
		}
	}
	return tot;
}
signed main()
{
	scanf("%lld%lld%lld%lld",&n,&m,&s,&t);
	int x,y,w;
	for(int i=1;i<=m;i++)
	{
		scanf("%lld%lld%lld",&x,&y,&w);
		add(x,y,w);
	}
	int ans=0;
	while(bfs())
	{
		ans+=dfs(s,inf);
	}
	printf("%lld",ans);
}

这样改造后的dinic就足以跑过大部分网络流的题目了
(应该吧)

———————————分割线————————————————

最小费用最大流

已经过去半个钟头惹,第一批可乐还是没有生产出来
“天台的景色很美,可惜这是最后一次在这个世界看这样的美景了”
某一个人坐在天台的栅栏上,摇晃着,舒缓的,哼着歌,仿佛在向这个世界做最后的告别
“我想创造一个,只有我和可乐的世界”她张开嘴,缓缓说道
“一个没有可乐的世界,活着,还有什么意义?”
“当我第一次与它相拥热吻时,泪水止不住的往下流,它,是我的,毕生挚爱”

你很着急,连忙赶到水厂质问为什么纯净水还是没有运到
原来是那个邪恶の管道公司老板最近颁布了项新的规定
对于每个管道,现在不仅有流量限制,而且每流过一定单位数量的水,也要交付一定数量的钱
在没有最优方案前,水厂老板不会轻易开工
问,在流量依旧最大的同时,求最小的费用
如果程序没在1s内跑出来的话,某一个人就喝不到可乐,他就会悲痛欲绝,请您帮帮他8!

———————————分割线————————————————
对于这种问题,我们一般称其为“最小费用最大流问题”
朴素的算法应该是不可行的啦
这时我们想起了我们的老朋友——spfa!

原图来自
虽说spfa已死 ,但是由于接下来会处理负权边的情况,所以也只能用spfa(当然如果你要用改进版dijkstra那就当我没说)
我们考虑到一开始的算法:建立反边跑dfs
所以事实上,最小费用流也只是把dfs改成了spfa而已,其本质并没有变
洛谷模板题

#include<bits/stdc++.h>
#define ll long long
using namespace std;
const int Max=2e5+5,inf=0x7fffffff;
int n,m,s,t;
struct edge{
	int to,w,c,from,next;
}p[Max];
int head[Max],last[Max],idx=0,from[Max],r[Max],dis[Max],infl[Max];
int maxw=0,maxc=0;
bool vis[Max];
void add(int u,int v,int w,int c)
{
	if(head[u])
		p[last[u]].next=++idx;
	else
		head[u]=++idx;
	last[u]=idx;
	p[idx].to=v;
	p[idx].from=idx+1;
	p[idx].w=w;
	p[idx].c=c;
	
	if(head[v])
		p[last[v]].next=++idx;
	else
		head[v]=++idx;
	last[v]=idx;
	p[idx].to=u;
	p[idx].from=idx-1;
	p[idx].w=0;
	p[idx].c=-c;
}
bool spfa()
{
	queue<int> in;
	for(int i=1;i<=n;i++)
	{
		dis[i]=inf;
		vis[i]=false;
		infl[i]=0;
		from[i]=0;
		r[i]=0;
	}
	dis[s]=0;
	infl[s]=inf;
	in.push(s);
	while(!in.empty())
	{
		int now=in.front();in.pop();
		vis[now]=false;
		for(int i=head[now];p[i].to!=0;i=p[i].next)
		{
			if(p[i].w==0)
				continue;
			if(dis[p[i].to]>dis[now]+p[i].c)
			{
				dis[p[i].to]=dis[now]+p[i].c;
				infl[p[i].to]=min(infl[now],p[i].w);
				from[p[i].to]=now;
				r[p[i].to]=i;
				if(!vis[p[i].to])
				{
					vis[p[i].to]=true;
					in.push(p[i].to);
				}
			}
		}
	}
	if(dis[t]==inf)
		return false;
	int now=t;
	maxw+=infl[t];
	maxc+=infl[t]*dis[t];
	while(now!=0)
	{
		p[r[now]].w-=infl[t];
		p[p[r[now]].from].w+=infl[t];
		now=from[now];
	}
	return true;
}
int main()
{
	scanf("%d%d%d%d",&n,&m,&s,&t);
	int u,v,w,c;
	for(int i=1;i<=m;i++)
	{
		scanf("%d%d%d%d",&u,&v,&w,&c);
		add(u,v,w,c);
	}
	while(spfa());
	
	printf("%d %d",maxw,maxc);
}
/*
4 5 4 3
4 2 30 2
4 3 20 3
2 3 20 1
2 1 30 9
1 3 40 5
*/

故事的最后,某一个人喝上了可乐,他/她十分感谢你并决定跟你过一辈子
但是网络流的故事还远远没有结束!

与网络流的爱恨情仇

分层图套网络流

家园 / 星际转移问题
请先看完题目!
请先看完题目!
请先看完题目!
请先看完题目!
请先看完题目!
观察题目,一眼顶真,鉴定为网络流
但是在建模的时候却出了大问题
怎么设计状态?
我们发觉这道题的n很小,而且有两个属性:点的编号与时间
所以我们可以考虑建分层图
对于每个点我们可以表示为一个二元组(rank,time)
其中rank表示点的编号,time表示这个点所在的时间
用一个getrank函数获取这个点的专属编号
我们考虑到身处第i号空间站的人可以一直停留在这个空间站
所以我们从每个(i,j)号点向第(i,j+1)号点连一条流量为inf,边权为0的边
对于每辆星穹列车,它有一条属于自己的星穹铁道(不是
我们用一个数组S把它存储起来
第0时刻时,星穹列车从点(S0,0)走到点(S1,1)
第1时刻时,星穹列车从点(S1,1)走到点(S2,2)
第j时刻,星穹列车从点(S(j%R+1),j)走到点(S((j+1)%R+1),j+1)
所以我们每次从点(S(j%R+1),j)向点(S((j+1)%R+1),j+1)连一条容量为h,边权为0
最后,我们从s点向点(0,0)连一条流量为k,费用为0的边
从每个点(-1,i)向点t连一条流量为inf,费用为i的边
最后把求记录费用相加改为取边权的最大值即可
代码:

#include<bits/stdc++.h>
#define ll long long
using namespace std;
const int Max=4e6+5,inf=0x7fffffff,nmax=1000;
int n,m,s,t,k;
struct edge{
	int to,w,c,from,next;
	edge()
	{
		to=-2;
	}
}p[Max];
int head[Max],last[Max],idx=0,from[Max],r[Max],dis[Max],infl[Max];
int maxw=0,maxc=0;
bool vis[Max];
int getrank(int rank,int time)
{
	return (rank+1)+time*n+2;
}
void add(int u,int v,int w,int c)
{
	if(head[u])
		p[last[u]].next=++idx;
	else
		head[u]=++idx;
	last[u]=idx;
	p[idx].to=v;
	p[idx].from=idx+1;
	p[idx].w=w;
	p[idx].c=c;
	
	if(head[v])
		p[last[v]].next=++idx;
	else
		head[v]=++idx;
	last[v]=idx;
	p[idx].to=u;
	p[idx].from=idx-1;
	p[idx].w=0;
	p[idx].c=-c;
}
bool spfa()
{
	queue<int> in;
	for(int i=1;i<=t;i++)
	{
		dis[i]=inf;
		vis[i]=false;
		infl[i]=0;
		from[i]=0;
		r[i]=0;
	}
	dis[s]=0;
	infl[s]=inf;
	in.push(s);
	while(!in.empty())
	{
		int now=in.front();in.pop();
		vis[now]=false;
		for(int i=head[now];p[i].to!=-2;i=p[i].next)
		{
			if(p[i].w==0)
				continue;
			if(dis[p[i].to]>dis[now]+p[i].c)
			{
				dis[p[i].to]=dis[now]+p[i].c;
				infl[p[i].to]=min(infl[now],p[i].w);
				from[p[i].to]=now;
				r[p[i].to]=i;
				if(!vis[p[i].to])
				{
					vis[p[i].to]=true;
					in.push(p[i].to);
				}
			}
		}
	}
	if(dis[t]==inf)
		return false;
	int now=t;
	maxw+=infl[t];
	maxc=max(maxc,dis[t]);
	while(now!=0)
	{
		p[r[now]].w-=infl[t];
		p[p[r[now]].from].w+=infl[t];
		now=from[now];
	}
	return true;
}
int main()
{
	scanf("%d%d%d",&n,&m,&k);
	n+=2;
	s=1;t=(n+2)*nmax+5;
	add(s,getrank(0,0),k,0);
	for(int i=-1;i<=n-2;i++)
	{
		for(int j=0;j<=nmax-2;j++)
		{
			add(getrank(i,j),getrank(i,j+1),inf,0);
		}
	}
	int h,R,S[nmax];
	for(int i=1;i<=m;i++)
	{
		scanf("%d%d",&h,&R);
		for(int j=1;j<=R;j++)
			scanf("%d",&S[j]);
		for(int j=0;j<=nmax-2;j++)
		{
			add(getrank(S[((j%R)+1)],j),getrank(S[(((j+1)%R)+1)],j+1),h,0);
		}
	}
	for(int i=0;i<=nmax-1;i++)
		add(getrank(-1,i),t,inf,i);
	while(spfa());
	
	printf("%d",maxc);
}
/*
4 5 4 3
4 2 30 2
4 3 20 3
2 3 20 1
2 1 30 9
1 3 40 5
*/

互斥模型

方格取数问题
请先看完题目!
请先看完题目!
请先看完题目!
请先看完题目!
请先看完题目!
科普:最小割,指对于一个有向有权图,删掉一条边的代价为对应的边权,求把两点分开所需的最小费用
结论:如果把边权变为边的容量,则最小割等于最大流
可以这么理解,把一条边的流量用掉之后,这条路径的容量要整体减去路径上的最小值,就相当于使这条路径无法通行的最小费用
当不存在增广路后,则说明两点间不连通,此时的最大流也就相当于最小割

这跟这道题有什么关系呢?
我们考虑到选择一个格子后,就无法选择其上下左右的四个格子
所以我们把一个没颜色的格子染成白色,把白色格子的周围四个格子染成黑色,把黑色格子的周围染成白色,可以发现形成了一个二分图
我们把白色的格子抽象成点,放在左边,从原点向白色格子的点连一条容量为格子上数字的边
对黑色的格子做同样的操作,放在右边,向汇点连一条容量为格子上数字的边
从每个白色格子向他周围四个黑色格子连一条容量为inf的边,此时求最小割再用总边权减去最小割即可
图差不多长这样:

为什么是正确的?
从最小割的角度理解:
如果我们不选择白色点i,则此时它周围四个点就都能选择
因为答案是总边权减去最小割,所以可以理解为初始时所有点都选
所以求出的最小割也就是挑去一些点使得方案合法也就是使得s向白色点的连边不选择或黑色点向t的连边不选择或两个都不选的反贡献最小值
所以具有正确性
代码:

#include<bits/stdc++.h>
#define int long long
using namespace std;
const int Max=2e4+5,inf=0x7fffffff;
int n,m,s,t;
struct edge{
	int to,w,from,next;
}p[Max];
int head[Max],last[Max],idx=0,dep[Max];
int Map[205][205];
bool vis[Max];
void add(int u,int v,int w)
{
	if(head[u])
		p[last[u]].next=++idx;
	else
		head[u]=++idx;
	last[u]=idx;
	p[idx].to=v;
	p[idx].from=idx+1;
	p[idx].w=w;
	
	if(head[v])
		p[last[v]].next=++idx;
	else
		head[v]=++idx;
	last[v]=idx;
	p[idx].to=u;
	p[idx].from=idx-1;
	p[idx].w=0;
}
bool bfs()
{
	queue<int> in;
	for(int i=1;i<=t;i++)
	{
		dep[i]=inf;
		vis[i]=false;
	}
		
	in.push(s);
	dep[s]=0;
	while(!in.empty())
	{
		int now=in.front();
		in.pop();
		for(int i=head[now];p[i].to;i=p[i].next)
		{
			if(p[i].w==0 or dep[p[i].to]!=inf)
				continue;
			dep[p[i].to]=dep[now]+1;
			in.push(p[i].to);
		}
	}
	if(dep[t]!=inf)
		return true;
	return false;
}
int dfs(int now,int minn)
{
	if(now==t)
		return minn;
	int tmp=0,tot=0;
	for(int i=head[now];p[i].to!=0 and minn>0;i=p[i].next)
	{
		if(p[i].w==0)
			continue;
		if(dep[p[i].to]==dep[now]+1 and !vis[p[i].to] and (tmp=dfs(p[i].to,min(minn,p[i].w))))
		{
			p[i].w-=tmp;
			if(p[i].from == i-1)
				p[i-1].w+=tmp;
			else
				p[i+1].w+=tmp;
			tot+=tmp;
			minn-=tmp;
			if(minn==0)
				break;
		}
	}
	vis[now]=true;
	return tot;
}
inline int getrank(int x,int y)
{
	return x+(y-1)*n+1;
}
signed main()
{
	scanf("%lld%lld",&m,&n);
	int sum=0;
	s=1,t=n*m+2;
	for(int i=1;i<=m;i++)
	{
		for(int j=1;j<=n;j++)
		{
			scanf("%lld",&Map[j][i]);
			sum+=Map[j][i];
		}
	}
	for(int i=1;i<=m;i++)
	{
		for(int j=1;j<=n;j++)
		{
			int rank=getrank(j,i);
			if(j%2==1)
			{
				if(i%2==1)
				{
					add(s,rank,Map[j][i]);
					if(j>1)
						add(rank,getrank(j-1,i),inf);
					if(j<n)
						add(rank,getrank(j+1,i),inf);
					if(i>1)
						add(rank,getrank(j,i-1),inf);
					if(i<m)
						add(rank,getrank(j,i+1),inf);
				}
				else
				{
					add(rank,t,Map[j][i]);
					if(j>1)
						add(getrank(j-1,i),rank,inf);
					if(j<n)
						add(getrank(j+1,i),rank,inf);
					if(i>1)
						add(getrank(j,i-1),rank,inf);
					if(i<m)
						add(getrank(j,i+1),rank,inf);
				}
			}
			else
			{
				if(i%2==0)
				{
					add(s,rank,Map[j][i]);
					if(j>1)
						add(rank,getrank(j-1,i),inf);
					if(j<n)
						add(rank,getrank(j+1,i),inf);
					if(i>1)
						add(rank,getrank(j,i-1),inf);
					if(i<m)
						add(rank,getrank(j,i+1),inf);
				}
				else
				{
					add(rank,t,Map[j][i]);
					if(j>1)
						add(getrank(j-1,i),rank,inf);
					if(j<n)
						add(getrank(j+1,i),rank,inf);
					if(i>1)
						add(getrank(j,i-1),rank,inf);
					if(i<m)
						add(getrank(j,i+1),rank,inf);
				}
			}
		}
	}
	int ans=0;
	while(bfs())
	{
		ans+=dfs(s,inf);
	}
	printf("%lld",sum-ans);
}

还有一道升级版的方格取数:骑士共存问题
实际上看题目给出的图,骑士和它攻击范围内的棋子所在的方格的颜色一定不同
所以还是能将其拆成一个二分图跑最小割
代码:

#include<bits/stdc++.h>
#define int long long
using namespace std;
const int Max=2e6+5,inf=0x7fffffff;
int n,m,s,t;
struct edge{
	int to,w,from,next;
}p[Max];
int head[Max],last[Max],idx=0,dep[Max];
int Map[505][505];
int tx[16]={0,1,2,2,1,-1,-2,-2,-1},ty[16]={0,2,1,-1,-2,-2,-1,1,2};
bool vis[Max];
void add(int u,int v,int w)
{
	if(head[u])
		p[last[u]].next=++idx;
	else
		head[u]=++idx;
	last[u]=idx;
	p[idx].to=v;
	p[idx].from=idx+1;
	p[idx].w=w;
	
	if(head[v])
		p[last[v]].next=++idx;
	else
		head[v]=++idx;
	last[v]=idx;
	p[idx].to=u;
	p[idx].from=idx-1;
	p[idx].w=0;
}
bool bfs()
{
	queue<int> in;
	for(int i=1;i<=t;i++)
	{
		dep[i]=inf;
		vis[i]=false;
	}
		
	in.push(s);
	dep[s]=0;
	while(!in.empty())
	{
		int now=in.front();
		in.pop();
		for(int i=head[now];p[i].to;i=p[i].next)
		{
			if(p[i].w==0 or dep[p[i].to]!=inf)
				continue;
			dep[p[i].to]=dep[now]+1;
			in.push(p[i].to);
		}
	}
	if(dep[t]!=inf)
		return true;
	return false;
}
int dfs(int now,int minn)
{
	if(now==t)
		return minn;
	int tmp=0,tot=0;
	for(int i=head[now];p[i].to!=0 and minn>0;i=p[i].next)
	{
		if(p[i].w==0)
			continue;
		if(dep[p[i].to]==dep[now]+1 and !vis[p[i].to] and (tmp=dfs(p[i].to,min(minn,p[i].w))))
		{
			p[i].w-=tmp;
			if(p[i].from == i-1)
				p[i-1].w+=tmp;
			else
				p[i+1].w+=tmp;
			tot+=tmp;
			minn-=tmp;
			if(minn==0)
				break;
		}
	}
	vis[now]=true;
	return tot;
}
inline int getrank(int x,int y)
{
	return x+(y-1)*n+1;
}
signed main()
{
	scanf("%lld%lld",&n,&m);
	int sum=n*n-m;
	s=1,t=n*n+2;
	int x,y;
	for(int i=1;i<=m;i++)
	{
		scanf("%d%d",&x,&y);
		Map[x][y]=1;
	}
	for(int i=1;i<=n;i++)
	{
		for(int j=1;j<=n;j++)
		{
			if(Map[j][i]==1)
				continue;
			if((i%2==1 and j%2==1) or (i%2==0 and j%2==0))
				add(s,getrank(j,i),1);
			else
				add(getrank(j,i),t,1);
			for(int k=1;k<=8;k++)
			{
				if(j+tx[k]>=1 and j+tx[k]<=n and i+ty[k]>=1 and i+ty[k]<=n)
				{
					if((i%2==1 and j%2==1) or (i%2==0 and j%2==0))
					{
						add(getrank(j,i),getrank(j+tx[k],i+ty[k]),inf);
					}
					else
					{
						add(getrank(j+tx[k],i+ty[k]),getrank(j,i),inf);
					}
				}
			}
		}
	}
	int ans=0;
	while(bfs())
	{
		ans+=dfs(s,inf);
	}
	printf("%lld",sum-ans);
}

总结:大部分具有互斥性质的问题一般都转化为互斥模型能够使用网络流二分拆点解决(包括二分图最大匹配)

P2765 魔术球问题
刚刚入门提高组的同学:“?这不是我们的初赛题吗?????”

😅
老实说,参加提高组时我第一时间就想起这道网络流的题目(

咳咳,回归正题
初看这道题可能会一脸懵
?这和网络流有半毛钱关系?????
别急,我们观察一下,我们发现对于每个点而言,他们和一些点都有一个连接关系(这个词是我瞎想的)
怎么理解呢?
比方说两个数a,b,满足a<b且a+b的和为一个完全平方数
那么在插入完a之后,当我们插入b时,b可能就接在a的后面
我们可以将它抽象成一个图的关系,想上面的a和b,我们就可以连一条a到b的有向边
化成图呢,就是差不多长这样

接下来思考这个图应该怎样应用
我们从1开始逐个添加魔术球
当添加了k个魔术球时
此时这k个魔术球要么是独立自己在一个柱子上
要么是接在一个与它的和为完全平方数的数后面
转换到图上,那就是一个数要么在一条链上,要么自己独立为一条链的根
分析一下样例
就像图上的3,它可能接在1的后面(接在1到3的链上),要么不接在1的后面(自己为根),形成链1-8和3-6-10
解剖开来就长这样(方法不唯一)

我们惊奇的发现:诶?这不就是放置魔术球的方案吗!
对啦!把原先的图解剖开来,最后得到的即是方案
像图中的点12,因为最多有4个柱子,所以它被排除在外面了
所以答案数是11

接下来考虑如何实现
首先绝对不能直接枚举分解树(废话)
我们发现对于所有先前的点而言,它一定能够被链的根节点连起来
换言之,我们能够走到现在这一步,则说明先前的k-1个点的方案是合法的,能被解剖成小于等于n条链
对于每个点而言,它要么是一条链的根,要么要被一个点指向
所以被其他点指向的点的个数=k-tot
其中tot为需要的柱子个数
也就是说,如果一个方案中被指向的点的个数=k-tot则这种方案是合法的

保证k-tot个点被指向?…这不就是流量吗!
我们对于每个点连接一条指向汇点的边
跑最大流后得到的流量若==k-tot,则这种答案是合法的
否则就多加一个柱子
答案呼之欲出!
从原点向每一个点连接一条流量为1的边,表示每个点都能与先前点结合或作为一条链的根
再从每个点向大于它且编号相加为完全平方数的的点连接一条边

等下
好像有点不对劲
每个点都直接与源点和汇点相连了
那它直接从源点流向汇点怎么办
那就把每个点分成两份
其他的点连向这个点的副本,副本连向汇点即可
为什么正确呢?
因为我们原先就保证了每个点都会被指向或作为根
所以连接向它的副本不会影响到它的答案,因为它迟早都是要被选的
每次加点,连边,跑最大流,若被指向的少了则tot++,知道tot>n
最后统计答案即可

#include<bits/stdc++.h>
using namespace std;
const int Max=5e5,inf=0x7fffffff;
int a,b,n,m,s,t;
int ans=0;
struct edge{
	int to,next,w,pre;
	edge()
	{
	    to=-1;
	    next=0;
	    w=0;
	    pre=0;
	}
}p[Max];
int head[Max],last[Max],idx=0,dep[Max],Now[Max],h[Max];
void add(int u,int v,int w)
{
	if(head[u])
		p[last[u]].next=++idx;
	else
		head[u]=++idx;
	last[u]=idx;
	p[idx].to=v;
	p[idx].w=w;
	p[idx].pre=idx+1;
	
	if(head[v])
		p[last[v]].next=++idx;
	else
		head[v]=++idx;
	last[v]=idx;
	p[idx].to=u;
	p[idx].w=0;
	p[idx].pre=idx-1;
}
void bfs(int now)
{
	for(int i=0;i<=now*2+1;i++){
		dep[i]=0;
		Now[i]=head[i];
	}
	dep[s]=1;
	queue<int> in;
	in.push(s);
	while(!in.empty())
	{
		int now=in.front();
		in.pop();
		for(int i=head[now];p[i].to!=-1;i=p[i].next)
		{
			if(p[i].w==0 or dep[p[i].to])
				continue;
			dep[p[i].to]=dep[now]+1;
			in.push(p[i].to);
		}
	}
}
int dfs(int now,int infl)
{
	if(now==t)
	{
		ans+=infl;
		return infl;
	}
	int fin=0;
	for(int i=Now[now];p[i].to!=-1;i=p[i].next)
	{
		if(infl==0)
			break;
		Now[now]=i;
		if(p[i].w==0 or dep[p[i].to]!=dep[now]+1)
			continue;
		int tot=dfs(p[i].to,min(infl,p[i].w));
		if(tot)
		{
			fin+=tot;
			infl-=tot;
			p[i].w-=tot;
			p[p[i].pre].w+=tot;
		}
	}
	return fin;
}
int main()
{
	scanf("%d",&n);
	s=0,t=1;
	int now=0,tot=0,fl=0;
	while(tot<=n)
	{
	    fl=0;
	    now++;
	    add(s,now*2,1);
	    add(now*2+1,t,1);
	    for(int i=sqrt(now)+1;i*i<now*2;i++)
	    {
	        add((i*i-now)*2,now*2+1,1);
	    }
	    bfs(now);
	    int Gett=0;
	    while(Gett=dfs(s,inf))
	    {
	    	fl+=Gett;
	        bfs(now);
	    }
	    if(!fl)
	    {
	        h[++tot]=now;
	    }
	}
	printf("%d\n",now-1);
	for(int j=1;j<=n;j++)
	{
	    now=h[j]*2;
	    bool can=true;
	    while(true)
	    {
	       printf("%d ",now/2);
	       can=true;
	       for(int i=head[now];p[i].to!=-1;i=p[i].next)
	       {
	           if(p[i].w==0 and p[i].to!=s)
	           {
	               can=false;
	               now=p[i].to-1;
	               break;
	           }
	       }
	       if(can)
	           break;
	    }
        printf("\n");  
	}
}
  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值