1 条题解
-
0
(原文作者:Botao Yuan,由 GPT-5.4 Thinking 翻译)
题目分析
我们要求对于 次询问,移除 个草垛所需的最小花费。为了移除草垛,我们可以雇佣奶牛。第 头奶牛有属性 ,表示如果当前草垛堆中有 个草垛,我们可以花费 移除
个草垛。
现在考虑一个最优(或者接近最优)的雇佣顺序应当是什么样的。显然,我们通常应该尽量雇佣那些 更大的奶牛(前提是当前草垛数量满足 )。把使这个比值最大的奶牛称为“最佳效率奶牛”。注意,每当我们处在 这个值时,最佳效率奶牛都可能发生变化。把所有形如 的值称为特殊值。
问题在于 带来的限制:当 时,一头奶牛虽然仍然花费 ,但实际移除的草垛数会少于 。这意味着,在特殊值附近,最优策略会依赖于精确的余数,因此不能总是贪心地不断选择最佳效率奶牛直到清空所有草垛。
另一方面,如果我们离某个特殊值足够远,那么就应当反复选择最佳效率奶牛,直到到达某个需要考虑余数的阈值。事实证明,相对于当前考虑的特殊值,这个阈值是 ,其中 是所有 的最大值。
关键结论
对于 ,最优代价满足
其中 是最佳效率奶牛,也就是使 最大的那头。这里, 表示:相对于当前特殊值的上方,多出 个草垛时的最小移除代价;并且只考虑满足 不大于当前特殊值的奶牛。
证明
考虑某个 的最优解。
如果它第一步就使用了最佳效率奶牛,那么显然有
现在假设在前 次雇佣中,最佳效率奶牛至少出现过一次,并且总共移除了 个草垛。那么我们可以交换奶牛的使用顺序,使得最佳效率奶牛先被使用。因为每头奶牛移除的草垛数都不变,所以交换后 不会变化。于是仍然得到
否则,假设这个解在前 次雇佣中完全没有使用最佳效率奶牛。设它雇佣了一串奶牛,分别移除了 个草垛,花费分别为 。由于每个 ,并且它们的总和至少为 ,所以有 。
只看前 个前缀和
对 取模后的结果。根据抽屉原理,其中必有两个前缀和模 同余,因此在前 次雇佣中,存在一个连续子段,恰好移除了 个草垛,其中 ,总花费记为 。
由于 是最佳效率奶牛,所以对任意奶牛 都有
也就是
把这个不等式对子段中的所有奶牛求和,可以得到
$$C_{\text{sub}} \ge \frac{(k \cdot s^*) \cdot c^*}{s^*} = k \cdot c^*.$$因此,用 头最佳效率奶牛替换这段连续子段,在移除草垛数相同的前提下,总代价至多为 。替换之后,最佳效率奶牛就会在前 次雇佣中出现,且总花费不增,于是就归约到了前一种情况。因此,
所以,对于所有 ,都有
也就是说,只需要显式计算 个 DP 状态。
你可能会注意到,这也和 Frobenius/Coin 问题有一定相似性。
部分解法:
我们按 从小到大处理奶牛,按 从小到大处理询问。同时,对于每个可能的 ,维护一头当前最优的奶牛,也就是 最大的那头。每当遇到一个新的 ,就计算一个大小为 的 DP 表,其中 表示当草垛数量为 时的答案。
为了重新计算这个 DP 表,首先可以直接查询上一张 DP 表来得到初值。也就是说,对每个 ,把 当作对上一张 DP 表的一次询问,此时暂时不考虑新加入的奶牛。然后,对这 头“各自 下最优”的奶牛逐个做背包转移。转移方程为
$$DP_j = \min\left(DP_j,\; c + DP_{\max(0,\; j - s_i)}\right), \quad j = 0, 1, \ldots, S^2.$$共有 个状态,而每次需要进行 轮扫描,因此复杂度是 。
处理完 后,回答所有满足 的询问。回答一个询问时,如果
那么直接查表即可,答案为
否则,使用最佳效率奶牛 把 降到表的范围内。令
$$q = \left\lceil \frac{a_k - (p_i - 1) - S^2 + 1}{s^*} \right\rceil,$$则答案为
总时间复杂度为
正解:
我们还可以进一步优化 DP 重算的过程。注意到,当处理一头新的、阈值为 的奶牛时,并不需要对全部 种不同的工作量重新做一遍背包。上一张 DP 表已经编码了此前所有奶牛的最优组合;当我们通过查询旧表来初始化新表时,这些旧奶牛的贡献实际上已经被计入了。
唯一新增的信息,就是这头刚刚加入的奶牛。因此,在用上一张表初始化完这 个状态之后,只需要针对这头新奶牛的工作量 和花费 做一次背包扫描即可。于是,这一步的复杂度就变成了 。
这样一来,每头奶牛只需要 的时间,总复杂度就是
参考代码
#include <bits/stdc++.h> using namespace std; using ll = long long; const ll INF = 1e18; const int S = 100; void upd(ll& a, ll b) { a = min(a, b); } int main() { ios::sync_with_stdio(false), cin.tie(nullptr); int t; cin >> t; while(t--) { ll n; cin >> n; vector<ll> a(n), oa(n); for(int i = 0; i < n; i++) { cin >> a[i]; oa[i] = i; } sort(oa.begin(), oa.end(), [&] (ll x, ll y) { return a[x] < a[y]; }); ll m; cin >> m; vector<ll> l(m), s(m), v(m), o(m); for(int i = 0; i < m; i++) { cin >> l[i] >> s[i] >> v[i]; l[i]--; o[i] = i; } sort(o.begin(), o.end(), [&] (ll x, ll y) { return l[x] < l[y]; }); int pl = 0; vector<ll> dp(S * S, INF); dp[0] = 0; vector<ll> best(S + 1, 1e15); // computes expdp which is used to query vector<ll> expdp; int bsi = 1; auto compute = [&] (int nsi) { for(int si = 1; si <= S; si++) { if(best[bsi] * si > bsi * best[si]) { bsi = si; } } expdp = dp; for(int i = 0; i < expdp.size(); i++) { if(i + nsi < expdp.size()) { upd(expdp[i + nsi], expdp[i] + best[nsi]); } } }; // uses expdp to query auto query = [&] (ll x) { ll d = x - pl; ll t = max((d - (ll)expdp.size()) / bsi + 1, 0ll); return t * best[bsi] + expdp[d - bsi * t]; }; int k = 0; int lasts = 0; vector<ll> ans(n); for(int i : o) { compute(lasts); while(k < n && a[oa[k]] < l[i]) ans[oa[k]] = query(a[oa[k]]), k++; vector<ll> ndp(S * S, INF); for(int j = 0; j < S * S; j++) { ndp[j] = query(l[i] + j); } for(int j = 1; j < s[i]; j++) upd(ndp[j], ndp[0] + v[i]); upd(best[s[i]], v[i]); lasts = s[i]; dp = ndp; pl = l[i]; } compute(lasts); while(k < n) ans[oa[k]] = query(a[oa[k]]), k++; for(ll i : ans) cout << i << " "; cout << endl; } }
- 1
信息
- ID
- 6540
- 时间
- 2500ms
- 内存
- 256MiB
- 难度
- 10
- 标签
- 递交数
- 4
- 已通过
- 1
- 上传者