Python技术迷

公司宣布降薪20%,同事因房贷压力拒绝接受,结果领导却说:不是非要大家接受,不接受可以选择走人

大厂直接宣布降薪20%,这数字听着就肉疼。一个同事当场说不接受,理由也很简单,房贷在那摆着呢,每个月银行又不会因为你公司困难就少扣点。

结果领导来一句:不是非要大家接受,不接受可以选择走人。

Image

这话一出,味儿就对了。前面还装得像商量,后面直接把门指给你看。你以为是在征求意见,其实人家是在通知结果。

最扎心的是,打工人哪有那么多选择。接受吧,生活质量往下砍一刀;不接受吧,简历还没投出去,房贷先到期。领导嘴上轻飘飘一句“可以走人”,底下人脑子里已经开始算这个月还剩多少钱了。

这种时候才发现,公司谈困难,员工谈生活,根本不是一个频道。

算法题:最长重复子数组

两个数组一丢进来,结果超时。

我第一眼看到“最长重复子数组”这个题,就不太想用那些花里胡哨的哈希、后缀数组。不是不能用,是容易把一个本来很直的题写歪。

题目要的是:两个数组里,连续相同的一段,最长有多长。

注意,是连续。

这个地方很多人会顺手写成最长公共子序列,那就偏了。子序列可以跳着拿,子数组不行,断了就得重新算。

比如:

a = [1, 2, 3, 2, 1]
b = [3, 2, 1, 4]

答案是 3,因为 [3, 2, 1] 在两个数组里都连续出现过。

如果你写双指针从头扫,基本扫不出来。因为两个数组的起点不一定对齐,真正匹配的地方可能藏在中间。

最容易想到的暴力写法,大概是这样:

defslow_check(a, b):
    best = 0

for i in range(len(a)):
for j in range(len(b)):
            step = 0
while i + step < len(a) and j + step < len(b):
if a[i + step] != b[j + step]:
break
                step += 1

if step > best:
                best = step

return best

这代码没毛病,小数据还能跑。

但我一般看到三层味道就会有点警惕。外面两层枚举起点,里面 while 继续往后比。数组一长,时间直接炸。不是 Python 慢,是这个思路本身就不太行。

这个题更像一个现场排查问题:你不要反复从头验证,你要把已经验证过的结果接住。

看这个状态:

如果 a[i] == b[j],并且它们前一个位置也能接上,那以 a[i] 和 b[j] 结尾的最长公共连续段,就是:

dp[i][j] = dp[i - 1][j - 1] + 1

如果不相等,直接断。

dp[i][j] = 0

这个“断”很关键。最长公共子序列那里不相等还能从左边、上边拿结果,这里不行。连续数组断了就是断了,别舍不得。

写成 Python,我会这么落:

deflongest_repeated_subarray(nums1, nums2):
    n = len(nums1)
    m = len(nums2)

    dp = [[0] * (m + 1) for _ in range(n + 1)]
    best = 0
    end_at_nums1 = -1

for i in range(1, n + 1):
        x = nums1[i - 1]

for j in range(1, m + 1):
if x == nums2[j - 1]:
                dp[i][j] = dp[i - 1][j - 1] + 1

if dp[i][j] > best:
                    best = dp[i][j]
                    end_at_nums1 = i - 1

if best == 0:
return0, []

    start = end_at_nums1 - best + 1
return best, nums1[start:end_at_nums1 + 1]

跑一下:

nums1 = [1, 2, 3, 2, 1]
nums2 = [3, 2, 1, 4, 7]

length, arr = longest_repeated_subarray(nums1, nums2)

print(length)
print(arr)

输出:

3
[3, 2, 1]

这里我顺手把数组也返回了。刷题只要长度,线上排查或者写工具脚本时,我一般会把命中的片段也带出来。不然只给一个 3,调试时还得自己回头找,挺烦。

不过这个二维 dp 还有个问题。

它占内存。

dp[i][j] 只依赖左上角的 dp[i - 1][j - 1]。也就是说,当前这一行只需要上一行的数据,不需要整张表一直放着。

可以压成一维。

这版更适合提交:

deffind_length(nums1, nums2):
ifnot nums1 ornot nums2:
return0

    m = len(nums2)
    prev = [0] * (m + 1)
    best = 0

for x in nums1:
        curr = [0] * (m + 1)

for j in range(1, m + 1):
if x == nums2[j - 1]:
                curr[j] = prev[j - 1] + 1

if curr[j] > best:
                    best = curr[j]

        prev = curr

return best

这段代码有个地方别改顺手了。

curr[j] = prev[j - 1] + 1

不是 prev[j],也不是 curr[j - 1]。

因为连续段要同时往两个数组的前一个位置回退。一个往前,一个不动,那就不是同一段连续匹配了。

我见过不少错误写法长这样:

if nums1[i] == nums2[j]:
    dp[j] = dp[j - 1] + 1

这地方看着省事,实际会把当前行的数据污染掉。你以为自己在接上一轮,实际上接的是本轮刚算出来的东西,结果会飘。

如果非要用一个数组,也可以从右往左扫:

deffind_length_one_row(nums1, nums2):
    row = [0] * (len(nums2) + 1)
    best = 0

for x in nums1:
for j in range(len(nums2), 0, -1):
if x == nums2[j - 1]:
                row[j] = row[j - 1] + 1
                best = max(best, row[j])
else:
                row[j] = 0

return best

这里必须倒着来。

正着扫会把 row[j - 1] 提前更新掉,等你再拿它时,它已经不是上一行的状态了。这个 bug 不一定马上暴露,小样例可能还过,一到重复数字多的数组就开始胡说。

比如:

nums1 = [1, 1, 1]
nums2 = [1, 1]

这种全是重复值的用例,最适合抓这种状态污染。

这个题到最后其实就一句话:

两个位置相等,就接左上角;不相等,清零。

能把“连续”这两个字咬住,代码就不会跑偏。DP 表不是为了显得高级,它只是帮我们记住:上一秒已经比过的那一段,别再傻乎乎重新比一遍。