Skip to content

金条切割 - 题解

标签与难度

标签: 数学, 位运算, 二进制, 贪心, 思维题, 脑筋急转弯 难度: 1200

题目大意喵~

主人,你好呀~!这道题是说,我们有一根长度为 LL 的金条,需要支付给一个工人接下来 MM 天的工资,喵。规则很特别哦:在第 jj 天结束的时候,工人手里必须正好有 jj 个单位的金子。我们可以像变魔术一样,给工人金子,也可以从工人那里把之前给过的金子要回来“找零”。我们的任务是,计算出最少需要把金条切多少刀,才能完成这 MM 天的支付任务,呐。

举个栗子,如果要支付7天的工资,我们可以把金条切成1、2、4三段(切2刀)。

  • 第一天:给工人1
  • 第二天:给工人2,要回1。工人手里有2
  • 第三天:再给工人1。工人手里有12,总共3。 ...以此类推,我们总能凑出当天需要的数量,喵~

解题思路分析

这道题的核心思想,就像题目里提示的那样,和二进制有着奇妙的联系哦,喵!

1. 如何凑出任意金额?

要能够在第 jj 天让工人拥有 jj 单位的黄金,意味着我们需要有能力凑出从 11MM 的任意一个整数。怎样用最少的金条块来做到这一点呢?

答案就是使用2的幂作为金条块的重量!也就是 20,21,22,23,2^0, 2^1, 2^2, 2^3, \dots1,2,4,8,1, 2, 4, 8, \dots。 为什么这样最有效率呢?因为任何一个正整数都可以被唯一地表示为一系列2的幂的和。这正是我们计算机世界里二进制表示法的基础呀,喵~

比如,数字 1313 的二进制是 1101,它就等于 8+4+0+18 + 4 + 0 + 1,也就是 23+22+202^3 + 2^2 + 2^0。如果我们手上有重量为1, 2, 4, 8的金条块,就可以轻松凑出1到15之间的任何重量啦!

2. 需要多少块金条?

假设我们需要 k 块金条,它们的重量分别是 20,21,,2k12^0, 2^1, \dots, 2^{k-1}。利用这些金条,我们能凑出的最大金额是它们全部加起来的总和:

Smax=i=0k12i=2k1S_{max} = \sum_{i=0}^{k-1} 2^i = 2^k - 1

为了能支付到第 MM 天,我们凑出的最大金额必须至少是 MM。所以,我们需要找到一个最小的 k,使得:

2k1M2^k - 1 \ge M

这个 k 就是我们为了完成任务,理论上需要的最少金条块数

3. 从“块数”到“切割次数”

找到了需要的块数 k,是不是切 k-1 刀就完事了呢?嘿嘿,这里有个小小的陷阱,就是金条的总长度 LL 的说。

我们来分两种情况讨论一下,喵~

情况一:金条很长,足够我们“挥霍” (L > 2^k - 1)

我们需要的理想金条块是 1,2,4,,2k11, 2, 4, \dots, 2^{k-1},它们的总长度是 2k12^k - 1。 如果我们的金条总长度 LL 比这个理想总长大,那真是太棒啦!我们可以从金条上依次切下这 k 块。切完之后,金条还会剩下一段,长度是 L(2k1)L - (2^k - 1)。 所以,我们最终得到了 k 块理想金条 + 1 块剩余金条 = k+1 块。 要把一根金条变成 k+1 块,需要切多少刀呢?当然是 k 刀啦!

情况二:金条长度有限 (L <= 2^k - 1)

如果金条的总长度 LL 并不比我们理想中需要的 2k12^k-1 更长,我们就没办法切出所有 1,2,4,1, 2, 4, \dots 的理想金条块了。 但是,我们真的需要这些理想的金条块吗?不一定!我们只需要保证手头的金条块能凑出 11MM 的任意整数。 要凑出 MM 这么多种可能,我们分析过至少需要 k 块金条。(如果只有 k-1 块,最多只能凑出 2k112^{k-1}-1 的金额,这是小于 MM 的,不够用哦)。 所以,我们还是需要把现有的长度为 LL 的金条分成 k 块。比如,我们可以切出 1,2,4,,2k21, 2, 4, \dots, 2^{k-2},然后最后一块就是剩下的所有,即 L(2k11)L - (2^{k-1}-1)。用这 k 块,我们同样可以完成任务。 要把一根金条分成 k 块,只需要切 k-1 刀就够了,喵~

总结一下思路:

  1. 找到满足 2k1M2^k - 1 \ge M 的最小整数 k。这个 k 就是我们最少需要的金条块数。
  2. 计算出这 k 块理想金条的总长度 S=2k1S = 2^k - 1
  3. 比较我们拥有的金条总长度 LL 和理想总长度 SS
    • 如果 L>SL > S,说明我们切完理想金条后还有剩余,总共会产生 k+1 块,所以需要切 k 刀。
    • 如果 LSL \le S,说明我们的金条要么刚刚好,要么不够长来切出所有理想块。但我们依然需要 k 块来保证支付能力,所以我们把整根金条切成 k 块,只需要 k-1 刀。

这样一来,问题就迎刃而解啦!

代码实现

这是我根据上面的思路,精心为你准备的一份C++代码,注释超详细的哦,喵~

cpp
#include <iostream>

// 我是我,我来解决这个问题,喵~
void solve() {
    long long len, m;
    std::cin >> len >> m;

    // Step 1: 寻找最小的块数 k, 使得用这 k 块可以凑出 1 到 m 的所有金额
    // 最优策略是使用 2 的幂次作为块的大小 (1, 2, 4, ...)。
    // k 块这样的金条能凑出的最大金额是 2^k - 1。
    // 我们需要找到最小的 k,满足 2^k - 1 >= m。
    int k = 0;
    long long max_payable_amount = 0;
    
    // 这个循环会找到最小的 k
    // k=1, max_payable_amount = 1
    // k=2, max_payable_amount = 3
    // k=3, max_payable_amount = 7
    // ...
    // 循环条件 `max_payable_amount < m` 确保了我们找到的 k 是最小的那个
    while (max_payable_amount < m) {
        k++;
        // max_payable_amount = 2 * max_payable_amount + 1; // 这样可以避免使用pow和位移
        // 使用位运算更酷一些,也更高效!1LL << k 表示 2^k,LL确保是long long类型
        max_payable_amount = (1LL << k) - 1;
    }

    // Step 2: 根据金条总长度 len 和理想总长 max_payable_amount 的关系,决定切割次数
    
    // 理想情况下,我们需要 k 块金条,它们的总长度是 max_payable_amount (也就是 2^k - 1)。
    long long ideal_sum_of_pieces = max_payable_amount;

    // Case 1: 如果我们的金条长度 > 理想总长
    // 我们可以切出 1, 2, 4, ..., 2^(k-1) 这 k 块,并且还有剩余。
    // 总共就会有 k (理想块) + 1 (剩余块) = k+1 块。
    // 把一根金条切成 k+1 块,需要切 k 刀。
    if (len > ideal_sum_of_pieces) {
        std::cout << k << std::endl;
    } 
    // Case 2: 如果我们的金条长度 <= 理想总长
    // 我们无法切出全部的理想块,或者刚好切完。
    // 但我们仍然需要 k 块金条来保证支付能力(k-1块是不够的)。
    // 所以我们把长度为 len 的金条切成 k 块。
    // 把一根金条切成 k 块,需要切 k-1 刀。
    else { // len <= ideal_sum_of_pieces
        std::cout << k - 1 << std::endl;
    }
}

int main() {
    // 加速输入输出,让程序跑得像猫一样快!
    std::ios_base::sync_with_stdio(false);
    std::cin.tie(NULL);

    int t;
    std::cin >> t;
    while (t--) {
        solve();
    }

    return 0;
}

复杂度分析

  • 时间复杂度: O(logM)O(\log M) 对于每一组测试数据,我们主要的操作是那个 while 循环。由于 m 最大约为 101810^{18},而 2601.15×10182^{60} \approx 1.15 \times 10^{18},所以循环变量 k 最多只会增加到60左右。因此,循环的次数非常少,其复杂度与 MM 的二进制位数成正比,即 O(logM)O(\log M)

  • 空间复杂度: O(1)O(1) 在整个计算过程中,我们只使用了几个变量(len, m, k, max_payable_amount等)来存储信息,没有使用任何随输入规模增大的数据结构。所以,空间复杂度是常数级别的,喵~

知识点总结

这道题虽然伪装成了一个脑筋急转弯,但背后考察的是非常扎实的计算机科学基础知识,呐:

  1. 二进制思想: 解决“用最少数量的物品凑出一定范围内的所有数值”这类问题的万能钥匙,就是使用2的幂次作为物品的“面值”。
  2. 贪心策略: 我们总是贪心地选择能覆盖当前需求的最小块数 k
  3. 分类讨论: 解题的关键一步是根据金条总长度 L 是否足够切出所有“理想”金块,来对最终的切割次数进行分类。这个细节决定了答案是 k 还是 k-1
  4. 编码技巧: 在C++中,计算 2k2^k 时,使用位运算 1LL << k 比使用 pow 函数更高效,并且可以避免浮点数精度问题。

希望我的讲解对你有帮助哦!继续加油,算法的世界还有更多有趣的冒险等着我们去探索,喵~!