我已经解决了一些类似的问题。
首先,如果您不需要确切的答案,我通常建议您研究诸如遗传算法之类的东西,或者做一个贪心算法。这不会是正确的,但通常也不会很糟糕。它会比精确算法快得多。例如,您可以从所有点开始作为缓存点,然后找到最能降低成本的点,使其成为非缓存点。继续直到移除下一个导致成本上升,并将其用作您的解决方案。这不会是最好的。它通常会相当不错。
如果您确实需要一个准确的答案,您将需要对大量数据进行暴力搜索。假设指定了初始缓存点,您将有 224 = 16,777,216 个可能的缓存点集进行搜索。太贵了。
更便宜(注意,不是便宜,只是更便宜)的诀窍是找到修剪搜索的方法。记住这样一个事实,如果在你查看的每个集合上做 100 倍的工作,让你从考虑中平均删除 10 个点作为缓存点,那么你的整体算法将访问 0.1% 的集合,并且你的代码将运行快 10 倍。因此,即使修剪步骤相当昂贵,也值得在早期且经常性地进行修剪。
您通常需要多种修剪策略。其中之一通常是“我们可以从这里做的最好的事情比我们以前发现的最好的事情还差”。如果您已经找到了一个非常好的最佳解决方案,这会更好。因此,在寻找解决方案的过程中进行一些局部优化通常是值得的。
通常,这些优化不会改变您正在做大量工作的事实。但它们确实让您的工作量减少了几个数量级。
我最初的尝试将利用以下观察结果。
- 假设
x 是一个缓存点,y 是它最近的缓存邻居。然后,如果您只是将缓存更新流量从x 路由到y 沿着该路径,那么您始终可以“免费”创建从x 到y 缓存的路径。因此,不失一般性,这组缓存点连接在网格上。
- 如果最终的最低成本可能超过我们发现的当前最佳成本,那么我们还没有走向全球解决方案。
- 一旦距离缓存点大于 1 的所有点的访问率加上您仍然可以使用的缓存点邻居的最高访问频率之和小于缓存频率,添加更多缓存点总是会损失的。 (这将是“让我们提前 10 分钟停下来的昂贵条件。”)
- 当前缓存点集的最高访问邻居是下一个缓存点尝试的合理候选者。 (您可以尝试其他几种启发式方法,但这种方法是合理的。)
- 任何总访问频率超过缓存频率的点都绝对是缓存点。
这可能不是最好的观察结果。但这可能是相当合理的。要利用这一点,您至少需要一种您可能不熟悉的数据结构。如果您不知道priority queue 是什么,那么请使用您选择的语言四处寻找一个高效的。如果找不到,heap 很容易实现,并且作为优先级队列工作得很好。
考虑到这一点,假设您已经获得了您所描述的信息和一个初始缓存节点P,这里是一个用于找到最佳算法的伪代码。
# Data structures to be dynamically maintained:
# AT[x, n] - how many accesses x needs that currently need to go distance n.
# D[x] - The distance from x to the nearest cache node.
# CA[x] - Boolean yes/no for whether x is a cache node.
# B[x] - Boolean yes/no for whether x is blocked from being a cache node.
# cost - Current cost
# distant_accesses - The sum of the total number of accesses made from more than
# distance 1 from the cache nodes.
# best_possible_cost - C * nodes_in_cache + sum(min(total accesses, C) for non-cache nodes)
# *** Sufficient data structures to be able to unwind changes to all of the above before
# returning from recursive calls (I won't specify accesses to them, but they need to
# be there)
# best_cost - The best cost found.
# best_solution - The best solution found.
initialize all of those data structures (including best)
create neighbors priority queue of neighbors of root cache node (ordered by accesses)
call extend_current_solution(neighbors)
do what we want with the best solution
function extend_current_solution (available_neighbors):
if cost < best_cost:
best_cost = cost
best_solution = CA # current set of cache nodes.
if best_cost < best_possible_cost
return # pruning time
neighbors = clone(available_neighbors)
while neighbors:
node = remove best from neighbors
if distant_accesses + accesses(node) < C:
return # this is condition 3 above
make node in cache set
- add it to CA
- update costs
- add its immediate neighbors to neighbors
call extend_current_solution
unwind changes just made
make node in blocked set
call extend_current_solution
unwind changes to blocked set
return
编写此代码需要大量工作,并且您需要小心维护所有数据结构。但我敢打赌——尽管它看起来很重——你会发现它修剪了你的搜索空间,足以比你现有的解决方案运行得更快。 (它仍然不会活泼。)
祝你好运!
更新
当我更多地思考这个问题时,我意识到一个更好的观察是注意如果你可以将“不是缓存节点,不是阻塞节点”的集合分成两部分,那么你可以独立解决这些部分。这些子问题中的每一个都比整个问题的解决速度要快几个数量级,因此请尽快解决。
一个很好的启发式方法是遵循以下规则:
- 当没有到达边缘时:
- 向最近的边缘行驶。距离是通过最短路径沿着非缓存、非阻塞集合的长度来衡量的。
- 如果两条边等距,则根据以下优先顺序断开连接:
(1, x), (x, 1), (5, x), (x, 5)。
- 根据更喜欢向边缘中心行驶,打破任何剩余的联系。
- 随机打破所有剩余的关系。
- 虽然已到达边并且您的组件仍有可能成为缓存块的边:
- 如果您可以立即移动到边缘并将边缘部分分成两个组件,请执行此操作。对于“缓存集中的边缘”和“不在缓存集中的边缘”,您都会得到 2 个更容易处理的独立子问题。
- 否则沿最短路径移动到边缘部分中间的部分。
- 如果存在平局,请打破平局,以使从添加的块到添加的缓存元素的线尽可能接近对角线。
- 随机打破所有剩余的关系。
- 如果你掉到这里,随机选择。 (此时您应该有一个非常小的子问题。无需聪明。)
如果您从 (3, 3) 作为缓存点开始尝试此操作,您会发现在最初的几个决定中,您会发现 7/16 的时间您设法切入两个偶数问题,1/ 16 次你在缓存点中阻塞并完成,1/4 的时间你设法将一个 2x2 块切割成一个单独的问题(使整个解决方案的运行速度提高 16 倍)和 1/4 的当您在朝着解决方案前进的道路上顺利结束时,该解决方案要么被装箱(并迅速耗尽),要么成为具有大量缓存点的解决方案的候选者,这些缓存点被修剪以实现正轨一个糟糕的解决方案。
我不会给出这个变体的伪代码。它将与我上面的内容有很多相似之处,需要处理许多重要的细节。但我愿意打赌,实际上它的运行速度会比您原来的解决方案快几个数量级。