题解:学而思编程 折半与最小值
【题目来源】
学而思编程:折半与最小值
【题目描述】
有 \(n\) 根木棒,第 \(i\) 根木棒的长度为 \(a_i\)。
接下来要进行 \(m\) 次操作:
选择一根长度大于 \(1\) 的木棒,设其长度为 \(l\)。
将这根木棒分割成两根,长度分别为 \(⌊l/2⌋\) 和 \(⌈l/2⌉\)。
(\(⌊r⌋\) 表示小于等于 \(r\) 的最大整数;\(⌈l/2⌉\) 表示大于等于 \(r\) 的最小整数)
根据题目的约束,可以证明一定能够进行 \(m\) 次操作。
经过 \(m\) 次操作后,一共会有 \(n+m\) 根木棒,记这些木棒中长度最小的那根的长度为 \(x\)。
请你求出:通过选择合适的操作策略,\(x\) 可能达到的最大值。
对于每个输入,需要处理 \(T\) 个测试用例。
【输入】
第 \(1\) 行两个正整数 \(n,m\)。第 \(2\) 行 \(n\) 个正整数 \(a_1,a_2,…,a_n\)。
【输出】
输出 \(T\) 行,每行对一个询问,输出操作到最后木棒最小长度 \(x\) 可能达到的最大值。
【输入样例】
3
5 2
5 6 7 14 17
4 3
444 444 44 444
2 2
100 222
【输出样例】
4
44
55
【核心思想】
-
问题分析:给定 \(n\) 根木棒,长度分别为 \(a_i\),进行 \(m\) 次操作(每次将一根长度 \(> 1\) 的木棒折半分成两根)。最终得到 \(n + m\) 根木棒,需要最大化这些木棒中最小长度 \(x\)。关键观察是:若要求所有木棒长度 \(\geq x\),则每根原始木棒 \(a_i\) 最多可以被分割成若干段,每段长度 \(\geq x\)。这是一个二分答案 + 贪心分割问题。
-
算法选择:
- 整数二分:对最小长度 \(x\) 进行二分,将最优化问题转化为判定问题
- 贪心分割判定:对于给定的 \(x\),计算每根木棒最多能分割成多少段长度 \(\geq x\) 的片段,判断总片段数是否 \(\geq n + m\)
-
关键步骤:
- 判定函数
check(x):- 若存在 \(a_i < x\),返回
false(该木棒本身就不够长) - 对每根木棒 \(a_i\),计算其最多能分割出的段数:
- 初始段长 \(\text{len} = a_i\),段数 \(\text{num} = 1\)
- 当 \(\lfloor \text{len}/2 \rfloor \geq x\) 时,不断折半:\(\text{len} \leftarrow \lfloor \text{len}/2 \rfloor\),\(\text{num} \leftarrow \text{num} \times 2\)
- 处理剩余长度 \(r = a_i - \text{len} \times \text{num}\):若 \(\text{len} + 1 = 2x\),则额外增加 \(r\) 段
- 累加总段数 \(\text{tot}\)
- 返回 \(\text{tot} \geq n + m\)
- 若存在 \(a_i < x\),返回
- 二分查找:
- 范围 \([1, 10^9]\),取上中点 \(mid = \lfloor \frac{l + r + 1}{2} \rfloor\)
- 若
check(mid)为真:\(l = mid\)(尝试更大的最小值) - 否则:\(r = mid - 1\)
- 输出答案:\(l\)
- 判定函数
-
时间/空间复杂度:
- 时间复杂度:\(O(T \cdot \log(10^9) \cdot n \cdot \log a_i) = O(T \cdot n \cdot \log^2 C)\),二分约 \(30\) 次,每次判定每根木棒折半 \(O(\log a_i)\) 次
- 空间复杂度:\(O(n)\),存储木棒长度数组
-
二分答案 + 贪心分割的核心思想:
- 单调性利用:若最小长度 \(x\) 可行,则任何 \(x' < x\) 也可行(更容易满足),因此可行解集合具有单调性,适合二分查找最大可行值
- 折半分割的最优性:对于固定的 \(x\),每根木棒采用"尽可能均匀折半"的策略能得到最多的段数。因为每次折半将一段变为两段,若折半后每段仍 \(\geq x\),则继续折半可最大化段数
- 剩余长度处理:当 \(\text{len}\) 折半后小于 \(x\) 时停止,此时剩余长度 \(r = a_i - \text{len} \times \text{num}\) 可以通过微调分配(如将某些段增加 \(1\))来产生额外段数,条件是 \(\text{len} + 1 \geq 2x\) 即 \(\text{len} + 1 = 2x\)(因为 \(\text{len} < 2x\) 已保证)
- 上中点取整:使用 \((l + r + 1) / 2\) 取上中点,配合 \(l = mid\) 和 \(r = mid - 1\),确保二分收敛到最大可行值
- 适用于资源分割、负载均衡、最小化最大值等优化问题
【算法标签】
整数二分
【代码详解】
#include <bits/stdc++.h>
using namespace std;
#define int long long
const int N = 100005;
int n, m, T, a[N]; // n: 原始数个数, m: 需要新增的数, T: 测试用例数
// 检查能否将每个数切分,使得所有片段长度至少为x
// 并且总片段数 ≥ n+m
bool check(int x)
{
int tot = 0; // 总片段数
for (int i = 1; i <= n; i++)
{
if (a[i] < x) // 如果原始长度小于x,无法满足
{
return false;
}
int len = a[i], num = 1; // 当前片段长度和数量
// 不断二分直到不能再分
while (len / 2 >= x)
{
len /= 2;
num *= 2;
}
int r = a[i] - len * num; // 剩余长度
// 特殊处理:如果len+1 == 2x,说明可以多分一次
if (len + 1 == 2 * x)
{
tot += num + r; // 可以多分出r个
}
else
{
tot += num; // 正常计数
}
}
return tot >= n + m; // 总片段数是否足够
}
signed main()
{
cin >> T; // 输入测试用例数
while (T--)
{
cin >> n >> m; // 输入原始数个数和需要新增的片段数
for (int i = 1; i <= n; i++) // 输入每个原始数
{
cin >> a[i];
}
// 二分查找最大的x
int l = 1, r = 1e9; // 二分边界
while (l < r)
{
int mid = (l + r + 1) / 2; // 向上取整
if (check(mid)) // 如果mid可行
{
l = mid; // 尝试更大的
}
else
{
r = mid - 1; // 尝试更小的
}
}
cout << l << endl; // 输出结果
}
return 0;
}
【运行结果】
3
5 2
5 6 7 14 17
5
4 3
444 444 44 444
44
2 2
100 222
55
浙公网安备 33010602011771号