[NOI2012]美食節——費用流(帶權二分圖匹配)+動態加邊
題目描述
小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)。他想知道的是最小的總等待時間是多少。
[數據範圍]
10 | n = 40 | m = 100 | p = 800 |
其中,$p=\sum_i^n p[i] $
題解
前置:[SCOI2007]修車
修車這個題目,可以把工人拆成n個階段,m*n個點。
工人j的階段i的意思是,正在維修,所有排在j這一隊的,加上這個汽車後面還有i個汽車的汽車。
即,如果(j,1)表示維修這一隊的最後一輛汽車。
汽車放在左部點,工人m*n個點放在右部點。
S向car連接流1費0,工人向T連接流1費0
作用:規定最大流為n。每個工人同一個階段只能維修一輛。
第i個車向階段為k的j個工人連接:流1費k*(tr[i][j])
作用:規定這個汽車只能被這個階段的工人修一次。如果把這個汽車放在後面還有k個情況下修,那麽總的等待時間會多出k*tr[i][j]
那麽現在,每一個增廣路,都代表一個汽車選擇了某個工人的某個位置。
並且不會選多,不會重選,不會選少。
直接費用流即可。
但是這個“美食節”就比較麻煩了。
(省選之於國賽。。。)
左部點,總的菜不止n了,但是可以左邊n個菜種,S到i的容量變成p[i]即可。
如果還像上面一樣暴力建邊的話,那麽,右部點一共有sum*m個。
然後暴力建立完全二分圖
那麽,邊數會比6e6還多。
親測會TLE成60分。
怎麽優化?
左部點不能少了。為了保證合法,右部點也沒法少了。
其實,真正卡SPFA一定是因為邊數太多了。
我們真的用得了這麽多邊麽?
顯然大部分都是沒用的。我們對著上限開了sum*m個點,真正匹配上的也就sum個點罷了。
對於這種問題我們處理地多了。
n個集合,數字一共m個。開n個vector,即動態數組。
需要n棵線段樹,一共修改n次。開n棵動態開點線段樹。
有什麽共同之處??
都叫“動態”
好,那我們就動態加邊!!
一共其實只會跑sum次SPFA
而求的是最小費用最大流,也就是求最短路。
所以,第一次一定是某個廚師在1階段做菜。然後,這個如果需要的話,這個廚師又會在2階段做菜。
所以,每個廚師很高的階段,會憑空浪費時間,而且dis太大,根本用不上。
而每次E-K是找到一條增廣路增廣。
所以,我們開始只要把每個菜種i向階段1的廚師連邊,階段1的廚師向T連邊即可。
然後,SPFA之後的upda,當處理到一個右部點(廚師j和階段k)時候,
把每個菜種i向這個廚師k+1階段連一條邊。
再把廚師j的k+1階段向T連接一條邊。
(註意一下向T連邊,不能單純只判斷是一個右部點,因為網絡流是可能回流反悔的。
所以,一條增廣路可能多次經過一個右部點。
而第一次經過的右部點才是這條增廣路的決策。
立一個flag即可。
)
代碼:
#include<bits/stdc++.h> using namespace std; typedef long long ll; const int N=80100; const int inf=0x3f3f3f3f; int n,m; int p[44],sum; struct node{ int nxt,to,w,c; }e[2*(224000+40+5)]; int hd[N],cnt=1; int tr[44][105]; void add(int x,int y,int w,int c){ e[++cnt].nxt=hd[x]; e[cnt].to=y;e[cnt].w=w,e[cnt].c=c; hd[x]=cnt; e[++cnt].nxt=hd[y]; e[cnt].to=x;e[cnt].w=0;e[cnt].c=-c; hd[y]=cnt; } int dis[N],pre[N],incf[N]; bool vis[N]; int s,t; queue<int>q; bool spfa(){ memset(dis,0x3f,sizeof dis); memset(vis,0,sizeof vis); while(!q.empty())q.pop(); dis[s]=0; pre[s]=0;incf[s]=inf; q.push(s); while(!q.empty()){ int x=q.front();q.pop(); vis[x]=0; for(int i=hd[x];i;i=e[i].nxt){ if(!e[i].w) continue; int y=e[i].to; if(dis[y]>dis[x]+e[i].c){ dis[y]=dis[x]+e[i].c; pre[y]=i; incf[y]=min(incf[x],e[i].w); if(!vis[y]){ vis[y]=1; q.push(y); } } } } if(dis[t]==inf) return false; return true; } int ans; void upda(){ int x=t; bool fl=false; while(x!=s){ //cout<<" xx "<<x<<" dis "<<dis[x]<<endl; if(!fl&&x>=1+n+1&&x<=1+n+sum*m){ int k=(x-n-1+m-1)/m; int num=(x-n-1-1)%m+1; //cout<<" kkk "<<k<<" "<<num<<endl; if(k<sum){ add(x+m,t,1,0); //add(x+m+m,t,1,0); for(int i=1;i<=n;i++){ add(i+1,x+m,1,(k+1)*tr[i][num]); } } fl=true; } e[pre[x]].w-=incf[t]; e[pre[x]^1].w+=incf[t]; x=e[pre[x]^1].to; } ans+=incf[t]*dis[t]; //cout<<" ----------------------after after "<<ans<<endl; } int getfood(int x){ return x+1; } int getchef(int p,int x){ return 1+n+(p-1)*m+x; } int main(){ scanf("%d%d",&n,&m); for(int i=1;i<=n;i++){ scanf("%d",&p[i]);sum+=p[i]; } s=1,t=1+n+sum*m+1; int tim; for(int i=1;i<=n;i++){ for(int j=1;j<=m;j++){ scanf("%d",&tim); tr[i][j]=tim; add(getfood(i),getchef(1,j),1,1*tim); } } for(int i=1;i<=n;i++){ add(s,getfood(i),p[i],0); } for(int i=1;i<=m;i++){ //for(int j=1;j<=sum;j++){ add(getchef(1,i),t,1,0); //} } while(spfa())upda(); printf("%d",ans); return 0; }
總結:
動態處理無處不在。
都借助了均攤或者總和的復雜度比較低的特點,達到節省空間和時間的目的。
刪除了許多並不會用到的節點或者是邊。
[NOI2012]美食節——費用流(帶權二分圖匹配)+動態加邊