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\) 的段,其最大贡献段数为:

\[\text{contribution}(a) = \begin{cases} \min\left(\left\lfloor \frac{a}{L} \right\rfloor,\ 2^k - 1\right), & a \neq 2^k L \\[6pt] 2^k, & a = 2^k L \end{cases} \]

解释:

  • 若 \(a = 2^k L\),可以恰好切成 \(2^k\) 段长度为 \(L\) 的胡萝卜。
  • 否则,最多只能得到 \(2^k - 1\) 段长度为 \(L\) 的胡萝卜(因为总长度无法被 \(2^k\) 整除,最后会剩残料)。
  • 同时受总长度限制,不能超过 \(\lfloor a/L \rfloor\)。

对所有胡萝卜求和,得到总贡献:

\[\text{total}(L,k) = \sum_{j=1}^{2^k-1} \text{suf}[jL] \;+\; \text{cnt}[2^k L] \]

其中:

  • \(\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\) 的答案。核心在于:

  1. 把每根胡萝卜的贡献拆成 suf[jL] 的求和 + 特殊处理 cnt[2^k L]。
  2. 利用 \(j\) 和 cur 的共享,避免重复计算。
  3. 只枚举到 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;
}
posted @ 2026-10-11 20:49  thenrify  阅读(8)  评论(0)    收藏  举报