最狠的裁员方式,有时候真不是直接让你走人,而是把你晾在一边。活不给,会不开,邮件也不带你,但工位还给你留着,就像公司供了个吉祥物。每天按时打卡,电脑一开,连装忙都找不到素材,这种状态持续久了,是个人都会开始怀疑自己是不是真的“空气化”了。

有人在评论区管这叫“软劝退”,还有人说这招“比骂你两句都阴损”,我觉得说得都挺在点子上。表面上看公司啥也没干,工资照发,流程上毫无问题。但实际上,刀子早就落下来了。很多人没等到被正式辞退,就已经先被这种冷处理给磨得没脾气了。最损的地方就在这儿:它不跟你撕破脸,而是让你自己怀疑自己、自己难受、最终自己提辞职。
算法题:最小窗口子序列
聊完了职场,来换换脑子,看一道名字相似但内核截然不同的算法题:最小窗口子序列。这题是很多人在准备面试时容易踩的坑。
不是所有带“窗口”两个字的题目,都能直接套用左右指针推来推去的滑动窗口模板。“最小窗口子串”可能刷顺手了,但看到“最小窗口子序列”也想照抄,大概率一写就跑偏。这道题最别扭的地方在于一个关键约束:窗口内部不要求字符连续,但匹配的顺序绝对不能乱。就这一点,直接把普通滑动窗口那套“够了就缩,不够就扩”的逻辑给搞不灵了。
题目的意思很简单:给你两个字符串 s1 和 s2,要在 s1 里找一个最短的连续子串,使得 s2 是这个子串的子序列。
举个例子:
s1 = "abcdebdde"
s2 = "bde"
正确答案是 "bcde",而不是 "bdde"。后者也能匹配 s2,但长度更长,不是最短的。
看到这题,我的第一反应不是直接上动态规划,而是先顺着 s1 往前扫描,看看能不能先找到一个完整的匹配,然后再想办法回头把这个窗口缩到最小。这个思路有点像线上排查问题:先确认 Bug 能复现,再去定位和缩小范围。
代码可以这样写:
def min_window_subsequence(s1: str, s2: str) -> str:
n, m = len(s1), len(s2)
best_start = -1
best_len = float('inf')
i = 0
while i < n:
if s1[i] != s2[0]:
i += 1
continue
# 先向前找,确认能不能完整覆盖 s2
j, k = i, 0
while j < n and k < m:
if s1[j] == s2[k]:
k += 1
j += 1
if k < m:
break
# 再反向收缩,把窗口压到最小
end = j - 1
k = m - 1
j = end
while k >= 0:
if s1[j] == s2[k]:
k -= 1
j -= 1
start = j + 1
if end - start + 1 < best_len:
best_len = end - start + 1
best_start = start
i = start + 1
return "" if best_start == -1 else s1[best_start:best_start + best_len]
这段代码的核心思路其实很清晰:正向找到终点,反向回缩起点。
为什么需要反向收缩?因为你第一次正向扫描找到的窗口,通常只是“能用”,但不一定是最短的。这就好比排查 SQL,发现“能跑”不代表执行计划已经最优,还得继续优化。
我们拿 "abcdebdde" 和 "bde" 来走一遍流程:
- 正向从第一个
'b' 开始,往后匹配出 b -> d -> e,先拿到一个可能的窗口。
- 然后从结尾的那个
'e' 开始往回倒着找:先找 s2 的最后一个 'e',再找 'd',最后找 'b'。这样反向匹配一遍,才能把左边界的起始位置卡到最小。
- 最终得到的最短窗口就是
"bcde"。
这题的时间复杂度算不上特别漂亮,最坏情况在 O(n * m) 到 O(n^2) 之间,取决于字符的分布情况。但在面试场景下,这个解法通常是够用的,而且它的思路比硬背动态规划的状态转移方程更好理解和讲述。
当然,如果真想继续抠性能极限,可以用动态规划,记录 s2 每个位置在 s1 中匹配后的起点。那种写法更规整,但也更容易写出一坨自己事后都不想复盘的状态转移代码。
所以对于这道题,我更建议先把上面这个“先匹配,再收缩”的版本吃透。它不耍花招,甚至有点“笨”,但判断路径是对的。千万别一上来就把“子串”和“子序列”的条件给混淆了——这恰恰是这道题埋坑的地方。
工作和刷题有时候挺像,都得先看清规则,再找对方法。无论是面对职场的“软刀子”,还是算法的“硬骨头,保持清晰的逻辑和主动的心态总是没错的。如果你在技术成长的路上有什么困惑或想法,也欢迎来 云栈社区 聊聊。