深蓝黑背景品牌封面:左侧大标题「整数变 1 的最少操作」,副标题「贪心为什么成立」;右侧原创装饰——数字 14 经三条运算路径逐步收敛到 1(14→7→6→3→2→1),每步标注 ÷2 或 −1,末端数字 1 高亮;左上标注「经典题解析 · 算法」,右上标注「Day 07 · 10-05」 图 1:本文封面,点明题目与「贪心为什么成立」的论证要求。

先说明属性:这是一篇经典题解析,题目出自本地历史面试资料,与任何公司的当前招聘无关。题面很短:给一个正整数 n,每一步如果 n 是偶数就除以 2,如果是奇数可以选择加 1 或减 1,一直处理下去,问最少多少步能变成 1。

面试现场,面试官写完规则笑而不语。不少人第一反应是反向枚举——从 n 开始做 DFS 或 BFS,把每个奇数加 1、减 1 的两条分支都走一遍取最小值。能算对,但分支指数膨胀,n 一大就超时。面试官真正想等的是:能不能不枚举,O(log n) 一步一条路直接走到底?

核心结论:可以,用贪心。

规则只有三行:偶数直接除 2;奇数如果等于 3 或者模 4 余 1,就减 1;其余奇数(模 4 余 3)加 1。沿这条规则走,每步都是局部最优,总步数就是全局最优。

为什么偶数一定除 2?

奇数加减 1,本质都是在为下一步的除法铺路。偶数除 2,一步就把二进制长度砍掉一位;如果放着除法不用先去做加减,得先花一步把偶数变奇数,再花一步变回偶数才能除——两步干别人一步的事,永远不划算。所以「偶数必除 2」没有争议,争议全在奇数。

奇数为什么看模 4?

奇数加减 1 之后必成偶数,下一步一定除 2。真正的分歧在于除完这一次的落点:n 减 1 再除 2 得 (n−1)/2,n 加 1 再除 2 得 (n+1)/2。选对了,落点继续是偶数,白赚一次连续除 2。n 模 4 余 1 时,n−1 是 4 的倍数,能连除两次;n 模 4 余 3 时,n+1 是 4 的倍数,同样能连除两次。所以局部最优的依据一句话:让下一步多除一次 2。

唯一例外是 3。按规则它模 4 余 3 应该加 1:3→4→2→1 共三步;而减 1 只要 3→2→1 两步。终点附近「连除两次」的红利来不及兑现,所以特判。

换成二进制视角,规则更直观。

除 2 就是右移一位。偶数末尾是 0,右移没成本。奇数末尾是 1,必须先消掉:末尾只有一个 1(形如 …01)时减 1 最省;末尾连着多个 1(形如 …0111)时加 1 引发进位,一次把一串 1 全消掉。贪心成立的根子还是那句话:每一步都让二进制长度缩短得最快。

写出可运行的代码。

下面这份实现附带 14 的走查和 1 到 2000 的 BFS 对拍,用 Node.js 22.15.0 直接运行(node integer-replacement.js)即可:

'use strict';

// 贪心:偶数除 2;奇数看模 4 选加减,唯一例外是 3
function minOps(n) {
  let ops = 0;
  while (n > 1) {
    if (n % 2 === 0) n /= 2;
    else if (n === 3 || n % 4 === 1) n -= 1;
    else n += 1;
    ops += 1;
  }
  return ops;
}

// 走查:14 的最短路径
const path = [];
for (let n = 14; n > 1;) {
  path.push(n);
  if (n % 2 === 0) n /= 2;
  else if (n === 3 || n % 4 === 1) n -= 1;
  else n += 1;
}
path.push(1);
console.log('14 的路径:', path.join(' → '), ',共', path.length - 1, '步');

// 对拍:BFS 暴力枚举,验证 1..2000 内贪心结果全部正确
function bfs(n) {
  const dist = new Map([[1, 0]]);
  const queue = [1];
  while (queue.length) {
    const cur = queue.shift();
    if (cur === n) return dist.get(cur);
    // 反向 BFS:cur 的邻居是 2*cur(正向的除 2),以及 cur 为偶数时的 cur±1(正向的奇数加减 1)
    const next = [2 * cur, ...(cur % 2 === 0 ? [cur - 1, cur + 1] : [])];
    for (const m of next) {
      if (m >= 1 && m <= 2 * n && !dist.has(m)) {
        dist.set(m, dist.get(cur) + 1);
        queue.push(m);
      }
    }
  }
  return -1;
}

let bad = 0;
for (let i = 1; i <= 2000; i++) {
  if (minOps(i) !== bfs(i)) bad += 1;
}
console.log('1..2000 与 BFS 对拍完成,不一致个数:', bad);

实测输出:

14 的路径: 14 → 7 → 8 → 4 → 2 → 1 ,共 5 步
1..2000 与 BFS 对拍完成,不一致个数: 0

两千个数字与暴力 BFS 全部一致,贪心正确性有了实证。走查里 7 是个有意思的并列点:减 1 走 7→6→3→2→1、加 1 走 7→8→4→2→1 都是 4 步,贪心规则选择了后者。

解法图:规则是偶数可以除 2,奇数只能加 1 或减 1,求变到 1 的最少操作数。左上是贪心决策:偶数直接除 2(除 2 一步顶加减两步的效果,永远最优);奇数看除 2 之后的落点——n 减 1 后是 4 的倍数就走减 1,n 加 1 后是 4 的倍数就走加 1,因为能让下一步连续除两次 2;唯一例外是 n=3 时减 1 更快(3→2→1 两步,加 1 要三步)。右侧用二进制视角解释:除 2 是右移一位,加 1 或减 1 是为了消掉末尾的 1;末尾是 0 就右移,末尾连续多个 1 时加 1 能一次消一串,末尾只有一个 1 时减 1 更省。示例 14(1110)→7(111)→加 1 得 8(1000)→连除三次到 1,共 5 步。底部对比:倒推贪心 O(log n),正向 DFS/BFS 枚举加减分支要指数级,论证贪心成立的关键是「每步都让二进制长度缩短得最快」 图 2:贪心决策规则与二进制视角的论证。

可能的追问。

「时间复杂度多少?」每次操作至少把二进制右移一位,O(log n)。「为什么不从 1 反向贪心?」正向除法有确定性,反向的乘 2 与加减没有等价的局部判据,容易把自己绕回枚举。「n 超出普通整数范围呢?」规则只依赖奇偶和模 4,用任意精度整数照样成立,每步只看最低两位。

易错点清单。

一,对奇数无脑选减 1,在 7、11 这类数上就不是最优。二,忘记特判 3,输出 3 步而不是 2 步。三,把 BFS 当满分答案讲,没意识到面试官在等贪心论证。四,只背规则说不清「为什么看模 4」,被追问一句就露馅。

回到面试现场,建议的答题节奏:先给出「偶数必除 2」的无争议部分,再把奇数的选择归约到「让下一步多除一次 2」,点出模 4 判据和 3 的例外,最后用二进制视角收尾。贪心题的高分从来不在代码,而在「为什么局部最优就是全局最优」的论证。

你在面试里被要求证明过贪心正确性吗?是讲清楚了还是当场卡壳?欢迎分享。