三倍经验题!
********************题目略长**********************
题目描述
一个餐厅在相继的N天里,第i天需要ri块餐巾(i=l,2,…,N)。餐厅可以从三种途径获得餐巾。
(1)购买新的餐巾,每块需p分;
(2)把用过的餐巾送到快洗部,洗一块需m天,费用需f分(f<p)。如m=l时,第一天送到快洗部的餐巾第二天就可以使用了,送慢洗的情况也如此。
(3)把餐巾送到慢洗部,洗一块需n天(n>m),费用需s分(s<f)。
在每天结束时,餐厅必须决定多少块用过的餐巾送到快洗部,多少块送慢洗部。在每天开始时,餐厅必须决定是否购买新餐巾及多少,使洗好的和新购的餐巾之和满足当天的需求量Ri,并使N天总的费用最小
输入输出格式
输入格式:
输入文件共3行,第1行为总天数;第2行为每天所需的餐巾块数;第3行为每块餐巾的新购费用p,快洗所需天数m,快洗所需费用f,慢洗所需天数n,慢洗所需费用s。
输出格式:
输出文件共1行为最小的费用。
输入输出样例
说明
N<=2000
ri<=10000000
p,f,s<=10000
时限4s
*************在网络流24题中算是神奇的一题(其实并不一定是网络流)************
网络流24题中,存在各种只是来让人练写模板熟练度的题。不过这题的建图方式算是一股清流,让人很不好想,但想到了也很不好证明,不过证明出来后真的很好写。
对于送到快洗部、慢洗部、直接购买都很好想,拆点后直接连就行了。也就是把一天拆成上午和下午两个点,上午可以接受洗完的餐巾、买新餐巾,下午可以把餐巾送去洗。也就是说,上午的“流”的意义是干净的餐巾,下午的“流”的意义是脏的餐巾。
让第i天“经过”汇点的流为ri显然是行不通的。可以把“拿去用ri个餐巾”拆成两个动作:上午把ri个干净的餐巾给汇点,下午接收从源点来的ri个脏餐巾。
还要注意的是,餐厅里可以攒干净的餐巾,所以第i天上午要连一条边到第i+1天上午。
以上过程其实可以用有上下界网络流来思考,但并不对劲的人并不想这么做。
#include<iostream> #include<iomanip> #include<cstdio> #include<cstdlib> #include<cstring> #include<cmath> #include<algorithm> #include<queue> #define ll long long #define maxn 4010 #define maxm 2000010 using namespace std; const ll inf=0x7fffffff; ll fir[maxn],nxt[maxm],v[maxm],fl[maxm],w[maxm],cnt; ll mincost,n,m,p,f,s,N,dis[maxn]; bool vis[maxn]; ll read() { ll x=0,f=1; char ch=getchar(); while(isdigit(ch)==0 && ch!='-') ch=getchar(); if(ch=='-')f=-1,ch=getchar(); while(isdigit(ch))x=x*10+ch-'0', ch=getchar(); return x*f; } void addedge(ll u1,ll v1,ll fl1,ll w1) { w[cnt]=w1,v[cnt]=v1,fl[cnt]=fl1,nxt[cnt]=fir[u1],fir[u1]=cnt++; w[cnt]=-w1,v[cnt]=u1,fl[cnt]=0,nxt[cnt]=fir[v1],fir[v1]=cnt++; } ll spfa() { memset(dis,-1,sizeof(dis)); memset(vis,0,sizeof(vis)); dis[N*2+1]=0; deque<ll >q; q.push_back(N*2+1); while(!q.empty()) { ll u=q.front();q.pop_front(); for(ll k=fir[u];k!=-1;k=nxt[k]) { ll vv=v[k]; if(fl[k^1]>0) { if(dis[vv]>dis[u]-w[k] || dis[vv]==-1) { dis[vv]=dis[u]-w[k]; if(vis[vv]==0) { if(q.empty()==0&& dis[q.front()]>dis[vv]) q.push_front(vv); else q.push_back(vv); } vis[vv]=1; } } } vis[u]=0; } return dis[0]; } ll getfl(ll u,ll nowflow) { vis[u]=1; if(u==N*2+1)return nowflow; ll tmp,sum=0; for(ll k=fir[u];k!=-1;k=nxt[k]) { if(nowflow<=0)break; ll vv=v[k]; if(vis[vv]==0 && fl[k]>0 && dis[vv]+w[k]==dis[u]&& (tmp=getfl(vv,min(nowflow,fl[k])))>0) { fl[k]-=tmp; fl[k^1]+=tmp; nowflow-=tmp; mincost+=tmp*w[k]; sum+=tmp; } } return sum; } int main() { memset(fir,-1,sizeof(fir)); N=read(); for(ll i=1;i<=N;i++) { ll r=read(); addedge(i,N*2+1,r,0); addedge(0,i+N,r,0); } p=read(),m=read(),f=read(),n=read(),s=read(); for(ll i=1;i<=N;i++) { addedge(0,i,inf,p); if(i+m<=N)addedge(i+N,i+m,inf,f); if(i+n<=N)addedge(i+N,i+n,inf,s); if(i+1<=N)addedge(i,i+1,inf,0); } while(spfa()!=-1) { memset(vis,0,sizeof(vis)); getfl(0,0x7fffffff); } cout<<mincost; return 0; }
ex1(BJWC):如果n<=2 * 10^5,ri<=100该怎么做?
其实可以三分购买的毛巾总数,然后贪心地算出是否可行。
假设购买的毛巾总数是给定的,那么这道题就变得简单多了。在第x天将毛巾送去洗,到第x+m或x+n天才能用,这件事可以看成在第x天毛巾不够用时,花s或f乘坐时光机回到第x-n或x-m天,将毛巾变成干净的带回来。对于前面的若干天,新买的毛巾不用白不用,尽量把新买的用完。新毛巾不够用时考虑回到之前的某一天。这时肯定回到x-m或再之前的时间更划算。因为x-m及以前可以用慢洗,花费少。如果x-m天及之前没有脏毛巾了,才去第x-n到x-m天之间。这时最好取离现在更近的。这样可以让离现在更远的时间,也就是离第x-m天更近的时间,更有机会慢洗。这个过程可以用双端队列维护。
根据这个策略,对于每一个购买的毛巾总数c,都有唯一确定的总费用与之对应。因此定义f(c)为总共购买c块毛巾的费用。注意c的取值范围应该是大于某个数,因为新毛巾总数太少的时候有些天会没毛巾可用。
三分法要保证单峰。感性地想,c较小时要频繁地使用快洗,所以花在洗毛巾上的费用会很大。而c较大时购买毛巾的费用会很大。
理性地想,并不对劲的人并不理性。
设ri之和为R,这样就可以做到时间复杂度O(n log R)了呢。
ex2:在ex1的条件下,若快洗店更便宜,该怎么办?
那就别慢洗了。
是贪心能解所有网络流问题,还是网络流能解所有贪心问题呢(就是时间复杂度……)?