小M发现,美食节共有n种不同的菜品。每次点餐,每个同学可以选择其中的一个菜品。总共有m个厨师来制作这些菜品。当所有的同学点餐结束后,菜品的制作任务就会分配给每个厨师。然后每个厨师就会同时开始做菜。厨师们会按照要求的顺序进行制作,并且每次只能制作一人份。此外,小M还发现了另一件有意思的事情:
虽然这m个厨师都会制作全部的n种菜品,但对于同一菜品,不同厨师的制作时间未必相同。他将菜品用1, 2, ..., n依次编号,厨师用1, 2, ..., m依次编号,将第j个厨师制作第i种菜品的时间记为 ti,j 。小M认为:每个同学的等待时间为所有厨师开始做菜起,到自己那份菜品完成为止的时间总长度。换句话说,如果一个同学点的菜是某个厨师做的第k道菜,则他的等待时间就是这个厨师制作前k道菜的时间之和。而总等待时间为所有同学的等待时间之和。现在,小M找到了所有同学的点菜信息: 有 pi 个同学点了第i种菜品(i=1,
2, ..., n)。他想知道的是最小的总等待时间是多少。
动态加边,没写过。。。
本来应该一个厨师拆成的每个点都与不同的菜连容量为1,费用为t[i][j]×k 的边(k表示倒数第几个做,因为倒数第一做只有这个人需要等,做这个菜的时间不会其他菜) ,但是我们发现其实这些边最后一定有很多是不会走到的。
所以刚开始,把所有菜和每一个厨师倒数第一个点相连
在增广过程中,在增光路上找到和原点相连的点,并计算出他是哪一个厨师做的倒数第几个菜,然后把所有菜向这个厨师下一个菜加边
如果不动态加边就会TLE,但是本题的关键还是建图是很经典的
#include<cstdio>
#include<cstring>
#include<cmath>
#include<algorithm>
#include<cstdlib>
#include<queue>
using namespace std;
const int N=100005;
const int inf=0x3f3f3f3f;
int s,t,n,m,p[50],total;
int head[N],dis[N],pre[N],now[N],tot,id[105][805],cost[105][105],h[1005];
bool b[N];
struct aa
{
int flow,cap,to,pre,w;
}edge[N*100];
void addedge(int x,int y,int z,int w)
{
edge[++tot].to=y;edge[tot].cap=z;edge[tot].w=w;edge[tot].pre=head[x];head[x]=tot;
edge[++tot].to=x;edge[tot].cap=0;edge[tot].w=-w;edge[tot].pre=head[y];head[y]=tot;
}
bool spfa()
{
memset(dis,inf,sizeof(dis));
memset(b,false,sizeof(b));
dis[s]=0;queue<int> q;q.push(s);
while (!q.empty())
{
int u=q.front();q.pop();
for (int i=head[u];i;i=edge[i].pre)
if (edge[i].cap>edge[i].flow&&dis[edge[i].to]>dis[u]+edge[i].w)
{
dis[edge[i].to]=dis[u]+edge[i].w;
pre[edge[i].to]=u;now[edge[i].to]=i;
if (!b[edge[i].to])
{
b[edge[i].to]=true;
q.push(edge[i].to);
}
}
b[u]=false;
}
return dis[t]!=inf;
}
int work()
{
int ans=0,a,b,y;
while (spfa())
{
int flow=inf;
for (int i=t;i!=s;i=pre[i])
{
flow=min(flow,edge[now[i]].cap-edge[now[i]].flow);
if (pre[i]==s) y=i;
}
ans+=dis[t]*flow;
for (int i=t;i!=s;i=pre[i])
edge[now[i]].flow+=flow,edge[((now[i]-1)^1)+1].flow-=flow;
a=(y-n-1)/total+1;b=(y-n)%total+1;
for (int i=1;i<=n;i++)
addedge(id[a][b],i,1,b*cost[a][i]);
}
return ans;
}
int main()
{
scanf("%d%d",&n,&m);
int x,bj=n;
s=0;
for (int i=1;i<=n;i++) scanf("%d",&h[i]),total+=h[i];
for (int i=1;i<=m;i++)
for (int j=1;j<=total;j++) id[i][j]=++bj,addedge(s,id[i][j],1,0);
t=bj+1;
for (int i=1;i<=n;i++) addedge(i,t,h[i],0);
for (int i=1;i<=n;i++)
for (int j=1;j<=m;j++)
{
scanf("%d",&cost[j][i]);
addedge(id[j][1],i,1,cost[j][i]);
}
printf("%d",work());
return 0;
}
总结
1:逆向思维并不简单,但是很多时候思维的转化尤其重要。这道题我觉得最为巧妙的是,厨师做的第几个菜进行分点,但是如果由此进行连边,每一个菜和厨师做的第几个菜连边分别连边的话,我们无法计算最小的费用(等待时间),因为我们无法确定之前做的菜是哪些?这里就引出了本题中一种非常关键的转化:第k个菜的等待时间是之前所有等待时间之和,所求的总时间就是所有等待时间之和,我们这样想,来想每一个点对于总时间的贡献,最后一个菜的贡献是time*1.,倒数第二个菜:time*2,……所以每一个厨师做的菜的时间就可以通过这种方式转化,每个点都与不同的菜连容量为1,费用为t[i][j]×k 的边(这里的k其实表示的倒数第k个菜)。因为肯定是先连k小的点,如果再来一个点就会占两道菜了,因为有增广路所以可以保证会反悔,新的两个菜会重新排顺序,使费用最小