Python技术迷

汇丰面试通过,给了offer。。

刚看到个贴子,说网友拿到了汇丰的 offer,但现在在外包公司干得挺舒服,节奏合适,不加班,就是待遇差点。汇丰这边催着一周内入职,他有点纠结,不知道该不该提离职。

Image

很多人都卡在“舍不得现在的安稳,又怕错过更好的机会”。网友有的劝他“稳住别动”,也有的说“汇丰这种机会不多,赶紧走”。但我想说,选择其实不在公司,而在你自己想要什么。

如果你现在追求的是稳定,那就安心留下;但如果你想往上走、想要更多见识和资源,那就得敢迈出去。毕竟成长从来都不是舒适带来的。外包没错,但长期待着也容易被温水煮熟。

勇敢点,做对自己未来有利的决定。【备注:文末可领最新资料】

面试题:统计 K-Free 子集的总数

就这么理解:给你一堆整数 nums 和一个 k,挑一个子集出来,要求不能同时包含数对 (x, x+k)(等价地,任意两个数差值不能正好是 k)。统计一共有多少种选法。一般默认把空集也算一种,我后面会把“含不含空集”做成参数,别纠结。

按“同余类”分组 + 打家劫舍式 DP

关键观察:如果把所有数按 v % k 分成 k 个桶,比如 k=3 时就是三个桶 {…, -3,0,3,6,…}、{…, -2,1,4,7,…}、{…, -1,2,5,8,…}。 为什么要这么干?因为只有同一个桶里的数才可能出现差值为 k 的冲突(相差一个台阶),不同桶彼此互不影响,最后把各桶的方案数相乘就行了。

桶内再干啥?把这个桶内出现过的值按大小排序,注意同一个值可能出现多次,记它的频次是 f。对同值位置,我们要么不选(1 种),要么从这 f 个里选至少一个(2^f - 1 种)。但一旦在位置 i 选了,就不能在位置 i+1(值恰好大 k)再选,这不就像“打家劫舍”相邻不能同时偷嘛。

于是来两个状态滚动一下:

  • skip:到当前位置不选它的方案数
  • take:到当前位置选择它(选至少一个)的方案数(只能从上一个位置的 skip 转移)

转移式很顺:

take_new = skip * (2^f_i - 1)
skip_new = (skip + take) * 1

这一小段做完,桶内总数就是 skip + take。所有桶相乘就是总答案;如果你不想把空集算进去,最后减个 1。

边界与细节

  • 负数、重复数都没问题,分桶用 v % k(Python 的取模对负数也稳定)。
  • 复杂度:排序主导,每个桶对自己那部分排序,整体 O(n log n);额外是哈希统计次数。
  • 大数取模:常见题会让你 mod 1e9+7,我也留了参数。
  • 空集:include_empty=True/False 随你。
from collections import Counter, defaultdict
from typing import List

MOD = 10**9 + 7

defcount_k_free_subsets(nums: List[int], k: int, include_empty: bool = True, mod: int = MOD) -> int:
if k == 0:
# k=0 的特殊情况:不能同时包含 (x, x) —— 也就是同一个值最多选一次
# 每个不同的值只有两种:选或不选
        kinds = len(set(nums))
        ans = pow(2, kinds, mod)
ifnot include_empty:
            ans = (ans - 1) % mod
return ans

# 1) 统计频次
    freq = Counter(nums)

# 2) 按 v % k 分桶
    buckets = defaultdict(list)
for v in freq:
        buckets[v % k].append(v)

defbucket_ways(values: List[int]) -> int:
        values.sort()
        skip, take = 1, 0# 空开始
        i = 0
        n = len(values)
while i < n:
            v = values[i]
# 把相同的值并成一段,频次合并
            f = freq[v]
            j = i + 1
# 同一个桶里,相邻可冲突的只有 v 和 v+k;同值直接合并频次
while j < n and values[j] == v:
                f += freq[values[j]]
                j += 1

            ways_nonempty = (pow(2, f, mod) - 1) % mod

# 看下一个不同值是否正好等于 v + k(冲突)
            nxt_is_adj = (j < n and values[j] == v + k)

if nxt_is_adj:
# 正常转移
                take_new = (skip * ways_nonempty) % mod
                skip_new = (skip + take) % mod
                skip, take = skip_new, take_new
else:
# 后面不是相邻(或者没有后面),这里可以把这一位“打包结算”进总 ways:
# 当前段完成后,相当于把 (skip,take) 与 当前节点的 {不选(1), 选(ways_nonempty)} 合并,
# 且后续不会受相邻约束影响,直接一次性乘上即可。
                total_here = (1 + ways_nonempty) % mod
                merged = (skip + take) % mod
                skip, take = (merged * total_here) % mod, 0
            i = j

return (skip + take) % mod

    ans = 1
for _, vals in buckets.items():
        ans = (ans * bucket_ways(vals)) % mod

ifnot include_empty:
        ans = (ans - 1) % mod
return ans

# 小测一下
if __name__ == "__main__":
    print(count_k_free_subsets([1,2,3], 1, include_empty=True))   # 5:{}, {1},{2},{3},{1,3}
    print(count_k_free_subsets([1,1,3], 2, include_empty=True))   # 7:两次1独立选择,3独立选择

什么时候会翻车

  • k 太大(远大于数值跨度)时,其实几乎没有冲突,答案基本是 2^{去重或特殊处理后的规模};
  • k=0 别忘了特判;
  • 有取模时,记得所有加减乘都 % mod,特别是 (2^f - 1) 可能是负数,要再 % mod 一次。

就这样,用“分桶 + 相邻互斥”的直觉去想,很顺。你要是有个数据样例想对拍,丢过来我给你跑一眼。

-END-

我为大家打造了一份RPA教程,完全免费:songshuhezi.com/rpa.html

🔥虎哥私藏精品🔥

虎哥作为一名老码农,整理了全网最全《python高级架构师资料合集》,总量高达650GB,点击下方公众号回复关键字 python 全部免费领