Python技术迷

某大厂员工吐槽:单位新来的00后嫌椅子不舒服,自己买了把一千多人体工学椅,老员工看到后沉默了,自己忍了十几年只买了靠垫!

刚刷到这个帖子我真是一下就乐了,太像办公室连续剧了。

新来的00后嫌公司椅子坐着难受,转头自己下单一把一千多的人体工学椅,动作干脆得像在修自己bug。旁边那帮老员工估计表情都挺复杂,先“啧啧啧”三声,随后集体安静。

Image

老员工不是不难受,是真忍习惯了,腰不行了买个靠垫,脖子酸了贴个膏药,主打一个能凑合就凑合。结果00后上来直接告诉大家,难受就换,别硬扛。你说他娇气吧,人家也没花公司钱。你说他不懂职场吧,问题是他这一下,反倒把“忍了十几年”这件事衬得有点心酸了。


算法题:最小好进制

这题第一次看,很多人会往进制转换上硬怼,结果很快就把自己绕进去了。

n = "13",答案是 3,因为 13 = 111(3)。n = "4681",答案是 8,因为 4681 = 11111(8)。

看到这里,直觉一般有两种:一种是枚举进制,一种是想着怎么把十进制转成别的进制去验证。前一种会超时,后一种大概率也不轻松。这个题麻烦的地方,不在“转”,而在“1 的个数到底有多少”。写这种题,别上来就扫进制,我一般先盯式子。

所谓“好进制”,意思是这个数在某个进制 k 下,表示出来全是 1。那它一定长这样:

n = 1 + k + k**2 + ... + k**m

这里的 m 表示最高位的指数,也就是说,总共有 m + 1 个 1。

这一下就不是进制题了,变成了等比数列求和题。再往前走一步:

n = (k ** (m + 1) - 1) // (k - 1)

但公式写出来不等于题就解了。坑在这里:k 不知道,m 也不知道。两个未知数,不能乱枚举。

这时候就该先收缩范围。 如果一个数写成全 1 的形式,位数越多,进制一定越小。题目要求最小好进制,本质上就是:尽量让 1 的个数更多。

所以我会先枚举位数,也就是枚举 m,从大到小试。为什么能这么干?因为一旦某个更长的长度成立,它对应的进制一定更小,直接就是答案。

m 的上界也不大。最极端的情况是进制为 2,这时候:

n = 1 + 2 + 4 + ... + 2**m

所以 m < log2(n)。题目里 n 是字符串,但转成整数后这个范围完全可控。

剩下的事就简单了: 固定一个 m,在区间里二分 k。因为随着 k 变大,1 + k + k^2 + ... + k^m 也单调变大,天然适合二分。

代码我更喜欢这么写,不直接套求和公式,现场一项一项累加,顺手还能控溢出,判断也更稳:

classSolution:
defsmallestGoodBase(self, n: str) -> str:
        num = int(n)
        max_m = num.bit_length() - 1# 相当于 floor(log2(num))

for m in range(max_m, 1, -1):
            left, right = 2, int(num ** (1 / m)) + 1

while left <= right:
                k = (left + right) // 2

                total = 1
                cur = 1
for _ in range(m):
                    cur *= k
                    total += cur
if total > num:
break

if total == num:
return str(k)
elif total < num:
                    left = k + 1
else:
                    right = k - 1

return str(num - 1)

最后这个 return str(num - 1) 很关键,算是兜底。

因为任何一个数 n,至少都可以写成 11 这种形式,也就是:

n = 1 * (n - 1) + 1

所以进制 n - 1 一定是合法答案。前面如果没找到更长的全 1 表示,那就只能退回这个最短的两位 11。

这题真正的转折点,不是想到“二分”,而是先把“最小进制”翻译成“最多位的 1”。这一步想明白,后面基本就是标准动作了。

很多算法题就是这样,表面问你 A,实际得先把它改写成 B。不改写,代码写得越勤,离答案越远。这个题就挺典型。