目录

题目描述

991. 坏了的计算器

题意分析

屏幕上显示着一个数 startValue,只有两个按钮可用:一个把当前数乘以二,一个把当前数减去一。问最少按多少次按钮,能让屏幕显示 target

注意两个操作是不对称的:乘二只能让数变大(起始值为正),减一只能让数变小一格。所以要把小数变大只能靠乘二,而乘二的步长又是成倍的,很容易一下子跳过目标,此时又得用一连串减一往回蹭——什么时候乘、什么时候减,正是这道题的全部难点。

约束信号:两个数都能到 $10^9$。这个范围直接封死了在数值空间上做广度优先搜索的路子,状态数太多。最少操作数本身甚至可能接近 $10^9$——例如从 $10^9$ 减到 $1$;但这段连续减一可以直接用差值计算。真正需要选择操作的 target > startValue 情形,则提示存在不断折半的对数级构造。

边界要点清楚:startValue 可能已经大于或等于 target,此时乘二只会让差距更大,唯一的办法是一路减一,答案就是两者之差;target 也可能恰好等于 startValue,答案为 0。

解法:逆向贪心

核心思路

正着做很难下手。从 startValue 出发,每一步都有两个分支,而且乘二会让数越来越大、减一会让数越来越小,状态空间在正方向上根本没有上界,无法界定搜索范围,也说不清什么时候该停止乘二。

换个方向就豁然开朗:把两个操作反过来,从 target 走回 startValue。「乘二」的逆操作是「除以二」,「减一」的逆操作是「加一」。逆向之后,最关键的性质出现了——分支消失了

其一,若当前的 target奇数,它绝不可能是上一步乘二得到的(乘二的结果必为偶数),所以正向的上一步只能是减一,逆向就只能加一。这里没有选择余地。

其二,若当前的 target偶数且仍大于 startValue,那么除以二严格不劣于加一。证明如下:任何把 target 降到 startValue 的逆向方案中,至少要用一次除法(只加不除只会越来越大)。设第一次除法之前先做了 $k$ 次加一,那么代价是 $k+1$,结果是 $\frac{t+k}{2}$,而且为了能整除,$k$ 必须是偶数。换个顺序:先除一次再加 $\frac{k}{2}$ 次,代价是 $1+\frac{k}{2}$,结果同样是 $\frac{t}{2}+\frac{k}{2}=\frac{t+k}{2}$。两者结果完全相同,但当 $k>0$ 时 $1+\frac{k}{2} < k+1$,先除严格更省。所以偶数时闭着眼睛除就行。

其三,一旦 target 降到不大于 startValue,逆向就只剩加一可用(除以二只会让它更小,越走越远)。此时还差 startValue - target,就补这么多次加一。

于是维持这条不变量:循环的每一轮,当前的 target 值都是「从 startValue 出发按已计数的操作次数所能到达的、通往最优解的中间值」,且每一步的选择要么唯一、要么已被证明不劣。 既然每一步都不存在更优的替代选择,贪心走完得到的总步数就是最小值。

复杂度也随之明朗:奇数一步之后必然变偶数,偶数一步就折半,所以每两步至少让 target 减半,总轮数是对数级。

解题步骤

  • 初始化操作计数 ops = 0,进入 while (target > startValue) 循环。为什么循环条件是严格大于:只有当目标还在起点上方时,逆向才需要用到除法把它降下来;一旦不大于起点,后续操作完全确定,用一个减法算完即可,不必再循环。
  • target 是奇数,令 target++,计数加一。为什么只能加一:奇数不可能由乘二产生,正向的最后一步必定是减一,逆向别无选择。
  • target 是偶数,令 target 折半,计数加一。为什么不考虑先加再除:前面的交换论证表明,把加一挪到除法之后做,结果相同而代价更小,所以最优解里绝不会出现「偶数时先加一」。
  • 循环结束后返回 ops + (startValue - target)。为什么补的是差值:此时 target 已经不大于 startValue,逆向只剩加一可用,需要加 startValue - target 次;对应到正向,就是从 startValue 开始先做这么多次减一。注意 target 可能在最后一次折半时跌到 startValue 以下,这个差值可能大于 0,不能想当然认为它是 0。

startValue = 3target = 10 走一遍:初始 ops = 0。第一轮,$10 > 3$ 进入循环,10 是偶数,折半得 5,ops = 1。第二轮,$5 > 3$,5 是奇数,加一得 6,ops = 2。第三轮,$6 > 3$,6 是偶数,折半得 3,ops = 3。第四轮判断 $3 > 3$ 不成立,退出循环。返回 $3 + (3-3) = 3$。把这条路径翻回正向读:$3 \xrightarrow{\times 2} 6 \xrightarrow{-1} 5 \xrightarrow{\times 2} 10$,恰好三步,且可以验证两步做不到(两步最多只能到 $3\times2\times2=12$、$3\times2-1=5$、$(3-1)\times2=4$、$3-2=1$,都不是 10)。

再走一组会用到最后那个差值的 startValue = 5target = 8ops = 0,$8>5$ 且 8 是偶数,折半得 4,ops = 1;此时 $4 > 5$ 不成立,退出循环。返回 $1 + (5-4) = 2$。正向读作 $5 \xrightarrow{-1} 4 \xrightarrow{\times 2} 8$,两步完成。若忘了补上这个差值 1,就会错答成 1。

代码实现

// 若 target 为奇数,只能 +1。
class Solution {
    public int brokenCalc(int startValue, int target) {
        int ops = 0;
        while (target > startValue) {
            if ((target & 1) == 1) {
                target++;
            } else {
                target >>= 1;
            }
            ops++;
        }
        return ops + (startValue - target);
    }
}
// 若 target 为奇数,只能 +1。
func brokenCalc(startValue int, target int) int {
    ops := 0
    for target > startValue {
        if target%2 == 1 {
            target++
        } else {
            target /= 2
        }
        ops++
    }
    return ops + (startValue - target)
}

复杂度分析

  • 时间复杂度:当 target > startValue 时是 $O(\log target)$,因为每一到两轮至少会发生一次折半;当 target <= startValue 时是 $O(1)$。最后的 startValue - target 只做一次算术计算,并没有逐次模拟这些操作。
  • 空间复杂度:$O(1)$,全程只维护当前 target 和操作数 ops

关键点总结

  • 正向的“乘 2 / 减 1”不好选择,逆向后变成“偶数除 2 / 加 1”,奇偶性会强迫下一步,贪心选择因此是确定的。
  • target > startValue 且为偶数时直接折半;为奇数时必须先加一把它变成偶数。
  • 一旦 target <= startValue,逆向只剩连续加一,次数可直接用 startValue - target 计算,无需模拟。
  • 面试时应主动给出交换论证:偶数目标若先加一,会变成奇数而无法除二;任何多余的加一都不会优于立刻折半。

易错点总结

  • startValue 正向贪心:例如 startValue = 5, target = 8,先乘二到 10 再减两次共 3 步并不是最优;最优是先减到 4,再乘二到 8,只要 2 步。正向的局部规则很难证明,逆向选择才由奇偶性唯一决定。
  • 奇数目标直接除以 2:整数除法会丢失信息。target = 9 的逆向第一步只能是 +1 到 10,再除以 2;直接变成 4 对应不到任何合法正向操作。
  • 循环条件写成 target != startValue:当最后一次除法把目标降到起点以下时会继续错误折半甚至死循环;应在 target <= startValue 时一次性补差值。
  • 忘记给奇数的 +1 计数startValue = 3, target = 10 应走 10 -> 5 -> 6 -> 3 共 3 步,漏计会答成 2。

相似题目

题目 难度 与本题的联系
397. 整数替换 中等 同样依据奇偶性逆向缩小整数,但奇数时有两个候选
1342. 将数字变成 0 的操作次数 简单 奇偶性直接决定操作,可练习按位分析
1658. 将 x 减到 0 的最小操作数 中等 也通过等价变换降低选择难度,但转化为最长子数组问题