[SCOI2007]修车 & [NOI2012]美食节 Solution

题意:有 n n n辆车待修,有 m m m个修车师傅,他们修相同的车所需的时间可能不同,问修车师傅修完所有车的总时间。
可以把修车师傅分成 n n n个点,这样会得到一个 m × n m\times n m×n的点阵, ( i , j ) (i,j) (i,j)这个点表示师傅 i i i倒数第 j j j辆修的车是啥。
倒过来设置是因为计算贡献方便,倒数第 j j j辆修的车会让 j j j后面的车都等上更长的时间。设修理这辆车的时间为 t t t,这条边的权值就是 j × t j\times t j×t
c o d e : code: code:

#include <bits/stdc++.h>
#define regi register int
int n,m;
int S,T;
int ans;
int mincost;
int vis[100100];
int dis[100100];
std::queue<int>q;
int a[1000][1000];
int head[100100],tot=1;
struct edge{
	int to;
	int nxt;
	int flow;
	int cost;
}e[100000];
void add(int x,int y,int flow,int cost){
	e[++tot]={y,head[x],flow,cost};
	head[x]=tot;
	e[++tot]={x,head[y],0,-cost};
	head[y]=tot;
}
bool spfa(){
	for(regi i=1;i<=T*3;++i){
	  dis[i]=0x3f3f3f3f;
	  vis[i]=0;
	}
	q.push(S);
	dis[S]=0;
	while(!q.empty()){
		int x=q.front();
		q.pop();
		vis[x]=0;
		for(regi i=head[x];i;i=e[i].nxt){
			regi y=e[i].to;
			if(e[i].flow&&dis[y]>dis[x]+e[i].cost){
				dis[y]=dis[x]+e[i].cost;
				if(!vis[y]){
					vis[y]=1;
					q.push(y);
				}
			}
		}
	}
	return dis[T]!=0x3f3f3f3f;
}
int dfs(int x,int min){
	if(x==T){
		vis[T]=1;
		return min;
	}
	vis[x]=1;
	int flow=0;
	for(regi i=head[x],w;i;i=e[i].nxt){
		int y=e[i].to;
		if((!vis[y]||y==T)&&e[i].flow&&dis[y]==dis[x]+e[i].cost)
			if(w=dfs(y,std::min(min-flow,e[i].flow))>0){
				mincost+=w*e[i].cost;
				e[i].flow-=w;
				e[i^1].flow+=w;
				flow+=w;
			}
	}
	return flow;
}
void dinic(){
	while(spfa()){
		vis[T]=1;
		while(vis[T]){
			memset(vis,0,sizeof vis);
			dfs(S,0x3f3f3f3f);
		}
	}
}
main(){
	scanf("%d%d",&m,&n);
	S=n*m+n+1;
	T=S+1;
	for(regi i=1;i<=n;++i)
	  for(regi j=1;j<=m;++j)
	    scanf("%d",&a[i][j]);
	//a[i][j]表示第i辆车第j个人修的时间。
	for(regi i=1;i<=n;++i)
	  add(S,n*m+i,1,0);
	for(regi i=1;i<=m;++i)
	  for(regi j=1;j<=n;++j){
	  	for(regi k=1;k<=n;++k)
	     add(n*m+k,(i-1)*n+j,1,a[k][i]*j);
	    add((i-1)*n+j,T,1,0);
	  }
	//连边。 
	dinic();
	printf("%.2lf\n",mincost*1.0/n);
	//输出总时间/n即可。 
	return 0;
}

那么考虑一下美食节这题的区别,无非就是无法一下子建出所有的边,一个厨师如果还没有烧倒数第一道菜,就不可能去烧倒数第二道菜,所以现在一下子建出所有的边是不优的,可以在一个厨师做完菜之后再扩点继续跑费用流。
c o d e : code: code:

//首先把所有厨师倒数第一次做什么菜安排了
//之后跑一边
//然后把使用的厨师扩点 
#include <bits/stdc++.h>
#define regi register int
int n,m;
int ans;
int cnt,tot=1;
int S=1,T=2;
int ok[50];
int need[50];
int use[110];
int dis[1010000];
int vis[1010000];
int head[1010000];
int flow[1010000];
int last[1010000];
//last表示流向这个点的边是哪条 
int cook[50][110];
int belone[1000];
struct edge{
	int to;
	int nxt;
	int flow;
	int cost;
}e[1010000];
std::queue<int>q;
void add(int x,int y,int flow,int cost){
	e[++tot]={y,head[x],flow,cost};
	head[x]=tot;
	e[++tot]={x,head[y],0,-cost};
	head[y]=tot;
}
bool spfa(){
	for(int i=1;i<=cnt;++i){
	  dis[i]=0x3f3f3f3f;
	  vis[i]=0;
	  flow[i]=0x3f3f3f3f;
	  last[i]=0;
	}
	q.push(S);
	vis[S]=1;
	dis[S]=0;
	while(!q.empty()){
	  int X=q.front();
		q.pop();
		vis[X]=0;
		for(regi i=head[X];i;i=e[i].nxt){
			regi y=e[i].to;
			if(e[i].flow&&dis[y]>dis[X]+e[i].cost){
				dis[y]=dis[X]+e[i].cost;
				last[y]=i;
				flow[y]=std::min(flow[X],e[i].flow);
				if(!vis[y]){
				  vis[y]=1;
				  q.push(y);
				}
			}
		}
	}
	return dis[T]!=0x3f3f3f3f;
}
void dinic(){
	while(spfa()){
		for(regi x=T;last[x];x=e[last[x]^1].to){
			e[last[x]].flow-=flow[T];
			e[last[x]^1].flow+=flow[T];
			ans+=flow[T]*e[last[x]].cost;
		}
		int X=belone[e[last[T]^1].to];
		//找到那个使用的厨师
		belone[++cnt]=X;
		add(cnt,T,1,0);
		for(regi i=1;i<=n;++i)
		  add(i+2,cnt,1,(use[X]+1)*cook[i][X]);
		++use[X];
	}
}
main(){
	scanf("%d%d",&n,&m);
	for(regi i=1;i<=n;++i){
	  scanf("%d",&need[i]);
	  add(S,i+2,need[i],0);
	}
	for(regi i=1;i<=n;++i)
	  for(regi j=1;j<=m;++j)
	    scanf("%d",&cook[i][j]);
	cnt=n+2;
	for(regi i=1;i<=m;++i){
		belone[++cnt]=i;
		add(cnt,T,1,0);
		for(regi j=1;j<=n;++j)
			add(j+2,cnt,1,(use[i]+1)*cook[j][i]);
		++use[i];
	}
	dinic();
	printf("%d\n",ans);
  return 0;
}
/*代码实现:
开一个数组记录每个厨师现在是在做他的倒数第几碗菜。这个是为了方便之后的连边 
记录一下这个厨师分出来的这个点对应源点连向的哪个点,这样可以确定这次是哪个厨师做了菜 
其实这张图就四层,去了源点和汇点就两层,所以寻找那个流过的点十分简单,让终点往前一步就能找到,然后造出这个点的下一个点。
*/

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值