题解:学而思编程 折半与最小值

【题目来源】

学而思编程:折半与最小值

【题目描述】

\(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

【核心思想】

  1. 问题分析:给定 \(n\) 根木棒,长度分别为 \(a_i\),进行 \(m\) 次操作(每次将一根长度 \(> 1\) 的木棒折半分成两根)。最终得到 \(n + m\) 根木棒,需要最大化这些木棒中最小长度 \(x\)。关键观察是:若要求所有木棒长度 \(\geq x\),则每根原始木棒 \(a_i\) 最多可以被分割成若干段,每段长度 \(\geq x\)。这是一个二分答案 + 贪心分割问题。

  2. 算法选择

    • 整数二分:对最小长度 \(x\) 进行二分,将最优化问题转化为判定问题
    • 贪心分割判定:对于给定的 \(x\),计算每根木棒最多能分割成多少段长度 \(\geq x\) 的片段,判断总片段数是否 \(\geq n + m\)
  3. 关键步骤

    • 判定函数 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\)
    • 二分查找
      • 范围 \([1, 10^9]\),取上中点 \(mid = \lfloor \frac{l + r + 1}{2} \rfloor\)
      • check(mid) 为真:\(l = mid\)(尝试更大的最小值)
      • 否则:\(r = mid - 1\)
    • 输出答案\(l\)
  4. 时间/空间复杂度

    • 时间复杂度:\(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)\),存储木棒长度数组
  5. 二分答案 + 贪心分割的核心思想

    • 单调性利用:若最小长度 \(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
posted @ 2026-07-02 22:45  团爸讲算法  阅读(5)  评论(0)    收藏  举报