ZRX的网络流题目总结

PARTI 最大流

      例1:bzoj 3931

Description

路由是指通过计算机网络把信息从源地址传输到目的地址的活动,也是计算机网络设计中的重点和难点。网络中实现路由发的硬件设备称为路由器。为了使数据包最快的到达目的地,路由器需要选择最优的路径转发数据包。例如在常用的路由算法OSPF(开放式最短路径优先)中,路由器会使用经典的Dijkstra算法计算最短路径,然后尽量沿最短路径转发数据包。现在,若已知一个计算机网络中各路由器间的连接情况,以及各个路由器的最大吞吐量(即每秒能转发的数据包数量),假设所有数据包一定沿最短路径转发,试计算从路由器1到路由器n的网络的最大吞吐量。计算中忽略转发及传输的时间开销,不考虑链路的带宽限制,即认为数据包可以瞬间通过网络。路由器1到路由器n作为起点和终点,自身的吞吐量不用考虑,网络上也不存在将1和n直接相连的链路。

     看过印象最深的一句话:网络流从源点到汇点的一条流代表了一条合法路径。对于本题,我们可以考虑先建出原图的最小路径图,(注意,建最小路径图的时候,可以考虑把每条边看作两条有向边,是否在某一条最短路上,即为判断一个端点到起点+边长+另一个端点到终点的距离是否最短路长,如果满足,则它在路径上。而不能通过判断点是否在最短路上,连接两个在最短路上的点边)。

      接着又因为每个点有容量,就很容易想到把每个点i 拆开作为两个点i 和 i',两个之间连一条容量为点权的边。接着对于每条最短路径图上的边,从i'->j连一条容量为inf的边即可。

     (500跑最短路 floyd多好,为什么要用spfa和dijikstra)

      下附AC还是bzoj最慢的代码。

#include<iostream>
#include<stdio.h>
#include<string.h>
#include<algorithm>
#define maxn 2005
#define inf (1e18)
using namespace std;
typedef long long ll;
ll n,m,tot=1;
ll u[maxn*maxn],v[maxn*maxn];
ll w[maxn*maxn];
ll dis[maxn][maxn];
ll head[maxn*maxn],nex[maxn*maxn],to[maxn*maxn];
ll cap[maxn*maxn];
// 把每个点拆乘i i+n
void add(ll x,ll y,ll z)
{
	to[++tot]=y; cap[tot]=z; nex[tot]=head[x]; head[x]=tot;
	to[++tot]=x; cap[tot]=z; nex[tot]=head[y]; head[y]=tot;
}
ll iter[maxn*maxn],level[maxn*maxn],q[maxn*maxn];
void bfs(ll now)
{
	for(ll i=1;i<=n;i++)
		level[i]=0;
	ll h=0,t=-1; q[++t]=now; level[now]=1;
	while(h<=t)
	{
		now=q[h]; h++;
//		cout<<now<<endl;
		for(ll i=head[now];i;i=nex[i])
		{
			if(!level[to[i]] && cap[i]>0)
			{
				level[to[i]]=level[now]+1;
				q[++t]=to[i];
			}
		}
	}
}
ll dfs(ll x,ll y,ll f)
{
	if(x==y || !f) return f;
	for(ll &i=iter[x];i;i=nex[i])
	{
		if(cap[i]>0 && level[to[i]]==level[x]+1)
		{
			ll res=dfs(to[i],y,min(f,cap[i]));
			if(res>0)
			{
				cap[i]-=res;
				cap[i^1]+=res;
				return res;
			}
		}
	}
	return 0;
}
ll dinic(ll x,ll y)
{
	ll flow=0;
	n+=n;
	while(1)
	{
		bfs(x);
		if(!level[y]) return flow;
//		cerr<<"=1"<<endl;
		for(ll i=1;i<=n;i++)
			iter[i]=head[i];
		ll f;
		while((f=dfs(x,y,inf))) 
			flow+=f;
//		cerr<<"its "<<endl;
//		cerr<<flow<<endl;
	}
}
int main()
{
	scanf("%lld%lld",&n,&m);
	for(ll i=0;i<=n;i++)
	{
//		dis[i][i]=0;
		for(ll j=0;j<=n;j++)
			dis[i][j]=inf;
		dis[i][i]=0;
	}
//	cerr<<"this "<<dis[1][1]<<endl;
	for(ll i=1;i<=m;i++)
	{
		scanf("%lld%lld%lld",&u[i],&v[i],&w[i]);
		long long temp=min(dis[v[i]][u[i]],w[i]);
		//dis[u[i]][v[i]]=dis[v[i]][u[i]]=min(dis[v[i]][u[i]],w[i]);
		dis[u[i]][v[i]]=dis[v[i]][u[i]]=temp;
	}
//	cerr<<"gg1 "<<dis[1][1]<<endl;
	for(ll k=1;k<=n;k++)
	{
		for(ll i=1;i<=n;i++)
		{
			for(ll j=1;j<=n;j++)
			if(i!=j && j!=k && k!=i)
			{
				dis[i][j]=min(dis[i][j],dis[i][k]+dis[k][j]);
			}
		}
	}
//	cout<<"gg "<<dis[1][1]<<endl;
//	cerr<<endl;
//	for(int i=1;i<=n;i++)
//	cerr<<"its "<<dis[1][i]<<endl;
	for(ll i=1;i<=m;i++)
	{
		ll x=u[i],y=v[i];
		ll z=w[i];
		if(dis[1][x]>dis[1][y]) swap(x,y);
//		cout<<x<<" "<<y<<" "<<dis[1][x]<<" "<<dis[1][y]<<endl;
		if(dis[1][x]+z+dis[n][y]==dis[1][n])
		{
//			cout<<x<<" "<<y<<endl;
			add(x+n,y,1e9);
//			add(y+n,x,inf);
		}
		if(dis[1][y]>dis[1][x]) swap(x,y);
		if(dis[1][y]+z+dis[n][x]==dis[1][n])
		{
			add(y+n,x,1e9);
		}
	}
	for(ll i=1;i<=n;i++)
	{
		ll x;
		scanf("%lld",&x);
		add(i,i+n,x);
	}
//	cerr<<"its "<<dis[1][n]<<endl;
//	cerr<<tot<<endl;
	ll ans=dinic(n+1,n);
	printf("%lld\n",ans);
}

PART II 最小割

        bzoj 1565 植物大战僵尸

Description

Plants vs. Zombies(PVZ)是最近十分风靡的一款小游戏。Plants(植物)和Zombies(僵尸)是游戏的主角,其中Plants防守,而Zombies进攻。该款游戏包含多种不同的挑战系列,比如Protect Your Brain、Bowling等等。其中最为经典的,莫过于玩家通过控制Plants来防守Zombies的进攻,或者相反地由玩家通过控制Zombies对Plants发起进攻。现在,我们将要考虑的问题是游戏中Zombies对Plants的进攻,请注意,本题中规则与实际游戏有所不同。游戏中有两种角色,Plants和Zombies,每个Plant有一个攻击位置集合,它可以对这些位置进行保护;而Zombie进攻植物的方式是走到植物所在的位置上并将其吃掉。游戏的地图可以抽象为一个N行M列的矩阵,行从上到下用0到N–1编号,列从左到右用0到M–1编号;在地图的每个位置上都放有一个Plant,为简单起见,我们把位于第r行第c列的植物记为Pr, c。Plants分很多种,有攻击类、防守和经济类等等。为了简单的描述每个Plant,定义Score和Attack如下:Score[Pr, c]Zombie击溃植物Pr, c可获得的能源。若Score[Pr, c]为非负整数,则表示击溃植物Pr, c可获得能源Score[Pr, c],若为负数表示击溃Pr, c需要付出能源 -Score[Pr, c]。Attack[Pr, c]植物Pr, c能够对Zombie进行攻击的位置集合。Zombies必须从地图的右侧进入,且只能沿着水平方向进行移动。Zombies攻击植物的唯一方式就是走到该植物所在的位置并将植物吃掉。因此Zombies的进攻总是从地图的右侧开始。也就是说,对于第r行的进攻,Zombies必须首先攻击Pr, M-1;若需要对Pr, c(0 ≤ c < M-1)攻击,必须将Pr,M-1, Pr, M-2 … Pr, c+1先击溃,并移动到位置(r, c)才可进行攻击。在本题的设定中,Plants的攻击力是无穷大的,一旦Zombie进入某个Plant的攻击位置,该Zombie会被瞬间消灭,而该Zombie没有时间进行任何攻击操作。因此,即便Zombie进入了一个Plant所在的位置,但该位置属于其他植物的攻击位置集合,则Zombie会被瞬间消灭而所在位置的植物则安然无恙(在我们的设定中,Plant的攻击位置不包含自身所在位置,否则你就不可能击溃它了)。Zombies的目标是对Plants的阵地发起进攻并获得最大的能源收入。每一次,你可以选择一个可进攻的植物进行攻击。本题的目标为,制定一套ombies的进攻方案,选择进攻哪些植物以及进攻的顺序,从而获得最大的能源收入。

            

         很显然这个attack是一种依赖关系,a attack b 意味着 要打死b必须要打死a,那么我们通过一个图来显示就是 a到b有一条有向边,那么依赖关系就变成了一个图,要打某个节点需要所有它的入点全部被打过,那么如果依赖关系成了一个环,那么环上以及环连出的边都是不可能被选的,所以我们可以通过拓扑排序,来找到那些可以被打死的节点,由于拓扑排序如果有环就进行不下去了,所以自然的,环上以及环之后的点都不被选了。

         这样问题就转化成了,有一个点集,选某些点之前,必须选所有它依赖的点,选的收益有正有负,求最大收益。这就是经典问题最大权闭合子图了,我们将正权点i与s连接,从s 到 i 连一条为正权的边,再将负权点j与t连接,从j 到 t 连一条为负权的绝对值的边,再将所有依赖关系如 i选必须选j,连一条i到j 流量为inf的边即可。 注意在拓扑排序前,我们建的边和这里的是相反的,所以如果有边要添加的话,记得反向。

       下附AC代码。

#include<iostream>
#include<stdio.h>
#include<string.h>
#include<algorithm>
#include<vector>
#define maxn 1005
#define inf (1e18)
using namespace std;
typedef long long ll;
vector<int>edge[maxn];
ll n,m,s1,t1,tot=1;
ll w[maxn*maxn];
ll head[maxn*maxn],nex[maxn*maxn],to[maxn*maxn],cap[maxn*maxn];
int pos(int i,int j)
{
	return (i-1)*(int)m+j;
}
void add(ll x,ll y,ll z)
{
	to[++tot]=y; cap[tot]=z; nex[tot]=head[x]; head[x]=tot;
	to[++tot]=x; cap[tot]=0; nex[tot]=head[y]; head[y]=tot;
}
ll iter[maxn*maxn],level[maxn*maxn],q[maxn*maxn],in[maxn];
void bfs(ll now)
{
	for(ll i=1;i<=n;i++)
		level[i]=0;
	ll h=0,t=-1; q[++t]=now; level[now]=1;
	while(h<=t)
	{
		now=q[h]; h++;
		for(ll i=head[now];i;i=nex[i])
		{
			if(!level[to[i]] && cap[i]>0)
			{
				level[to[i]]=level[now]+1;
				q[++t]=to[i];
			}
		}
	}
}
ll dfs(ll x,ll y,ll f)
{
	if(x==y || !f) return f;
	for(ll &i=iter[x];i;i=nex[i])
	{
		if(cap[i]>0 && level[to[i]]==level[x]+1)
		{
//			cerr<<x<<" "<<y<<endl;
			ll res=dfs(to[i],y,min(f,cap[i]));
			if(res>0)
			{
				cap[i]-=res;
				cap[i^1]+=res;
				return res;
			}
		}
	}
	return 0;
}
ll dinic(ll x,ll y)
{
	ll flow=0;
	n=t1;
	while(1)
	{
		bfs(x);
		if(!level[y]) return flow;
		for(ll i=1;i<=n;i++)
			iter[i]=head[i];
		ll f;
		while((f=dfs(x,y,inf))) 
			flow+=f;
//		cerr<<flow<<endl;
	}
}
int deg[maxn];
int main()
{
	scanf("%lld%lld",&n,&m);
	for(int i=1;i<=n;i++)
	{
		int num;
		for(int j=1;j<=m;j++)
		{	
			scanf("%lld%d",&w[pos(i,j)],&num);
			for(int k=1;k<=num;k++)
			{
				int x,y;
				scanf("%d%d",&x,&y);
				x++; y++;
				edge[pos(i,j)].push_back(pos(x,y));
				deg[pos(x,y)]++;
//				add(pos(i,j),pos(x,y),)
			}
		}
	}
//	cerr<<"+1"<<endl;
	for(int i=1;i<=n;i++)
	{
		for(int j=1;j<=m-1;j++)
		{
			edge[pos(i,j+1)].push_back(pos(i,j));
			deg[pos(i,j)]++;
		}
	}
	int h=0,t=-1;
	for(int i=1;i<=n;i++)
		for(int j=1;j<=m;j++)
			if(!deg[pos(i,j)])
				q[++t]=pos(i,j);
	while(h<=t)
	{
		int now=q[h]; h++;
//		cout<<now<<endl;
		for(int j=0;j<edge[now].size();j++)
		{
			int nex=edge[now][j];
			deg[nex]--;
			if(!deg[nex])
				q[++t]=nex;
		}
	}

	ll ans=0; s1=n*m+1; t1=n*m+2;
	for(int i=1;i<=n;i++)
		for(int j=1;j<=m;j++)
			if(!deg[pos(i,j)])
			{
//				cerr<<pos(i,j)<<endl;
				if(w[pos(i,j)]>=0)
				{
					ans+=w[pos(i,j)];
					add(s1,pos(i,j),w[pos(i,j)]);
				}
				else
				{
					add(pos(i,j),t1,-w[pos(i,j)]);
				}
				for(int k=0;k<edge[pos(i,j)].size();k++)
				{
					int nex=edge[pos(i,j)][k];
					if(!deg[nex])
					{
						add(nex,pos(i,j),inf);
					}
				}
			}
//	cerr<<ans<<endl;
	ll temp=dinic(s1,t1);
//	cerr<<temp<<endl;
	printf("%lld\n",max(0ll,ans-temp));
}

PART III  费用流

        例一:bzoj2673

Description

有一个芯片,芯片上有N*N(1≤N≤40)个插槽,可以在里面装零件。

有些插槽不能装零件,有些插槽必须装零件,剩下的插槽随意。

要求装好之后满足如下两条要求:

1、第 i 行和第 i 列的零件数目必须一样多(1≤i≤N)。

2、第 i 行的零件数目不能超过总的零件数目的 A/B(1≤i≤N,0≤A≤B≤1000,B≠0)。

求最多可以另外放多少个零件(就是除掉必须放的)。如果无解输出impossible。


       这道题真是妙啊。(注意是要求最大费用最大流,spfa的时候要改一下哦)

       首先我们并不知道这一行最大值是多少。但是我会枚举!我们可以从0-n枚举这一行填的最大值是x,如果我们放出来最多的零件设为b ,它a/b的比例都不满足小于等于A/B,那其他情况b更小,就更不可能满足了。如果在图上体现一行最多放x个呢,那么就是对于每个行i,从s向它连一条边,容量为x,对于每个列j,向t连一条边,容量也为x。

       接着如果一个点可以放芯片,那么就从i到j连一条代价为1,流量为1的边,如果这个点本来就是芯片,我们有一个骚操作,将它的代价设置为10000+1,因为10000>n*n,而且我们要求的是最大费用最大流,所以这个是肯定要选的,最后结果ans出来我们用ans/10000就知道了这种必须选的选了多少个,如果不够本来就有芯片点的个数,则说明该状态非法了。

      最后就是很妙的如何处理第i行和第j列的个数要求相等,我们只需要从第i行向第i列自己,连一条容量为inf,代价为0的边即可。这时与源点汇点连接的边都满流了。 设当前枚举的最大值为x,那么如果行i,向除了第i列以外的点,放了y个芯片,那么从源点到i的流量还剩x-y,那么这个流只能从我们开始连的第i行向第i列连的边走过去,使得第i列到汇点的边有了x-y的流量,可是这样的话,第i列向源点的连边就会缺y的流量才能满流,这样就需要其他行向第i列的点来向他贡献y个流量,即要求放y个芯片到第i列。

       下附AC代码。


#include<iostream>
#include<stdio.h>
#include<string.h>
#include<algorithm>
#define maxn 205
using namespace std;
int n,m,s,t,tot=1,ans,sum,a,b;
char s1[maxn][maxn];
int head[maxn*maxn],nex[maxn*maxn],to[maxn*maxn],val[maxn*maxn],cap[maxn*maxn];
void add(int x,int y,int o,int z)
{
	to[++tot]=y; val[tot]=z;  cap[tot]=o; nex[tot]=head[x]; head[x]=tot; 
	to[++tot]=x; val[tot]=-z; cap[tot]=0; nex[tot]=head[y]; head[y]=tot;
}	
int dis[maxn*maxn],minn[maxn*maxn],vis[maxn*maxn],pre[maxn*maxn],q[maxn*maxn];
bool ek()
{
	for(int i=1;i<=t;i++)
		dis[i]=-1023456789,vis[i]=0,pre[i]=0,minn[i]=0;

	int h1=0,t1=-1;
	minn[s]=1023456789; minn[t]=0; 
	dis[s]=0;  q[++t1]=s; vis[s]=1;
	while(h1<=t1)
	{
		int now=q[h1]; h1++; vis[now]=0;
		for(int i=head[now];i;i=nex[i])
		{
			if(cap[i] && dis[to[i]]<dis[now]+val[i])
			{
				dis[to[i]]=dis[now]+val[i];
				minn[to[i]]=min(minn[now],cap[i]);
				pre[to[i]]=i;
				if(!vis[to[i]])
				{
					vis[to[i]]=1;
					q[++t1]=to[i];
				}
			}
		}
	}
	if(!minn[t]) return false;
	ans+=minn[t]*dis[t];
	for(int i=pre[t];i;i=pre[to[i^1]])
		cap[i]-=minn[t], cap[i^1]+=minn[t];
	return true;
}
int main()
{
	int kase=0;
	while(~scanf("%d%d%d",&n,&a,&b) && (n+a+b))
	{
		for(int i=1;i<=n;i++)
			scanf("%s",s1[i]+1);
		int cnt=0;
		for(int i=1;i<=n;i++)
			for(int j=1;j<=n;j++)
				if(s1[i][j]=='C')
					cnt++;
		s=n+n+1; t=s+1;
		int res=-1;
		for(int maxx=0;maxx<=n;maxx++)
		{
			ans=0;tot=1;
			for(int i=0;i<=t;i++) head[i]=0;
			for(int i=1;i<=n;i++)
				add(s,i,maxx,0),add(i+n,t,maxx,0),add(i,i+n,123456789,0);
			for(int i=1;i<=n;i++)
			{
				for(int j=1;j<=n;j++)
				if(s1[i][j]!='/')
				{
					add(i,j+n,1,(s1[i][j]=='C')?10001:1);
				}
			}
			while(ek());
			if(ans/10000!=cnt) continue;
//			cerr<<maxx<<" "<<ans%10000<<" "<<ans/10000<<endl;
			if(a*(ans%10000)/b>=maxx)
				res=max(res,ans%10000-cnt);
		}
		if(res==-1)
			printf("Case %d: impossible\n",++kase);
		else
			printf("Case %d: %d\n",++kase,res);

	}
}

          例二 bzoj2668

Description

有一个nm列的黑白棋盘,你每次可以交换两个相邻格子(相邻是指有公共边或公共顶点)中的棋子,最终达到目标状态。要求第i行第j列的格子只能参与mi,j次交换。

          我们可以把问题转化成我们可以移动黑子,每个点有限制被交换过多少次,求最小总移动总次数。

          由于移入和移出次数可能是不等的,所以我们可以把一个点拆成三个i,i',i'',那么i到i'的容量即为能够移入的次数,i'-i''的容量为能够移出的次数。

         所以我们将所有初始为黑色的点,让源点向它连容量为1,花费为0的边, 将所有终止为黑色的点,让它向源点连一条容量为1,花费为0的边,一个点移动到一个格子,一定要再移动出去(除了在这个点终止),所以,移入和移出的容量均为m[i][j]/2,如果开始是白色,最后是黑色,我移入之后就不用移出了,所以可以比移出量多一次,但是总量不能超过,即为移入量可以为m[i][j]-m[i][j]/2。如果开始是黑色,最后是白色,移出之后就不用移入了,则移除量可以为m[i][j]-m[i][j]/2。如果同色则移入移出量一样了。上述的花费均为0。

        最后再考虑移动,就是对于每个点的八个方向连边,容量为无穷,花费为1即可。

       最后求一个费用流就好啦。

#include<iostream>
#include<stdio.h>
#include<string.h>
#include<algorithm>
#define maxn 1005
using namespace std;
int n,m,s,t,tot=1,ans,sum;
char s1[maxn][maxn],s2[maxn][maxn],s3[maxn][maxn];
int head[maxn*maxn],nex[maxn*maxn],to[maxn*maxn],val[maxn*maxn],cap[maxn*maxn];
int fx[20]={0,0,0,1,1,1,-1,-1,-1};
int fy[20]={0,1,-1,1,0,-1,1,0,-1};
int pos(int i,int j)
{
    return (i-1)*m+j;
}
void add(int x,int y,int o,int z)
{
    to[++tot]=y; val[tot]=z;  cap[tot]=o; nex[tot]=head[x]; head[x]=tot; 
    to[++tot]=x; val[tot]=-z; cap[tot]=0; nex[tot]=head[y]; head[y]=tot;
}   
int dis[maxn*maxn],minn[maxn*maxn],vis[maxn*maxn],pre[maxn*maxn],q[maxn*maxn];
bool ek()
{
    for(int i=1;i<=t;i++)
        dis[i]=1023456789,vis[i]=0,pre[i]=0,minn[i]=0;
 
    int h1=0,t1=-1;
    minn[s]=1023456789; minn[t]=0; 
    dis[s]=0;  q[++t1]=s; vis[s]=1;
    while(h1<=t1)
    {
        int now=q[h1]; h1++; vis[now]=0;
        for(int i=head[now];i;i=nex[i])
        {
            if(cap[i] && dis[to[i]]>dis[now]+val[i])
            {
                dis[to[i]]=dis[now]+val[i];
                minn[to[i]]=min(minn[now],cap[i]);
                pre[to[i]]=i;
                if(!vis[to[i]])
                {
                    vis[to[i]]=1;
                    q[++t1]=to[i];
                }
            }
        }
    }
    if(!minn[t]) return false;
    ans+=minn[t]*dis[t];
    for(int i=pre[t];i;i=pre[to[i^1]])
        cap[i]-=minn[t], cap[i^1]+=minn[t];
    return true;
}
void build()
{
    for(int i=1;i<=n;i++)
    {
        for(int j=1;j<=m;j++)
        {
            if(s1[i][j]=='0' && s2[i][j]=='1')
            {
                sum++;
                add(s,pos(i,j)+n*m,1,0);
                add(pos(i,j),n*m+pos(i,j),(s3[i][j]-'0')/2,0);
                add(n*m+(pos(i,j)),n*m*2+pos(i,j),(s3[i][j]-'0'+1)/2,0);
            }
            else if(s1[i][j]=='1' && s2[i][j]=='0')
            {
                sum--;
                add(pos(i,j)+n*m,t,1,0);
                add(pos(i,j),n*m+pos(i,j),(s3[i][j]-'0'+1)/2,0);
                add(n*m+pos(i,j),n*m*2+pos(i,j),(s3[i][j]-'0')/2,0);
            }
            else
            {
                add(pos(i,j),n*m+pos(i,j),(s3[i][j]-'0')/2,0);
                add(n*m+(pos(i,j)),n*m*2+pos(i,j),(s3[i][j]-'0')/2,0);
            }
        }
    }
    for(int i=1;i<=n;i++)
    {
        for(int j=1;j<=m;j++)
        {
            for(int k=1;k<=8;k++)
            {
                int nx=i+fx[k],ny=j+fy[k];
                if(1<=nx && nx<=n && 1<=ny && ny<=m)
                {
                    add(pos(i,j)+n*m*2,pos(nx,ny),1023456789,1);
                }
            }
        }
    }
}
int main()
{
    scanf("%d%d",&n,&m);
    for(int i=1;i<=n;i++)
        scanf("%s",s1[i]+1);
    for(int i=1;i<=n;i++)
        scanf("%s",s2[i]+1);
    for(int i=1;i<=n;i++)
        scanf("%s",s3[i]+1);
    s=n*m*3+1; t=s+1;
    build();
    if(sum)
    {
        printf("-1\n");
        return 0;
    }
    while(ek());
    printf("%d\n",ans);
}



PARTIV 有上下界的网络流

        例一 bzoj 3698

Description

XWW是个影响力很大的人,他有很多的追随者。这些追随者都想要加入XWW教成为XWW的教徒。但是这并不容易,需要通过XWW的考核。
XWW给你出了这么一个难题:XWW给你一个N*N的正实数矩阵A,满足XWW性。
称一个N*N的矩阵满足XWW性当且仅当:(1)A[N][N]=0;(2)矩阵中每行的最后一个元素等于该行前N-1个数的和;(3)矩阵中每列的最后一个元素等于该列前N-1个数的和。
现在你要给A中的数进行取整操作(可以是上取整或者下取整),使得最后的A矩阵仍然满足XWW性。同时XWW还要求A中的元素之和尽量大。

       先是有一个结论吧,在上下界网络流的时候,如果对于一个点 i ,有x的流量从s流向它,有y的流量从它流向t,那么如果x-y>0,那么我们就只需要从 s 到 i 连一条容量为x-y的边, 繁殖我们只需要从 i 到 t 连一条 容量为 y-x 的边,正确性显然,也可以推广到其他网络流的建图上。还有就是,对于有上下界的网络流,假如有一条i->j的边我们建边的时候的方法就是,从s向j连一条流量为下界的边,从i到t的时候连一条流量为下界的边,i->j连一条为流量上界-流量下界的边即可。 注意,这里的s是要新建一个起点,t也是要新建一个汇点的,而不能用朴素建图时候的s,t。

        还是行与列分开处理,我们对于每一行,从s1(这里的s1 是我们正常建图的s1,并不是上面所说新开的s2,这个s2就是在处理流量差的时候用的)进入它的流量最小值就是这一行的sum即a[i][n]向下取整,最大值就是a[i][n]向上取整。对于每一列,它流出t的流量与行也同理。 接着考虑行与列的关系,第i行连第j列的边也与上面同理考虑,即最小值为a[i][j]向下取整,最大值为a[i][j]向上取整。(由于这是带上下界的网络流,所以我们需要从t1->s1连一条容量为inf的边,来方便下面的操作)

         对于有上下界的网络流,我们建刚刚的 s2,t2,就是要先跑 s2->t2的最大流,如果等于下界之和,就说明有合法解,接着再跑s1->t1的最大流,这样跑出来的最大流即为题目要求的最大值了,由于每个元素会在每一列末以及每一行末尾统计一次,所以我们要将答案*3。

         下附AC代码。

 
#include<iostream>
#include<stdio.h>
#include<string.h>
#include<algorithm>
#define maxn 405
#define inf (1e18)
using namespace std;
typedef long long ll;
ll n,m,s1,t1,s2,t2,tot=1;
double a[maxn][maxn];
ll head[maxn*maxn],nex[maxn*maxn],to[maxn*maxn];
ll cap[maxn*maxn];
void add(ll x,ll y,ll z)
{
    to[++tot]=y; cap[tot]=z; nex[tot]=head[x]; head[x]=tot;
    to[++tot]=x; cap[tot]=0; nex[tot]=head[y]; head[y]=tot;
}
ll iter[maxn*maxn],level[maxn*maxn],q[maxn*maxn],in[maxn];
void bfs(ll now)
{
    for(ll i=1;i<=n;i++)
        level[i]=0;
    ll h=0,t=-1; q[++t]=now; level[now]=1;
    while(h<=t)
    {
        now=q[h]; h++;
        for(ll i=head[now];i;i=nex[i])
        {
            if(!level[to[i]] && cap[i]>0)
            {
                level[to[i]]=level[now]+1;
                q[++t]=to[i];
            }
        }
    }
}
ll dfs(ll x,ll y,ll f)
{
    if(x==y || !f) return f;
    for(ll &i=iter[x];i;i=nex[i])
    {
        if(cap[i]>0 && level[to[i]]==level[x]+1)
        {
//          cout<<x<<" "<<to[i]<<" "<<cap[i]<<endl;
            ll res=dfs(to[i],y,min(f,cap[i]));
//          cerr<<x<<" "<<to[i]<<" "<<cap[i]<<" "<<res<<endl;
            if(res>0)
            {
                cap[i]-=res;
                cap[i^1]+=res;
                return res;
            }
        }
    }
    return 0;
}
ll dinic(ll x,ll y)
{
//  cerr<<"its "<<x<<" "<<y<<endl;
    ll flow=0;
    while(1)
    {
        bfs(x);
        if(!level[y]) return flow;
        for(ll i=1;i<=n;i++)
            iter[i]=head[i];
        ll f;
        while((f=dfs(x,y,inf))) 
            flow+=f;
//      cerr<<flow<<endl;
    }
}
ll sum=0;
void build()
{
    for(int i=1;i<n;i++)
    {
        if(a[i][n]!=(int)(a[i][n]))
        {
            add(s1,i,1);
        }
        in[s1]-=(int)(a[i][n]); in[i]+=(int)(a[i][n]);
    }
    for(int i=1;i<n;i++)
    {
        if(a[n][i]!=(int)(a[n][i]))
        {
            add(i+n,t1,1);
        }
        in[i+n]-=((int)(a[n][i])); in[t1]+=(int)(a[n][i]);
    }
    for(int i=1;i<n;i++)
    {
        for(int j=1;j<n;j++)
        {
            if((a[i][j]!=(int)(a[i][j])))
            {
                add(i,j+n,1);
            }
            in[i]-=(int)(a[i][j]); in[j+n]+=(int)(a[i][j]);
        }
    }
    for(int i=1;i<=t2;i++)
    {
        if(in[i]>0)
            sum+=in[i],add(s2,i,in[i]);
        else
            add(i,t2,-in[i]);
    }
    add(t1,s1,inf);
}
int main()
{
    scanf("%lld",&n); s1=2*n+1; t1=s1+1; s2=t1+1; t2=s2+1;
    for(int i=1;i<=n;i++)
        for(int j=1;j<=n;j++)
            scanf("%lf",&a[i][j]);
    build();
//  cerr<<"+1"<<endl;
    n=t2;
    if(dinic(s2,t2)!=sum)
    {
        printf("No\n");
        return 0;
    }
    printf("%lld\n",dinic(s1,t1)*3ll);
}

例二:bzoj3876

Description

【故事背景】
宅男JYY非常喜欢玩RPG游戏,比如仙剑,轩辕剑等等。不过JYY喜欢的并不是战斗场景,而是类似电视剧一般的充满恩怨情仇的剧情。这些游戏往往
都有很多的支线剧情,现在JYY想花费最少的时间看完所有的支线剧情。
【问题描述】
JYY现在所玩的RPG游戏中,一共有N个剧情点,由1到N编号,第i个剧情点可以根据JYY的不同的选择,而经过不同的支线剧情,前往Ki种不同的新的剧情点。当然如果为0,则说明i号剧情点是游戏的一个结局了。
JYY观看一个支线剧情需要一定的时间。JYY一开始处在1号剧情点,也就是游戏的开始。显然任何一个剧情点都是从1号剧情点可达的。此外,随着游戏的进行,剧情是不可逆的。所以游戏保证从任意剧情点出发,都不能再回到这个剧情点。由于JYY过度使用修改器,导致游戏的“存档”和“读档”功能损坏了,
所以JYY要想回到之前的剧情点,唯一的方法就是退出当前游戏,并开始新的游戏,也就是回到1号剧情点。JYY可以在任何时刻退出游戏并重新开始。不断开始新的游戏重复观看已经看过的剧情是很痛苦,JYY希望花费最少的时间,看完所有不同的支线剧情。


        第一题放一道难一点的题这道题就显得比较简单了,这道题就是每个边必须走一次,而且我们可以从每一个点随时回到源点,还有就是走每个边有一定的费用。所以就变成了找一条可行流,使得费用最小。所以直接按上面说的方法建图,因为对于这道题来说肯定存在可行流,所以我们只需要按上面说的方法把图建出来跑费用流即可啦。

        下附AC代码。

#include<iostream>
#include<stdio.h>
#include<string.h>
#include<algorithm>
#define maxn 200005
using namespace std;
int n,s,t,tot=1,ans;
int in[maxn],out[maxn];
int head[maxn],nex[maxn],to[maxn],val[maxn],cap[maxn];
void add(int x,int y,int o,int z)
{
    to[++tot]=y; val[tot]=z;  cap[tot]=o; nex[tot]=head[x]; head[x]=tot; 
    to[++tot]=x; val[tot]=-z; cap[tot]=0; nex[tot]=head[y]; head[y]=tot;
}   
int dis[maxn],dp[maxn],vis[maxn],pre[maxn],q[maxn];
bool ek()
{
    for(int i=s;i<=t;i++)
        dis[i]=1023456789,vis[i]=0,pre[i]=0,dp[i]=0;
 
    int h=0,tail=-1;
    dp[s]=1023456789; dp[tail]=0; 
    dis[s]=0;  q[++tail]=s; vis[s]=1;
    while(h<=tail)
    {
        int now=q[h]; h++; vis[now]=0;
//      cerr<<"its "<<h<<endl;
//      cerr<<"its "<<dis[now]<<endl;
        for(int i=head[now];i;i=nex[i])
        {
            if(cap[i] && dis[to[i]]>dis[now]+val[i])
            {
                dis[to[i]]=dis[now]+val[i];
                dp[to[i]]=min(dp[now],cap[i]);
                pre[to[i]]=i;
                if(!vis[to[i]])
                {
                    vis[to[i]]=1;
                    q[++tail]=to[i];
                }
            }
        }
    }
    if(!dp[t]) return false;
//  for(int i=s;i<=t;i++)
//      cout<<dp[i]<<' '<<dis[i]<<" "<<pre[i]<<endl;
//  cerr<<dp[t]<<" "<<dis[t]<<endl;
    ans+=dp[t]*dis[t];
    for(int i=pre[t];i;i=pre[to[i^1]])
    {
//      cerr<<to[i^1]<<endl;
        cap[i]-=dp[t]; cap[i^1]+=dp[t];
     
    }
//  cerr<<endl;
    return true;
}
int main()
{
    scanf("%d",&n);
    s=0; t=n+1;
    for(int i=1;i<=n;i++)
    {
        int x;
        scanf("%d",&x); out[i]=x;
        for(int j=1;j<=x;j++)
        {
            int y,z;
            scanf("%d%d",&y,&z);
            in[y]++;
            add(i,y,12345678,z);
//          add(s,y,1,z);
            ans+=z;
        }
//      add(i,t,x,0);
        if(i!=1)
            add(i,1,12345678,0);
    }
    for(int i=2;i<=n;i++)
    {
        if(in[i]>out[i])
            add(s,i,in[i]-out[i],0);
        else
            add(i,t,out[i]-in[i],0);
    }
//  cerr<<"+1"<<endl;
    while(ek());
    printf("%d\n",ans);
}


Especially For U 

By ZRX



评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值