UVa 11563 这道题我当年第一次看到题目名字“Introspective Caching”的时候,还以为是什么系统设计题,结果点进去发现是一道纯正的动态规划加贪心优化题。但说实话,这道题对理解缓存淘汰策略的本质特别有帮助,它把 LRU、LFU 这些经典策略全部打碎,让你站在“最优决策”的角度重新审视了一遍:缓存到底应该怎么淘汰数据,才能在访问序列已知的情况下花费最小代价。如果你正在刷 UVa 或者准备面试里的缓存系统设计题,这道题值得好好啃一遍。
题目本身讲的是一个小型缓存系统,缓存容量固定,外部给出一串访问请求。每次访问一个不在缓存里的数据就会触发一次缓存未命中,此时如果缓存已满,就必须踢掉一个旧数据。问题在于,踢掉哪个数据不是随便选的,一旦选错,未来短时间内这个数据又被访问到,系统就要付出额外的代价。换句话说,这道题引入了一个“内省”机制:缓存策略不只是看过去的访问频率或者最近的访问时间,而是可以根据未来的请求序列动态调整自己的淘汰决策,从而最小化总代价。这就是题目名 Introspective Caching 的含义。
适合看这篇文章的人有两类:一类是刷 UVa 的老选手,想搞懂这题的 DP 建模和 AC 思路;另一类是准备后端面试、对缓存淘汰策略有兴趣的人。前者可以直接跳到第三节看算法核心,后者建议从第一节开始,把 LRU、LFU 和“内省式淘汰”之间的关系捋清楚。我把当年的做题笔记、代码踩坑和优化过程全部整理在下面,尽量写得直接可用,不绕弯子。
1. 从 LRU 到 Introspective Caching:这道题在说什么
1.1 经典缓存淘汰策略的短板
缓存淘汰策略里最经典的三兄弟就是 LRU、LFU 和 FIFO。FIFO 就是先进先出,谁先来谁先走,实现简单但是完全不顾访问频率和局部性,实际效果通常最差。LRU 的意思是“最近最少使用”,思路是保留最近被访问过的数据,淘汰最久没被访问过的数据。这个策略在大多数场景下表现不错,因为程序访问通常有局部性,刚访问过的数据大概率还会再被访问。LFU 则是“最不经常使用”,统计每个数据的访问次数,淘汰频率最低的,适合那种访问热度稳定的场景。
但 LRU 和 LFU 各有各的毛病。LRU 最怕循环扫描类的访问模式:假设缓存容量是 3,请求序列是 1, 2, 3, 4, 1, 2, 3, 4,每来一个新数都要把最久没用的那个踢掉,结果就是每次请求都是缓存未命中,缓存形同虚设。LFU 则怕访问模式突然变化:一个数据在过去被访问了一万次,但接下来再也不会被访问了,它依然占着缓存位,导致真正热的新数据进不来。这两个问题本质上是同一个:策略只基于“历史信息”做决策,对“未来”没有感知。只要访问模式发生变化,历史信息就不可靠了。
那如果提前知道整个请求序列,能不能做一个“上帝视角”的缓存策略?这就是 Belady 最优算法做的事情。Belady 算法规则特别简单:当缓存满了需要淘汰时,永远选择未来最久才会被再次访问的键淘汰掉。这个策略在不考虑淘汰代价的前提下是最优的,因为被淘汰的键在最短时间内不会用到,对缓存命中率伤害最小。但实际系统里没人能预知未来,所以 Belady 只能当理论下界。
UVa 11563 有趣的地方就在这里:它把“未来可知”这个假设变成了题目设定。请求序列完整给出,你自己决定每次淘汰谁,同时每一次淘汰都可能产生代价,目标是最小化总代价。这不仅是一个模拟题,更是一个决策优化问题。
1.2 什么是“内省式”缓存策略
“Introspective”这个词直译是“内省的、自省的”。在缓存策略里,它指的是一种能够根据当前访问流量特征调整自身行为的策略。学术界有个叫 ARC(Adaptive Replacement Cache)的算法,它就是典型的自适应缓存淘汰策略,在维护 LRU 列表的同时维护一个 LFU 列表,根据实际访问命中情况动态调整两个列表的长度比例。如果近期访问模式偏向局部性,LRU 列表占比就大;如果偏向高频热度,LFU 列表占比就大。ARC 的核心是“观察自己的表现,并据此改变行为”,这就是内省。
UVa 11563 里的“内省”比 ARC 更激进:它不是在 LRU 和 LFU 之间做比例调整,而是要求你在每一步淘汰决策时都做出全局最优的选择。题目里的容错参数引入了一种“回头惩罚”:如果淘汰了一个在未来不久会被访问的键,系统就要付出代价;如果淘汰一个在未来很长一段时间都不会被访问的键,就不会或很少付出代价。这就逼着算法去“往前看”,而不是仅仅依据过去的状态做决定。
我当年第一次接触这道题时,第一反应是直接模拟 Belady 最优算法,选未来最远才出现的键淘汰。但后来发现没那么简单,因为淘汰代价不是 0/1 二值的,而是和容错参数挂钩的。简单说:如果容错窗口是 K,那么淘汰一个在未来 K 步内会被再次访问的键,就会产生代价;被淘汰键的下一次访问距离越近,代价越大。这样一来,最优策略不一定是简单地选“未来最远”的键,而要在“淘汰这个键产生的代价”和“它
