把博客园图标替换成自己的图标
把博客园图标替换成自己的图标end

【BZOJ2324】[ZJOI2011] 营救皮卡丘(费用流)

点此看题面

大致题意: 一张无向图,节点编号\(0\sim n\)。在\(0\)号点有\(k\)个人,要求有人经过了\(1\sim i-1\)的所有节点,才能经过节点\(i\)。求经过全部\(n\)个节点\(k\)个人走过的最小路径总长。

网络流

又是无向图,又是这么稀奇古怪的限制,这道题怎么看都不像是网络流啊。。。(不对,好像稀奇古怪的限制正是网络流的精神所在

实际上,我们考虑对于某一个人,由TA首次经过的节点一定是一个递增序列。(是不是一下就有了方向?)

而且,所有人首次经过的节点一定是不重复的(废话,不然还叫首次经过。。。),且所有人首次经过的节点拼在一起一定是\(1\sim n\)的所有节点(这应该也显然)。

考虑一个人当前首次经过了节点\(x\),接下来TA需要首次经过节点\(y\)

因为在这道题中,一个人可以保持不动,而且题目也没有限制时间。所以,我们完全可以让TA傻站在那里,一直等到节点\(x+1\sim y-1\)都被其他人首次经过。

好,然后这个人就开始往节点\(y\)进军了。TA怎样走才是最优的呢?当然是走最短路啦!

而此时节点\(y+1\sim n\)依然不能行走,也就是说,TA只能经过节点\(1\sim y\),从节点\(x\)走最短路到达节点\(y\)

令这个最短路为\(f_{x,y}\),则它显然是可以用\(Floyd\)搞出来的。

总结一下,我们对于每个点\(x\),向比它更大的点\(y\)连一条流量为\(1\),费用为\(f_{x,y}\)的边,就把这个问题转化成网络流了。

更具体的建图

刚才我们已经确定了点与点之间的连边,现在我们要进一步讨论。

显然根据先前提到的性质,每个点只能经过一次(不重复),且至少要经过一次(拼起来是\(1\sim n\))。

按照套路,把每个点拆成入点和出点,那么就相当于从入点向出点连一条上下界均为\(1\)的边。

而这可以转化为,建立超级源/超级汇,然后从超级源向出点连一条流量\(1\)的边从入点向超级汇连一条流量\(1\)的边

显然,之前点与点的连边就要变成出点与入点的连边

而对于节点\(0\)这个特殊点,我们实际上不需要把它分成两个(因为它并没有下界限制),直接从超级源向它连一条流量\(k\)的边就可以了。

具体实现详见代码。

代码

#include<bits/stdc++.h>
#define Tp template<typename Ty>
#define Ts template<typename Ty,typename... Ar>
#define Reg register
#define RI Reg int
#define Con const
#define CI Con int&
#define I inline
#define W while
#define N 150
#define M 20000
#define INF (int)1e9
using namespace std;
int n,m,t,f[N+5][N+5];
class MinCostMaxlow//最小费用最大流
{
	private:
		#define E(x) ((((x)-1)^1)+1)
		int ee,lnk[2*N+5];struct edge {int to,nxt,F,C;}e[2*M+5];
		int lst[2*N+5],IQ[2*N+5],F[2*N+5],C[2*N+5];queue<int> q;
		I bool SPFA()
		{
			RI i,k;for(i=0;i<=2*n+2;++i) F[i]=C[i]=INF;q.push(S),C[S]=0;
			W(!q.empty()) for(i=lnk[k=q.front()],q.pop(),IQ[k]=0;i;i=e[i].nxt)
			{
				if(!e[i].F||C[k]+e[i].C>=C[e[i].to]) continue;
				C[e[i].to]=C[k]+e[lst[e[i].to]=i].C,F[e[i].to]=min(F[k],e[i].F),
				!IQ[e[i].to]&&(q.push(e[i].to),IQ[e[i].to]=1);
			}return F[T]^INF;
		}
	public:
		I void Add(CI x,CI y,CI f,CI c)
		{
			e[++ee].nxt=lnk[x],e[lnk[x]=ee].to=y,e[ee].F=f,e[ee].C=c,
			e[++ee].nxt=lnk[y],e[lnk[y]=ee].to=x,e[ee].F=0,e[ee].C=-c;
		}
		int S,T;I void MCMF()
		{
			RI x,res=0;W(SPFA())
			{
				res+=F[T]*C[T],x=T;
				W(x^S) e[lst[x]].F-=F[T],e[E(lst[x])].F+=F[T],x=e[E(lst[x])].to;
			}printf("%d",res);
		}
}F;
int main()
{
	RI i,j,k,x,y,z;scanf("%d%d%d",&n,&m,&t);
	for(i=0;i<=n;++i) for(j=0;j<=n;++j) f[i][j]=INF;for(i=0;i<=n;++i) f[i][i]=0;//初始化
	for(i=1;i<=m;++i) scanf("%d%d%d",&x,&y,&z),f[x][y]>z&&(f[x][y]=f[y][x]=z);//对于给定边修改距离
	for(k=0;k<=n;++k) for(i=0;i<=n;++i) for(j=0;j<=n;++j)//Floyd
		(i>k||j>k)&&f[i][k]+f[k][j]<f[i][j]&&(f[i][j]=f[i][k]+f[k][j]);//注意不能经过编号更大的点
	for(i=0;i<=n;++i) for(j=i+1;j<=n;++j) F.Add(i?n+i:0,j,1,f[i][j]);//出点与入点间的连边
	F.Add(F.S=2*n+1,0,t,0),F.T=2*n+2;//从超级源向0连边
	for(i=1;i<=n;++i) F.Add(i,F.T,1,0),F.Add(F.S,n+i,1,0);//1~n节点和超级源汇的连边
	return F.MCMF(),0;
}
posted @ 2020-05-21 12:00  TheLostWeak  阅读(115)  评论(0编辑  收藏  举报