在公司干了3年的技术骨干,负责核心业务,月薪30K,旁边刚带的应届生,还在熟悉代码,能力和产出都差一大截,薪资高达28K。。
刚看到个贴子,说一位技术老哥在公司干了三年,是业务核心、半夜还要on-call那种,但月薪30K。结果新来的应届生啥都不熟,拿着28K的offer入职。
活还得老哥干,锅还得老哥背,最后发现自己涨薪速度还不如校招生。
我觉得这事吧,不只是“薪资不公”那么简单,更像是职场的一面镜子。公司看重的不是谁更辛苦,而是谁的“市场价”更高。新人贵,是因为外面行情高;老员工便宜,是因为被稳定绑架。网友们有的喊“跑路”,有的劝“认清现实”,其实都对。
说到底,职场不讲资历讲价值。老员工也得学会“刷新价格”,不是只靠熬,而是让自己随时有跳槽的底气。市场才是真正的绩效表。哎,认清规则,才能少被气到。【备注:文末可领最新资料】
面试题:完成所有任务的最少时间
你先想象这么个场景哈:你一天排了很多小任务,每个任务都规定了「可以干活的时间段」和「总共要花多久」。但是同一时刻你只能干一个任务。那问题就是——到底挑哪些具体的时间点来干活,才能把所有任务都做完,而且总用时最少?
这个就是「完成所有任务的最少时间」这道题的大白话版本。
每个任务给你三样东西:
start:任务最早可以开始的时间end:任务最晚必须结束的时间duration:这个任务总共要干多少个单位时间
时间我们可以当成是 1,2,3,... 这样的整数点。 只要在 [start, end] 这个闭区间里,你挑出 duration 个不同的时间点专门给这个任务干活,就算完成。
但是你同时只能干一个任务,所以所有任务用到的时间点不能重叠(同一时间只能干一个任务)。 问:最少要占用多少个时间点,才能让所有任务都满意?
贪心思路为什么靠谱
这种题一看就有点「安排日程表」那味儿,其实核心思路就一句话:
任务按结束时间从早到晚排个序,每个任务尽量往「右边」塞。
为什么要按 end 排序? 直觉上,越早「到期」的任务越着急,应该优先满足它们。万一你先把时间都花在那些很晚才结束的任务上,早结束的任务可能就没位置了。
为什么「往右塞」? 比如一个任务允许你在 1~10 号之间干 3 天的活,如果你惯性都往左边塞 1,2,3,那么后面别的任务可能也只能用 4~10,就被你挤没地方了。 但如果你习惯从右边塞 10,9,8,那左边 1~7 还留给后面的任务用,更灵活。
所以整个策略就是:
先按任务的 end升序排好。对每个任务,看它可用区间里,之前已经被别的任务占了多少天。 还差多少天,就从 end往start反着找没用过的时间点补上。
难点在实现细节
上面的想法挺直观,但如果直接用一个数组硬扫,复杂度很容易炸掉。
我们需要两种能力:
快速算出某个区间 [l, r]里已经用了多少个时间点。快速在「某个时间点往左」找到最近的一个还没用过的位置。
这里就可以上两个经典小工具:
树状数组(Fenwick Tree):支持「单点加」「前缀和查询」,就能用来算区间用了多少时间。 并查集当「下一个可用位置」指针用:把每个时间点看成一个节点,一旦这个时间点被占用了,就把它「合并」到左边去,这样以后再找可用位置就能一步跳过去。
听着有点抽象,你可以简单理解成:
树状数组负责「数数」:某段时间里用了几天。 并查集负责「找坑」:从右往左找下一个空位,但中间的已占用位置会被自动跳过。
Python 实现一把
直接上代码,你对着思路看就行,别被数据结构名字吓到。
from typing import List
classFenwick:
def__init__(self, n: int):
self.n = n
self.bit = [0] * (n + 1)
defadd(self, i: int, delta: int):
# 树状数组下标从 1 开始
while i <= self.n:
self.bit[i] += delta
i += i & -i
defsum(self, i: int) -> int:
res = 0
while i > 0:
res += self.bit[i]
i -= i & -i
return res
defrange_sum(self, l: int, r: int) -> int:
if l > r:
return0
return self.sum(r) - self.sum(l - 1)
classDSU:
"""并查集,这里用来找 <= x 的最后一个可用时间点"""
def__init__(self, n: int):
# parent[i] 表示从 i 往左能跳到的“代表点”
self.parent = list(range(n + 1))
deffind(self, x: int) -> int:
if x <= 0:
return0
if self.parent[x] != x:
self.parent[x] = self.find(self.parent[x])
return self.parent[x]
defoccupy(self, x: int):
# 把位置 x 占用掉,以后再找就会跳到 x-1
self.parent[x] = self.find(x - 1)
defmin_time_to_finish_tasks(tasks: List[List[int]]) -> int:
"""
tasks[i] = [start, end, duration]
返回完成所有任务需要的最少时间点数量
"""
ifnot tasks:
return0
# 1. 按结束时间排序
tasks.sort(key=lambda x: x[1])
max_end = max(t[1] for t in tasks)
bit = Fenwick(max_end)
dsu = DSU(max_end)
total_used = 0
for start, end, duration in tasks:
# 2. 统计当前区间内已经被占用的时间点个数
already = bit.range_sum(start, end)
need = duration - already
if need <= 0:
continue# 这任务已经自然被之前的时间覆盖够了
# 3. 从右往左找空位,把缺的补上
while need > 0:
pos = dsu.find(end) # 找到 <= end 的最新空位
if pos < start:
# 理论上题目会保证有解,这里防御一下
raise ValueError("无可用时间完成所有任务")
# 在 pos 上干一天
bit.add(pos, 1)
dsu.occupy(pos)
total_used += 1
need -= 1
return total_used
你可以随便拍两个例子测下,比如:
tasks = [
[1, 3, 2], # 1~3 之间要工作 2 天
[2, 5, 2], # 2~5 之间要工作 2 天
]
print(min_time_to_finish_tasks(tasks))
这个结果会是 4,合理的安排比如在第 2,3,4,5 天工作,既满足了第一个任务,也满足了第二个任务。
复杂度顺带说一句
排序是 O(n log n)。每个时间点只会被占用一次,每占用一次会在树状数组里做一次 add,以及在并查集里做一次find/union,两个都是近似O(log T)或更快。所以整体大致可以看成 O((n + T) log T),完全能应付题目里那种十几万级别的数据。
整体下来,这个算法的精髓就是一句话: 「按结束时间排序 + 每个任务尽量往右塞 + 用数据结构帮你数数和找空位」。
差不多就这些了,你要是愿意,可以自己先写个简单暴力版(直接数组扫),再对比这个优化版,会更有感觉。
-END-
我为大家打造了一份RPA教程,完全免费:songshuhezi.com/rpa.html
虎哥作为一名老码农,整理了全网最全《python高级架构师资料合集》,总量高达650GB,点击下方公众号回复关键字 python 全部免费领