Carrot Chopdown (Hard Version)
Carrot Chopdown (Hard Version)
快速计数技巧,降低复杂度
问题概述
给定 \(n\) 根胡萝卜,长度分别为 \(a_1,a_2,\dots,a_n\)。有一台切割机,每次操作可以选择一组胡萝卜和一个正整数 \(x\),对于每根选中的长度为 \(l\) 的胡萝卜:
- 若 \(l \le x\),保持不变;
- 否则,切成 \(x\) 和 \(l-x\) 两根。
要求使用机器恰好 \(k\) 次后,最终能卖出多少根长度完全相同的胡萝卜。需要对每个 \(k=1,2,\dots,m\) 独立求解,输出对应的最大数量。
核心思路
枚举最终统一的长度 \(L\)(\(1 \le L \le m\)),计算在恰好 \(k\) 次操作下,所有初始胡萝卜能贡献多少根长度为 \(L\) 的胡萝卜。对所有 \(L\) 取最大值,就是 \(k\) 次操作的答案。
关键观察
每次操作最多把一根胡萝卜一分为二,所以 \(k\) 次操作后,一根初始胡萝卜最多能变成 \(2^k\) 段。
对于一根长度为 \(a\) 的胡萝卜,若想得到长度为 \(L\) 的段,其最大贡献段数为:
解释:
- 若 \(a = 2^k L\),可以恰好切成 \(2^k\) 段长度为 \(L\) 的胡萝卜。
- 否则,最多只能得到 \(2^k - 1\) 段长度为 \(L\) 的胡萝卜(因为总长度无法被 \(2^k\) 整除,最后会剩残料)。
- 同时受总长度限制,不能超过 \(\lfloor a/L \rfloor\)。
对所有胡萝卜求和,得到总贡献:
其中:
- \(\text{cnt}[x]\):初始长度恰好为 \(x\) 的胡萝卜数量。
- \(\text{suf}[i]\):长度 \(\ge i\) 的胡萝卜数量,即后缀和 \(\sum_{x \ge i} \text{cnt}[x]\)。
代码逐行解析
for(ll L = 1; L <= m; L ++) {
ll j = 1, cur = 0;
for(int k = 1; k <= m && k <= maxK; k ++) {
while(j < (1 << k) && j * L <= m) {
cur += suf[j * L];
j ++;
}
if((L << k) <= m) ans[k] = max(ans[k], cur + cnt[(L << k)]);
else ans[k] = max(ans[k], cur);
}
}
外层循环:枚举目标长度 \(L\)
for(ll L = 1; L <= m; L ++) {
最终卖出的胡萝卜长度必须相同,设为 \(L\)。枚举所有可能的 \(L\)。
初始化
ll j = 1, cur = 0;
- \(j\):当前要累加的下标(表示 \(jL\))。
- \(cur\):当前已累加的 \(\sum \text{suf}[jL]\),即 \(\sum_{j'=1}^{j-1} \text{suf}[j'L]\)。
注意:\(j\) 和 \(cur\) 在内层 \(k\) 循环之间共享,只增不减。这是效率的关键。
内层循环:枚举操作次数 \(k\)
for(int k = 1; k <= m && k <= maxK; k ++) {
maxK约为 \(\lfloor \log_2 m \rfloor + 1\)。- 当 \(k > \text{maxK}\) 时,\(2^k > m\),答案不再变化,所以只需枚举到
maxK。
扩展 \(j\) 到 \(2^k - 1\)
while(j < (1 << k) && j * L <= m) {
cur += suf[j * L];
j ++;
}
- 对于当前的 \(k\),公式中求和的上限是 \(2^k - 1\)。
- 因为 \(j\) 是跨 \(k\) 共享的,之前 \(k\) 较小时已经加过前面的项,所以这里只需把 \(j\) 扩展到新的上限。
- 条件
j * L <= m防止越界访问suf数组(suf只定义到 \(m\))。 - 每加一项
suf[j*L],就把所有长度 \(\ge jL\) 的胡萝卜都算进来。
更新答案
if((L << k) <= m) ans[k] = max(ans[k], cur + cnt[(L << k)]);
else ans[k] = max(ans[k], cur);
L << k就是 \(2^k \cdot L\)。- 如果 \(2^k L \le m\),则存在长度恰好为 \(2^k L\) 的胡萝卜,它们可以额外贡献一段(因为可以恰好切成 \(2^k\) 段),所以加上
cnt[2^k L]。 - 否则,只能取
cur(即 \(\sum_{j=1}^{2^k-1} \text{suf}[jL]\))。 - 用
max更新ans[k],因为我们要找所有 \(L\) 中的最大值。
为什么 \(j\) 可以跨 \(k\) 共享?
考虑 \(L\) 固定时,不同 \(k\) 对应的求和上限:
| \(k\) | 求和上限 \(2^k-1\) |
|---|---|
| 1 | 1 |
| 2 | 3 |
| 3 | 7 |
| 4 | 15 |
cur 在 \(k=1\) 时累加了 suf[L]。
到 \(k=2\) 时,只需再累加 suf[2L] 和 suf[3L],就得到了 \(\sum_{j=1}^{3} \text{suf}[jL]\)。
到 \(k=3\) 时,再累加 suf[4L] 到 suf[7L],以此类推。
因此 \(j\) 和 cur 是增量式维护的,避免了对每个 \(k\) 都从头求和。
复杂度分析
- 统计和后缀和:\(O(n+m)\)。
- 外层枚举 \(L\):\(O(m)\)。
- 内层 \(j\) 的总增加量:对于每个 \(L\),\(j\) 最多到 \(\min(2^{\maxK}, m/L+1)\),所以总操作次数约为\[\sum_{L=1}^{m} \frac{m}{L} = O(m \log m) \]
- 总时间复杂度:\(O(n + m \log m)\)。
- 空间复杂度:\(O(m)\)。
总结
这段代码用枚举最终长度 \(L\) + 枚举操作次数 \(k\) + 后缀和增量维护的方式,在 \(O(m\log m)\) 时间内求出所有 \(k\) 的答案。核心在于:
- 把每根胡萝卜的贡献拆成
suf[jL]的求和 + 特殊处理cnt[2^k L]。 - 利用 \(j\) 和
cur的共享,避免重复计算。 - 只枚举到
maxK,超过部分答案不变。
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
void solve() {
int n, m; cin >> n >> m;
vector<ll> cnt(m + 2, 0);
for(int i = 1; i <= n; i ++) {int a; cin >> a; cnt[a] ++;}
vector<ll> suf(m + 3, 0);
for(int i = m; i > 0; i --) suf[i] = suf[i + 1] + cnt[i];
int maxK = 0;
while((1LL << maxK) <= m) maxK ++;
vector<ll> ans(m + 1, 0);
for(ll L = 1; L <= m; L ++) {
ll j = 1, cur = 0;
for(int k = 1; k <= m && k <= maxK; k ++) {
while(j < (1 << k) && j * L <= m) {
cur += suf[j * L];
j ++;
}
if((L << k) <= m) ans[k] = max(ans[k], cur + cnt[(L << k)]);
else ans[k] = max(ans[k], cur);
}
}
for(int k = 1; k <= m; k ++) cout << ans[min(k, maxK)] << " \n"[k == m];
}
int main() {
ios::sync_with_stdio(false); cin.tie(nullptr); cout.tie(nullptr);
int _=1; cin >> _;
while(_--) solve();
return 0;
}

浙公网安备 33010602011771号