题解【P3826 [NOI2017] 蔬菜】

command_block

2021-06-15 22:34:32

Solution

更严谨的正确性分析。 更自然的非启发式思路。(然而也有点难想) 更强的扩展性。(然而也没做过几个题) ~~更多的草稿纸。~~ **广告** : [模拟费用流小记](https://www.luogu.com.cn/blog/command-block/mu-ni-fei-yong-liu-xiao-ji) (建议阅读) ------------ **题意** : 有 $n$ 种蔬菜,第 $i$ 种蔬菜单价是 $a_i$,初始时数量为 $c_i$,若不出售,每天有 $x_i$ 份会变质。初次出售第 $i$ 种蔬菜可以获得 $s_i$ 的奖励。 每天可以卖掉 $m$ 个未变质的蔬菜。有 $k$ 个询问,每次问在 $1\sim p$ 天出售蔬菜能够获得的最大收益。 ------------ 首先解释一下题意。 **同种蔬菜中**,每棵蔬菜都有自己独立的变质时间,在不出售的情况下,效果是每天有 $x_i$ 份会变质。但可以出售这些较快变质的菜,保留那些较慢变质的菜。 - **正确理解** : 每天会有 $3$ 棵菜变质,则变质时间为 $\{1,1,1,2,2,2,3,3,3...\}$。若第一天卖掉了两棵 $[1]$ ,则只有一棵 $[1]$ 会变质。 - **错误理解** : 无论怎么卖,剩余的该种蔬菜都会变质 $3$ 个。 也可以理解为,将蔬菜再按照过期时间分类为不同类别的菜。 ------------ - $\small\blacksquare$ **费用流模型** 考虑**时间倒流**,问题变为 : 每天都会加入一些菜(原来会变质的菜),菜可以无限期保存,但每天最多出售 $m$ 个菜,问最大总收益。 为第 $t$ 天的第 $i$ 种蔬菜建立点 $(t,i)$。建边 $S\rightarrow (t,i)$ ,边权为 $a_i$,容量为当天加入的蔬菜数目。 连边 $(t,i)\rightarrow (t+1,i)$ ,容量不限,表示可以将蔬菜留到下一天。 为第 $t$ 天建立一个点表示出售,记为 $[t]$ 。建边 $[t]\rightarrow T$ ,容量为 $m$。 建边 $(t,i)\rightarrow[t]$ ,容量不限。 在菜 $i$ **第一次出现**的时间,从 $S\rightarrow (t,i)$ 中分出一个流量(特殊菜),边权改为 $a_i+s_i$ ,表示出售奖励。 (不难发现,任意一种没有卖掉“特殊菜”情况都可以转化为卖掉“特殊菜”的情况而增加收益,算法一定不会错过奖励) 然后求解最大费用任意流。 - $\small\blacksquare$ **模拟费用流** : 获得性质 & 贪心算法 首先观察建图 : ![](https://cdn.luogu.com.cn/upload/image_hosting/cyjctszl.png) 只有源边有权值,其余边的权值均为 $0$。 考虑使用 “**增量-最大费用任意流**” 模型。 时间倒流后,原来限制只能在前 $1\sim p$ 天(一个前缀)出售,现在变为在一个前缀不能出售,之后才能出售。 **从后向前**依次加入每一天的出售点(这样才能符合原来的时间顺序),观察会新形成怎样的“源汇路”或“环” : ![](https://cdn.luogu.com.cn/upload/image_hosting/a9qvsm06.png) 图 $\rm I$ : 未加入出售点时的图。此时图中即没有源汇路也没有环,无需处理。 图 $\rm II$ : 红色部分为新加入的点和边,边的方向是给定的。黑色部分为之前的点和边,边的方向并不确定。 图 $\rm III$ : 三种新的源汇路。具体意义无需关心。 图 $\rm IV$ : 一种新的环。和上一题类似,该方案不如单走一个源汇路,可以排除。 图 $\rm V$ : 一种新的环。注意到不和 S 相连的边均无权,故该环不可能是负环,可以排除。 综上,可能的更新操作中,**所有 S 的出边不会退流**。 因此,我们的决策实际上是非常简单的。形式化地,在**未经时光倒流的原问题**中,前 $p$ 天售卖蔬菜的最优方案,是在前 $p-1$ 售卖蔬菜的最优方案的超集。 有了这个性质,考虑先求出前 $p$ 天的最优方案,然后推出 $1\sim p-1$ 天的方案。 若前 $p$ 天的最优方案为蔬菜集合 $S$ ,则 $S$ 中的所有蔬菜的过期日期都 $\geq p$。 对于第 $p'$ 天,只需保留其中最贵的 $p'*m$ (可能不足)棵,不难发现这是合法的,且是所有合法子集中最优的。 那么将 $S$ 排序就容易得到最终答案。 至于如何求出前 $p$ 天的最优方案,则需设计另一种模拟费用流。 现在已经没有时序的限制,我们可以按照任意顺序处理费用流图。 ![](https://cdn.luogu.com.cn/upload/image_hosting/95eilgnl.png) (我们将思路回到时光倒流后的形式) 图 $\rm I$ : 红色部分为新加入的点和边,黑色部分为之前的点和边。 图 $\rm II$ : 一种可能的源汇路,对应将今天加入的菜直接卖掉。 图 $\rm III$ : 一种可能的源汇路,对应将之前存下的菜卖掉。 图 $\rm IV,V$ : 两种环。和上文类似,可以排除。 在同一种菜中,出售那颗的获益都是一样的,故使用堆维护各种菜即可。当售空时,则从堆中取出。 当加入菜时,若原来这种菜售空,则将其加入堆。这需要维护售空且有持续输入的菜的集合。 (还有一点关于 $s$ 的小细节,不难处理) 复杂度 $O(nm\log n)$。 ```cpp #include<algorithm> #include<cstdio> #include<queue> #define pb push_back #define Pr pair<int,int> #define mp make_pair #define fir first #define sec second #define ll long long #define MaxN 100500 using namespace std; int a[MaxN],s[MaxN],tot[MaxN],d[MaxN],c[MaxN]; int n,lim,m,t[MaxN],tim; ll ans[MaxN*10]; vector<int> g[MaxN]; int st[MaxN],tn; priority_queue<Pr> q; int main() { scanf("%d%d%d",&n,&lim,&m); for (int i=1;i<=n;i++) scanf("%d%d%d%d",&a[i],&s[i],&tot[i],&d[i]); for (int i=1;i<=m;i++){ scanf("%d",&t[i]); tim=max(tim,t[i]); } for (int i=1;i<=n;i++) if (!d[i])g[tim].pb(i); else g[min((tot[i]+d[i]-1)/d[i],tim)].pb(i); for (int t=tim;t;t--){ for (int i=0;i<g[t].size();i++) st[++tn]=g[t][i]; for (int i=1;i<=tn;i++){ int k=st[i]; q.push(mp(!c[k] ? a[k]+s[k] : a[k],k)); }tn=0; int cnt=lim; while(cnt){ if (q.empty())break; int i=q.top().sec; if (!c[i]){ c[i]=1;cnt--; q.pop(); if (tot[i]-d[i]*(t-1)>1)q.push(mp(a[i],i)); else if (d[i])st[++tn]=i; }else { int cnt2=min(tot[i]-d[i]*(t-1)-c[i],cnt); c[i]+=cnt2;cnt-=cnt2; if (tot[i]-d[i]*(t-1)==c[i]) {q.pop();if (d[i])st[++tn]=i;} } } } tn=0; for (int i=1;i<=n;i++){ if (c[i])ans[++tn]=a[i]+s[i]; for (int j=2;j<=c[i];j++)ans[++tn]=a[i]; }sort(ans+1,ans+tn+1);reverse(ans+1,ans+tn+1); for (int i=1;i<=tn;i++)ans[i]+=ans[i-1]; for (int i=1;i<=m;i++) printf("%lld\n",ans[min(tn,t[i]*lim)]); return 0; } ```