NOI 的题果然是非常的难且有意思。还有就是推荐一下command_block 的题解。
这题的题意比较难。
题意:有 \(n\) 种菜,初始每种菜有 \(c_i\) 个,单价 \(a_i\),如果不出售每天会变质 \(x_i\) 棵。第一次卖这种菜会获得 \(s_i\) 的奖励。每天至多卖 \(m\) 个菜。
给出 \(q\) 次询问,每次询问 \(1\sim q_i\) 天后的最大获利。
这里对变质的理解很重要。
变质的错误理解:无论如何,每天都必然有 \(x_i\) 个变质。
变质的正确理解:每天可以卖掉一些原本今天会变质的蔬菜,这样今天变质的蔬菜数量就 \(<x_i\) 了。
理解完题意,接下来看题解。
算法:模拟费用流。
【费用流模型】
蔬菜减少不太好,考虑时光倒流,从后往前做。每天会加入一些原来会变质的菜,菜可以无限保存。
把每个菜 \(i\) 每天 \(t\) 的状态抽象为 \((t,i)\)。
\(S\rightarrow (t,i)\),费用 \(a_i\) 容量为第 \(t\) 天新增的菜 \(i\) 数量。这个数具体是多少先放着。
\((t,i)\rightarrow (t+1,i)\),费用 \(0\) 容量 \(+\infty\),表示菜可以无限保存。
\((t,i)\rightarrow T_t\),费用 \(0\) 容量 \(+\infty\)。
\(T_t\rightarrow T\),费用 \(0\) 容量 \(m\),即 \(T_t\) 是负责收取第 \(t\) 天所有售出的菜的结点。
↑ 大概这样。
【一次模拟找性质】
因为图很复杂,考虑增量式分析。
借个 command_block 的图。
不断增加 \(T_i\) 点。
图 II 是新增的点和边,导致了图 III 的三种增广路。接下来是找环。
图 V 的环上每一条边费用都是 \(0\),没有用。
图 IV 的环的费用相当于一条边的费用减去另一条边,不如直接增广路,没有用。
然后怎么做?
上面这个图只能求一个后缀,不能求前缀。
【关注性质】
注意到上面图的一个性质:与 \(S\) 相连的边永不会退流。
这可以推出一个性质:第 \(p\) 天的最优决策包含第 \(p-1\) 天的最优决策。
具体一点,第 \(p-1\) 天最优决策卖的菜是第 \(p\) 天的子集。
进一步可以得到:假如知道了第 \(p\) 天卖了什么菜,第 \(p'(p'<p)\) 天的最优决策就是取这些菜中前 \(p'\times m\) 大的。(记得 \(m\) 是每天卖菜的最大数量)
现在的问题就是:怎么求出第 \(+\infty\) 天的最优决策。(其实不用 \(+\infty\),求出所有询问天数的最大值的最优决策即可)
【二次模拟求方案】
现在的问题是求出前 \(p\) 天的最优方案。因为现在只要求一个天数的答案,没有时间顺序的限制,所以可以随心所欲处理图。
变成添加一列点。
图 II,图 III 是两条增广路,具体意义为:把今天的菜今天卖/把过去的菜今天卖。
图 IV 图 V 的两个环类似上面的分析,都没有用。
所以我们维护图 II,III 的增广路就行了。
【Code】
点击查看代码
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N = 1e5 + 5;
int n, m, Q; //蔬菜种类,每天可卖个数,询问数
int a[N], s[N], c[N], d[N]; //单价,首次奖励,初始个数,变质个数
int qt[N]; //询问
int mxt = -1; //最晚天数
vector<int> add[N]; //add[i]保存所有在第i天第一次出现的菜
int sel[N] = {}; //set[i]表示此时菜i卖了多少个
typedef pair<int, int> pii;
#define mk_pr make_pair
priority_queue<pii> q;
ll ans[N * 10], cur = 0;
int main() {
cin >> n >> m >> Q;
for (int i = 1; i <= n; i++) {
cin >> a[i] >> s[i] >> c[i] >> d[i];
}
for (int i = 1; i <= Q; i++)
cin >> qt[i], mxt = max(mxt, qt[i]);
for (int i = 1; i <= n; i++) {
if (d[i] == 0)
add[mxt].push_back(i);
else
add[min((c[i] + d[i] - 1) / d[i], mxt)].push_back(i);
}
vector<int> v; //v保存当天会增加的菜
for (int nw = mxt; nw >= 1; nw--) { //从后往前
//第i种菜应有c[i]-d[i]*(nw-1)-sel[i]个
for (int i = 0; i < add[nw].size(); i++)
v.push_back(add[nw][i]);
for (int i = 0; i < v.size(); i++) {
if (sel[v[i]] == 0)
q.push(mk_pr(a[v[i]] + s[v[i]], v[i]));
else
q.push(mk_pr(a[v[i]], v[i]));
}
v.clear();
int cnt = m;
while (cnt) { //只要还有得卖
if (q.empty()) //没菜了
break;
pii tmp = q.top();
// q.pop();
int id = tmp.second;
if (sel[id] == 0) { //还没卖过
sel[id] = 1;
cnt--;
q.pop();
if (c[id] - sel[id] - (nw - 1) * d[id] > 0) //还有
q.push(mk_pr(a[id], id));
else if (d[id]) //这一次没了,但下一天会加回来
v.push_back(id);
else //永远没了
;
}
else {
int nw_sel = min(cnt, c[id] - sel[id] - (nw - 1) * d[id]); //能卖就卖
cnt -= nw_sel;
sel[id] += nw_sel;
if (c[id] - sel[id] - (nw - 1) * d[id] > 0)
q.push(mk_pr(a[id], id)); //没卖完呢
else if (c[id] - sel[id] - (nw - 1) * d[id] == 0) {
q.pop();
if (d[id])
v.push_back(id); //还有新增
else
; //永远没了
}
}
}
}
//此时得到了sel[i]:每种菜卖了多少个
//求ans序列:有哪些收益的菜卖了
for (int i = 1; i <= n; i++) {
if (sel[i]) {
ans[++cur] = a[i] + s[i];
for (int j = 2; j <= sel[i]; j++)
ans[++cur] = a[i];
}
}
sort(ans + 1, ans + cur + 1);
reverse(ans + 1, ans + cur + 1);
for (int i = 2; i <= cur; i++)
ans[i] += ans[i - 1];
for (int i = 1; i <= Q; i++)
cout << ans[min(qt[i] * m * 1ll, cur)] << endl;
return 0;
}