上线前夜,发现致命bug,情急之下直接把服务器上的组件文件给删了,结果整个项目瘫痪,直接收拾东西回了老家,连夜跑路
刚看到个贴子,说有个程序员上线前夜发现致命bug,情急之下直接删了服务器上的组件文件,结果整个项目瘫痪,人也连夜跑路了。
网友们一边笑他“太怂”,一边又感叹“这是真实职场惊魂夜”。
年轻时候没经验,心态又急,遇到问题容易慌神,越想补救越容易越陷越深。删库跑路这四个字,几乎是每个程序员的噩梦,也是成长的必修课。
网友有人说他不负责任,但从另一面看,这也是职场新人最容易暴露的软肋——技术之外的责任感和心理素质。真正成熟的程序员,不是永远不出错,而是出了错能稳住、敢扛、能补。
说到底,删的不是库,是教训。能扛过去,就是经验;扛不过去,就成故事。【备注:文末可领最新资料】
面试题:收集树中金币
我们有一棵 n 个点的树(无向、连通、无环),节点编号 0 ~ n-1。 给你:
edges:树的边 coins:长度为 n 的数组,coins[i] 要么是 0,要么是 1,表示这个点有没有金币
人一开始在节点 0,每走一条边花费 1 单位时间(来回就是 2),可以在任意节点拐弯、重复走,只要你最后能把所有金币都拿到手,题目问:最少要花多少时间?
你可以理解成:一棵“家谱树”,每个节点有可能藏了一个金币,你从老祖宗 0 出发,把所有金币都收完再回家。
先想想:哪些路是完全没必要走的?
这个题最关键的点,其实是两个字:修剪。
你可以脑补一棵大树:
如果某个叶子(度数=1)下面根本没有金币,那你肯定不会跑去那条支路瞎逛,对吧? 而且如果你把这个没金币的叶子剪掉后,它的父节点又变成“新叶子”,又没金币,那这货也可以继续剪。
所以第一步,我们干一件事:
从所有“没金币的叶子”开始,一层一层往上剪,直到剩下的节点,要么自己有金币,要么是连接金币路径必经之路。
这个过程用一个队列就能搞定:
先把所有 degree == 1 && coins[node] == 0的点丢进队列每次弹出一个,把它“删除”(度数-1),连着的点如果也变成了没金币的叶子,再丢进队列 反复直到剪不动
做完这步,你可以想象原来的树被削成了“只保留和金币有关的骨架”。
但就算都跟金币有关,也不一定每条边都要走两次
那接下来还有个优化点。
你想象一下真正去收金币的过程:
某个最偏远的叶子有金币,你一定得走到它再走回来,这条链上的边,咋看都要走两次。 可是越接近中间(接近根)那一段,很多金币的路径是顺路共享的,最后其实有一小段你可以不必再“专门走一趟”。
比较经典的做法是这么玩:
在“剪完无金币叶子后的树”上,把所有当前的叶子再丢到队列里(这次不管它有没有金币)。
再剪两层叶子:
每轮把队列里所有点删了,看它们邻居是不是变成新叶子,是就入队。
我们做两轮 BFS,每一轮都删掉当前所有叶子,更新它们邻居的度数。
比如 step = 0,1:
剪完两层之后,剩下的那坨点就是“真正需要来回走的核心区域”; 剩余的边数记成 remainEdges,答案就是 remainEdges * 2。
为啥是“剪两层”?
你可以这样理解: 从一片金币叶子出发往回走:
最最后那两条边(越靠近叶子那一端)你一定是“专程去一趟又走回来”,没法省; 但更靠上的边,在多个叶子路径之间是可以重用的,不用按“每个叶子单独往返”去算。
这个算法的推导如果展开会比较长,这里就直接记结论:在保留了所有金币相关节点后,再在整棵树上“去掉两层叶子”,剩下的边数乘 2 就是最短时间。如果最后剩下的边为 0,那就说明根本不用动,或者只有一个点,答案就是 0。
用 Python 把这个过程写出来
直接上代码,先看一眼,后面我再快速过一遍:
from collections import deque
from typing import List
classSolution:
defcollectTheCoins(self, coins: List[int], edges: List[List[int]]) -> int:
n = len(coins)
if n <= 1:
return0
# 建图 & 度数统计
g = [[] for _ in range(n)]
degree = [0] * n
for u, v in edges:
g[u].append(v)
g[v].append(u)
degree[u] += 1
degree[v] += 1
# 当前还存在的边数
edge_cnt = n - 1
# 第一次修剪:把“没有金币的叶子”一层层剪掉
q = deque()
for i in range(n):
if degree[i] == 1and coins[i] == 0:
q.append(i)
while q:
x = q.popleft()
degree[x] = 0# 相当于删掉
edge_cnt -= 1if edge_cnt > 0else0# 这个点连着的一定只有一条边
for y in g[x]:
if degree[y] > 0: # 邻居还在树上
degree[y] -= 1
if degree[y] == 1and coins[y] == 0:
q.append(y)
# 如果边都被剪光了,说明根本没有需要走的路
if edge_cnt <= 0:
return0
# 第二次修剪:不管有没有金币,把所有叶子入队
q.clear()
for i in range(n):
if degree[i] == 1:
q.append(i)
# 再剪两层叶子
steps = 2
while q and steps > 0:
size = len(q)
for _ in range(size):
x = q.popleft()
degree[x] = 0
edge_cnt -= 1
for y in g[x]:
if degree[y] > 0:
degree[y] -= 1
if degree[y] == 1:
q.append(y)
steps -= 1
# 剩下的边需要来回各走一次
if edge_cnt < 0:
edge_cnt = 0
return edge_cnt * 2
大概过一下流程:
g是邻接表,degree存每个点当前的度数。edge_cnt = n - 1,树一开始有n-1条边,后面每删掉一个节点(叶子)就把对应的边减一。第一个 while:
从“度数为 1 且没有金币的点”开始往外剪; 每删掉一个,它的父节点有可能变成新的“无金币叶子”,继续剪。 第二阶段:
如果 edge_cnt <= 0,直接 return 0。否则把当前所有 degree == 1的点当作叶子,再 BFS 两层(steps = 2),每删一批叶子就更新邻居度数。最后剩下的
edge_cnt条边,每条都得来回走一次,所以乘 2。
时间复杂度是 O(n),因为每条边最多被处理常数次;空间复杂度也是 O(n),主要就是图和队列。
整套逻辑其实就两句话:
先把“和金币完全没关系的枝丫”剪掉; 再从外往里削掉两层叶子,剩下的骨架边数 * 2 就是答案。
实际写的时候多注意两个细节:
用度数 + 队列来做“多轮修剪”,不要怕改 degree,这个就相当于在树上做拓扑层次删除; edge_cnt记的是当前还存活的边数,不要忘了边全没了就直接返回 0。
这样一来,这道“收集树中金币”的题就比较顺了,思路也挺工程化的,跟我们平时做“剪枝优化 + 层次遍历”那一挂是一个套路。
-END-
我为大家打造了一份RPA教程,完全免费:songshuhezi.com/rpa.html
虎哥作为一名老码农,整理了全网最全《python高级架构师资料合集》,总量高达650GB,点击下方公众号回复关键字 python 全部免费领