Open
Conversation
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
LeetCode 1404. Number of Steps to Reduce a Number in Binary Representation to One
题目信息
文件位置
include/leetcode/problems/number-of-steps-to-reduce-a-number-in-binary-representation-to-one.hsrc/leetcode/problems/number-of-steps-to-reduce-a-number-in-binary-representation-to-one.cpptest/leetcode/problems/number-of-steps-to-reduce-a-number-in-binary-representation-to-one.cpp我刚看到这道题时,脑子里闪过的第一个念头是:"这不就是个简单模拟吗?" 把二进制字符串转成整数,然后按照规则一步步算,直到变成 1,计数器++,完事。
但等等,瞟一眼约束条件——字符串长度最长 500。500 位的二进制数!这比$2^{500}$ 还大,连 unsigned long long 都得哭出声。好吧,这题在故意堵死我们的捷径。我们必须得在字符串上直接操作,像手工算二进制那样。
这题到底在问什么?
本质上,我们要模拟一个极其特定的计算过程:
但这里的陷阱是,那个"加 1"操作在二进制里不是 O(1) 的。想象一下
1111 + 1,会变成10000,进位像多米诺骨牌一样一路向左推。第一次尝试:从右往左的直觉
既然不能转成数字,那我们就在字符串上模拟。我本能地想:从字符串末尾(最低位)开始往左看,维护一个
carry(进位)标志。让我画个图看看状态。假设我们在处理某一位
s[i],同时有个来自低位的进位carry:这看起来对。等等,如果
carry是 1,而当前位也是 1,那真实值就是 2(偶数)?不对,二进制位只能是 0 或 1,加上进位 1,真实值可以是 0、1、2。让我重新梳理。当
carry=1时,意味着低位加法给我们留了个"欠债",当前位实际上被加了 1。具体走一遍 "1101"(十进制的 13)
真实的手算过程应该是:
1101(13, 奇数) → 加 1 →1110(14) → 步数 11110(14, 偶数) → 除 2 →111(7) → 步数 2111(7, 奇数) → 加 1 →1000(8) → 步数 31000(8, 偶数) → 除 2 →100(4) → 步数 4100(4, 偶数) → 除 2 →10(2) → 步数 510(2, 偶数) → 除 2 →1(1) → 步数 6总共 6 步。
现在用我的"从右往左+进位"思路模拟。注意,我们是从字符串末尾向开头遍历,但逻辑上是在模拟"当前数字"的演变。
初始化
carry = 0,steps = 0从右往左(除了最左边的 '1',因为我们要变成 1,不是 0):
i=3 (最右的 '1'):
i=2 ('0'):
i=1 ('1'):
这里我搞混了。当真实值是 2(二进制
10),除以 2 应该得到 1,没有余数,但会产生一个向高位的进位 1?不,除以 2 就是右移,就是把这一位去掉,把进位留给下一位。让我重新思考进位的含义。当我们说"加 1 后除以 2",对于奇数
...x1:...(x+1)0...(x+1)(右移)所以原来这一位的 1 变成 0 被扔掉了,而高一位的 x 变成了 x+1。
因此,当处理第 i 位时,如果它是奇数(考虑进位后),我们需要:
如果它是偶数:
修正后的 walkthrough for "1101":
从右到左遍历,i 从 len-1 到 1(不包括最左边的位,因为我们要留一个 1):
现在处理最高位(i=0, s[0]='1'):
总步数 = 5 + 1 = 6。✓
Ah-ha! 那个微妙的边界
但是等等,我在第一次写代码时踩了个坑。看这段逻辑:
如果
lastBit是 2(二进制10),我们需要:所以只需要 1 步,不是 2 步!
只有当最高位加进位后变成 1(即
lastBit == 1),我们才不需要任何步骤,因为它已经是 1 了。最终代码的构建
现在代码的结构清晰了。我们从右往左遍历到 index 1,根据当前位加进位的奇偶性决定步数,并更新进位。
等等,上面的偶数分支逻辑有点乱。让我简化。实际上,我们可以统一处理:
如果
bit是 0:偶数,1 步,carry 变 0。如果
bit是 1:奇数,2 步,carry 变 1(因为 1+1=2,进位)。如果
bit是 2:偶数(1+1),1 步,carry 变 1(因为 2/2=1,进位到更高位)。哦,原来
bit == 2时,carry 应该保持 1!因为这一位虽然被处理了,但产生的 1 要传给更高位。所以更简洁的写法:
或者更简洁,因为
bit只能是 0、1、2:魔鬼细节:为什么要单独处理最高位?
想象一下字符串 "1"。按照循环
i > 0,循环不会执行。lastBit = 1 + 0 = 1,不需要额外步骤。返回 0。正确。想象 "10"(2):
想象 "11"(3):
如果我们在循环里不小心把 i=0 也处理了,会发生什么?对于 "1",我们会试图处理它,然后可能错误地加步骤。或者对于 "11",我们可能在处理完 i=0 后,还试图
lastBit处理,导致重复计算。这就是为什么我们要在循环条件里写
i > 0,把最高位留出来做特殊判断。那道进位的幽灵
这题最精妙的部分在于理解:当我们对一个奇数做"加 1 再除 2"的操作时,实际上等价于:
这个进位可能会连锁反应。比如遇到
...0111:这种连锁反应正好被我们的
carry变量优雅地捕捉了,而不需要真的去修改字符串。** takeaway:反向思考与状态机**
这道题教会我们什么?当正向模拟(从高位到低位,或从数字本身出发)遇到困难(数字太大)或过于复杂(频繁的字符串修改)时,试试反向思考(从低位到高位),并引入状态(carry)来记录"历史遗留问题"。
很多字符串/数字处理问题都有这个模式:看起来需要原地修改数组,实际上只需要一个变量记录"欠账"或"余量",就能在 O(1) 空间内解决。
本报告由 AI 自动生成。