leetcode3739 村长云服务器 II:三年日志又何妨,哈希O(n)镇全场

🖥️ 村长云服务器 II:三年日志又何妨,哈希O(n)镇全场

上回说到,村长的二手服务器靠着我从暴力到前缀和再到哈希增量的三重大法稳住了阵脚,故障窗口一个不少全揪了出来。老头抱着服务器乐颠颠地去找老板娘邀功。我当时拍着胸脯吹了牛,说三十年也照跑不误。

才过了一天,牛就真找上门了。今天一大早,村长又抱着那台熟悉的二手服务器冲进来,机箱上的标签竟从“村长云服务器·内测版”升级成了“村长云服务器·公测版”,只是那风扇的咆哮声听起来,像是随时准备原地起飞。

“小白!老板娘看了昨天的故障窗口报告,非常满意!”村长把显示器往桌上一搁,屏幕上的日志流像瀑布一样疯狂刷屏,滚动速度快得能闪瞎眼,“ 她说‘村长云果然靠谱’,反手就把抽奖活动的规模扩大了十倍!现在不只是小卖部,全村五十家商铺都用上了我的服务器!”

村长擦着额头的汗,“而且这只是开始!老板娘说活动要持续一个月,到时候日志量可能突破百万!我昨天不信邪,想试试你最开始那个暴力法还能不能跑——就是你写的第一版,双重循环那个——结果……”他咽了口唾沫,心有余悸地说,“跑了三分钟还没出结果,老板娘差点就要换老李的服务器了!”

我接过显示器,扫了一眼数据范围:n = 10^5。十万条日志。暴力O(n²)肯定会被创飞,但昨天在老勇者的点化下,我已经提前从O(n²)一路优化到O(n log n)再到O(n),哈希表+动态维护f的终极版本就躺在我的魔法书里。

“村长,您这不是自找苦吃吗?”我靠在椅背上,连袖子都懒得撸,“明明昨天老勇者已经把终极法宝塞您眼前了,您偏要回去翻那本已经淘汰的暴力秘籍。“

我调出昨天哈希表+增量维护的终极版本。cnt是哈希表,记录每种前缀和出现了几次;s是当前前缀和,f是动态维护的变量,每步更新只需O(1)。一趟遍历,没有嵌套循环,没有二分查找。一个字符都不用改,直接复制粘贴。

from collections import defaultdict

class Solution:
    def countMajoritySubarrays(self, nums: List[int], target: int) -> int:        
        cnt = defaultdict(int)  # 初始化哈希表,记录每个前缀和出现的次数
        cnt[0] = 1  			# 哨兵:还没开始时的前缀和为0,出现1次
        
        ans = s = f = 0  		# ans最终答案,s当前前缀和,f当前合法起点个数
        for num in nums:  
            if num == target:  
                f += cnt[s]  	# s 即将变成 s+1,原本前缀和等于 s 的那些起点现在都合法了
                s += 1  		# 更新前缀和
            else:  
                s -= 1  		# 先更新前缀和
                f -= cnt[s]  	# s 已经变成 s-1,原本前缀和等于 s-1 的那些起点现在不合法了
            ans += f  			# 累加当前位置的贡献
            cnt[s] += 1  		# 将当前前缀和的出现次数加1

        return ans

提交,AC。“看到没?”我拍着屏幕上那段哈希表代码,像个刚打赢Boss的菜鸟勇者,“这就是昨天老勇者敲出来的终极奥义——步长±1,增量维护,哈希O(1)查找!整体O(n)通关!O(n)是什么意思?就是日志量翻十倍,时间也只翻十倍!昨天几千条跑了几毫秒,今天十万条顶多也就几十毫秒!”

村长盯着代码里的cnt[s]f变量,挠了挠头:“等等,让我捋捋……遇到FAIL就s -= 1然后f -= cnt[s],遇到SUCCESS就f += cnt[s]然后s += 1——每次更新只要O(1),一趟循环完事?就这么简单?”

“就这么简单!步长±1这个性质,让原本需要SortedList二分查找的操作,退化成了加减法。不是数据结构选得好,是数据变化的规律挖得深——老勇者原话。”

村长若有所思地点点头,然后问了一个让我差点从椅子上摔下来的问题:“那如果老板娘把活动搞到太阳系毁灭呢?日志量无限大怎么办?”

“理论上,只要磁盘能存下,O(n)就能跑。磁盘存不下的话——那就不是算法的问题,是您该给服务器加硬盘了。或者换个老板娘,让她别把活动搞那么大。”


看着村长抱着服务器远去的背影,我靠在椅背上,脑海中回放着这两天的算法之旅,从O(n²)到O(n),三步进化:

  1. 暴力枚举 O(n²) :双重循环,硬刚所有子数组。直观但低效,像用放大镜在沙漠里数沙子。
  2. 前缀和+SortedList O(n log n):把区间问题转成查询问题,用平衡树加速。快了不少,但要在红黑树里爬来爬去。
  3. 前缀和 + 哈希增量 O(n):抓住步长±1的命门,用变量f动态维护。不再依赖重型数据结构,像直接开了上帝视角,一步到位。

三步进化,是一场思维方式的三重升级——从“暴力硬刚”到“找规律走捷径”,从“依赖重型数据结构”到“洞察数据本质”。

脑子一旦转起来就停不住。今天十万条的日志O(n)秒过,但如果老板娘的需求继续升级呢?

如果老板娘再搞大点——全村五十家商铺、每人每秒都在抽奖、日志量破亿——单机O(n)迟早撞上物理天花板。到时候就得把日志分片,扔到多台服务器上并行处理——每台机器独立算自己那段的净胜球前缀和,最后汇总的时候要处理跨分片边界的区间。这个“跨片合并”才是分布式最难啃的骨头,搞不好要请MapReduce出山。

或者老板娘换了个需求——不要离线统计了,要实时监控,每秒刷新一次故障窗口数量——那哈希表就不够用了,得上支持动态前缀和查询的结构——树状数组或者线段树,每次日志写入O(log n)更新,每次查询O(log n)出结果。从离线批处理变成在线流式,数据结构跟着需求一起升级。

再或者,老板娘不仅想知道故障窗口有几个,还想知道“最长的故障窗口有多长”、“故障密度最高的时间段在哪里”——那光靠f变量就兜不住了,得上更复杂的状态追踪,最长窗口、密度峰值,每一个新需求都是一个新的算法副本。

我突然感觉背上一片冷汗——倒不是被这些未来需求吓的,而是忽然意识到,算法工程师的日常,就像是一条永不停歇的日志流。需求来的时候像是 DDoS 攻击,流量瞬间爆表,CPU 直接拉满;开发的时候像在打一个无限副本的 Boss,刚交完差还没来得及存档,产品经理就带着新需求闪亮登场了——发需求的时间复杂度是O(1),随时随地,想到就来;而客户的想象力空间复杂度是O(∞),永不封顶,比老板娘的抽奖活动还能膨胀。

不过转念一想,昨天不也是从O(n²)一路进化过来了吗?兵来将挡,需求来了解法挡。

三年日志哈希扛,昨日神功今上场。 客户之心深似海,日日精进破新墙。

知识共享许可协议
本作品采用 知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议 进行许可。