1 条题解
题解与来源题共享。已有题解默认通过;新增或编辑后需由来源域管理员审核。待审核、驳回题解仅作者和审核管理员可见。来源域:system
-
0
1. 前置思考
题目要求我们在恰好 次操作后,得到尽可能多的、长度完全相同的胡萝卜。 假设我们最终希望得到的大量相同胡萝卜的长度为 。
每次操作,我们可以选择一个集合,并指定一个 。如果胡萝卜长度 ,它就会被切成 和 。 因为我们可以选择任意集合的胡萝卜同时操作,这意味着在 次操作中,我们可以把选中的每一段都切成两半。 这就像细胞分裂一样:
- 次操作后, 根胡萝卜最多变成 根。
- 次操作后,最多变成 根。
- 次操作后, 根胡萝卜最多变成 根。
假设有一根初始长度为 的胡萝卜,我们想用 次操作从中提取出尽量多长度为 的段。 最贪心、最高效的切法是二分切割
- 第 次操作选
- 第 次操作选
- ……
- 第 次操作选
通过这种方式,我们能在 次操作内,把长度充分利用,并行地切出大量长度为 的段。
2. 单根胡萝卜的极限推导
对于单根长度为 的胡萝卜,目标是切出长度为 的段。我们最多能切出多少段? 显然,从长度上限制,最多只能切出 段。 但由于操作次数 的限制,我们最多只能产生 个碎块。我们分三种情况讨论:
情况 A: (即 )
因为理论最多只能切出 段,且 。我们在 次操作内完全有能力把这 段全部分离出来。
- 结论: 贡献了 段。
情况 B:
这是一根非常完美的胡萝卜。我们按照上述的二分切割法,每次对半切, 次操作后,刚好切成 个碎块,且每个碎块的长度恰好都是 。没有任何浪费。
- 结论: 贡献了 段。
情况 C:
此时胡萝卜非常长。我们在 次操作后最多只能把它切成 个碎块。 这 个碎块的长度总和是 。因为 ,根据鸽巢原理,不可能所有碎块的长度都是 (否则总长只有 )。 也就是说,至少有 1 个碎块的长度 。因此,长度恰好为 的碎块数量最多只能是 。 (通过贪心二分切割,我们确实可以做到留下 块较长的废料,其余 块全是 )。
- 结论: 贡献了 段。
3. 算法设计与优化
理解了单根胡萝卜的贡献规则后,我们现在要对所有的胡萝卜求和。 假设当前枚举的目标长度为 ,操作次数为 。令
limit。 根据上面的推导,一根长度为 的胡萝卜的贡献如下:- 如果 :贡献为 。
- 如果 :贡献为 (也就是 )。
- 如果 :贡献为 (也就是 )。
为什么 最大只考虑到 17?
题目中 。 当 时,。 这意味着对于 ,所有的胡萝卜都属于情况 A,我们可以把所有胡萝卜都切成长度为 的段。此时能得到的最多段数就是所有初始胡萝卜长度之和()。 因此,对于 的询问,答案恒为
sum_a,我们只需计算 。调和级数枚举优化
为了快速计算,我们可以预处理:
cnt[x]:长度为 的胡萝卜数量。s[x]:cnt[x]的前缀和,用于快速查询区间内的胡萝卜总数。
对于固定的 ,我们枚举 。 落在区间 内的所有胡萝卜,它们的 都是 。 我们预处理一个数组
sumj:sumj[j]表示:所有 的胡萝卜,如果完全按照情况 A 提取,总共能提供多少段。
然后在计算固定的 和 时,我们直接根据上面的 种情况,使用
sumj和前缀和s以 的时间算出总贡献。最后所有 取最大值即可。
4. Std
#include <bits/stdc++.h> #define ll long long #define mod 998244353 #define eps 1e-7 using namespace std; const double PI = acos(-1); const int INF = 0x3f3f3f3f; const int N = 5e2 + 10; void solve() { ll n, m; cin >> n >> m; vector<ll> a(n), cnt(m + 10); // cnt[x]表示长度为x的胡萝卜有几个 ll sum_a = 0; for (ll i = 0; i < n; i++) { cin >> a[i]; sum_a += a[i]; cnt[a[i]]++; } vector<ll> s(m + 10); for (ll i = 1; i <= m; i++) { s[i] = s[i - 1] + cnt[i]; } vector<ll> sumj(m + 10), ans(20); ll maxk = min(m, 17ll); for (ll v = 1; v <= m; v++) { // 倍数枚举 v=3 [0,1,2] [3,4,5]... //[v*j, v*(j+1)-1] -> floor(L/v)==j ll maxj = m / v; for (ll j = 1; j <= maxj; j++) { ll L = v * j; ll R = min(m, v * (j + 1) - 1); ll num = s[R] - s[L - 1]; sumj[j] = sumj[j - 1] + num * j; } for (ll k = 1; k <= maxk; k++) { ll limit = (1 << k) - 1; ll tmp = 0; if (limit >= maxj) // 当前所有的段都是v { tmp = sumj[maxj]; } else // limit<maxj { tmp = sumj[limit]; ll val = (limit + 1) * v; if (val <= m) { tmp += cnt[val] * (limit + 1) + (s[m] - s[val]) * limit; } } ans[k] = max(ans[k], tmp); } } for (ll k = 1; k <= m; k++) { if (k <= 17) cout << ans[k] << " "; else cout << sum_a << " "; } cout << "\n"; } int main() { // ios_base::sync_with_stdio(false); // cin.tie(nullptr); int t = 1; cin >> t; while (t--) { solve(); } return 0; } /* 我们现在关注对某一根长度为 L 的胡萝卜,如果把它切成长度为 v 的段,最多切成多少段? floor(L/v) 如果我们对某一根长度为L的胡萝卜,切 k 次,能够得到多少长度为 v 的段?O(mlogm*min(m,17)) 只有 k =1,2,...,17才需要精确计算和枚举 floor(L/v) 去和 2^k 比较 floor(L/v)<2^k-1 -> floor(L/v) floor(L/v)==2^k-1 -> 2^k+1 floor(L/v)>2^k-1->2^k-1 v=1, v=2, v=3,..., v=m 调和级数的枚举 k大到一定的数字的时候,就不需要算了,一定是都切成长度为1 */
信息
- ID
- 1314
- 时间
- 1000ms
- 内存
- 256MiB
- 难度
- 普及
- 标签
- 递交数
- 8
- 已通过
- 2
- 上传者