Python技术迷

在公司干了3年的技术骨干,负责核心业务,月薪30K,旁边刚带的应届生,还在熟悉代码,能力和产出都差一大截,薪资高达28K。。

刚看到个贴子,说一位技术老哥在公司干了三年,是业务核心、半夜还要on-call那种,但月薪30K。结果新来的应届生啥都不熟,拿着28K的offer入职。

活还得老哥干,锅还得老哥背,最后发现自己涨薪速度还不如校招生。

Image

我觉得这事吧,不只是“薪资不公”那么简单,更像是职场的一面镜子。公司看重的不是谁更辛苦,而是谁的“市场价”更高。新人贵,是因为外面行情高;老员工便宜,是因为被稳定绑架。网友们有的喊“跑路”,有的劝“认清现实”,其实都对。

说到底,职场不讲资历讲价值。老员工也得学会“刷新价格”,不是只靠熬,而是让自己随时有跳槽的底气。市场才是真正的绩效表。哎,认清规则,才能少被气到。【备注:文末可领最新资料】

面试题:完成所有任务的最少时间

你先想象这么个场景哈:你一天排了很多小任务,每个任务都规定了「可以干活的时间段」和「总共要花多久」。但是同一时刻你只能干一个任务。那问题就是——到底挑哪些具体的时间点来干活,才能把所有任务都做完,而且总用时最少?

这个就是「完成所有任务的最少时间」这道题的大白话版本。

每个任务给你三样东西:

  • start:任务最早可以开始的时间
  • end:任务最晚必须结束的时间
  • duration:这个任务总共要干多少个单位时间

时间我们可以当成是 1,2,3,... 这样的整数点。 只要在 [start, end] 这个闭区间里,你挑出 duration 个不同的时间点专门给这个任务干活,就算完成。

但是你同时只能干一个任务,所以所有任务用到的时间点不能重叠(同一时间只能干一个任务)。 问:最少要占用多少个时间点,才能让所有任务都满意?

贪心思路为什么靠谱

这种题一看就有点「安排日程表」那味儿,其实核心思路就一句话:

任务按结束时间从早到晚排个序,每个任务尽量往「右边」塞。

为什么要按 end 排序? 直觉上,越早「到期」的任务越着急,应该优先满足它们。万一你先把时间都花在那些很晚才结束的任务上,早结束的任务可能就没位置了。

为什么「往右塞」? 比如一个任务允许你在 1~10 号之间干 3 天的活,如果你惯性都往左边塞 1,2,3,那么后面别的任务可能也只能用 4~10,就被你挤没地方了。 但如果你习惯从右边塞 10,9,8,那左边 1~7 还留给后面的任务用,更灵活。

所以整个策略就是:

  1. 先按任务的 end 升序排好。
  2. 对每个任务,看它可用区间里,之前已经被别的任务占了多少天。
  3. 还差多少天,就从 end 往 start 反着找没用过的时间点补上。

难点在实现细节

上面的想法挺直观,但如果直接用一个数组硬扫,复杂度很容易炸掉。

我们需要两种能力:

  1. 快速算出某个区间 [l, r] 里已经用了多少个时间点。
  2. 快速在「某个时间点往左」找到最近的一个还没用过的位置。

这里就可以上两个经典小工具:

  • 树状数组(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 全部免费领