从可以有效更新的集合中进行有效加权采样的算法

计算科学 数据结构 随机抽样
2021-12-18 00:31:12

我正在编写一个蒙特卡罗模拟,我必须在其中维护大量项目。此集合包含大量重复项,最好以记录重复项数量的数字形式存储其中的部分或全部,而不是单独存储每个。(这些项目本质上是字符串,因此它们具有不可忽略的内存成本。)

在每次迭代中,我从集合中删除一个随机项目,并可能添加一个或两个新项目。每个项目都有一个权重,从容器中抽样时,重要的是抽取给定项目的概率等于其权重乘以该类型项目的数量。

在每次迭代中添加的项目可能与容器中已经存在的项目重复,也可能不重复。这些项目可能大致呈 Pareto 分布,其中一些项目有大量重复项,而许多项目没有,但很难提前判断。

这似乎是一件相当普遍的事情,而且有效地做这件事似乎也不是一件容易的事。显而易见的方法是将项目存储在某种哈希表中,将每个项目与一个表示其频率的整数配对。问题是,从容器中采样不是很有效,因为您必须以基本上随机的顺序迭代项目。

另一方面,如果它们存储在平衡二叉树之类的东西中,那么采样将非常有效。然而,由于容器的内容在每次更新时都会发生变化,这将涉及一直重新平衡树,这(至少如果以一种天真的方式完成)将非常低效。

在我看来,我需要一个在采样速度和更新速度之间进行权衡的数据结构。也许是某种部分排序的堆状事物,当只有少数项目发生变化时,可以以较低的平均成本重新排序。但我不知道这样的结构。

如果有用于此目的的标准算法/数据结构,那么它叫什么?如果在 Python 和/或 C++ 中有现成的实现,那将是一个巨大的好处。

(请注意,用户thus spake a.k.在未加权的情况下对问题给出了优雅的答案,但我已经意识到对于我的应用程序来说,权重确实是必要的。)

4个回答

您可以通过额外维护每个节点下方的总权重来使用任何平衡的二叉搜索树数据结构来做到这一点。要随机抽样,计算一个介于 $0$ 和根节点权重之间的均匀随机数,然后向下遍历树,直到找到其范围包含随机数的叶子。0 and the weight of the root node, and traverse down through the tree until you find the leaf whose range contains the random number.

不幸的是,虽然将总权重保持在给定节点以下很简单,但这确实意味着您可能必须实现自己的平衡二叉树结构。就程序员时间而言,复制和修改现有数据结构可能是最快的;你甚至可以使用 libstdc++ 的红黑树实现。

假设 C++,如果您愿意存储重复的项目,对于未加权的情况,一种省时的方法是将它们未排序地存储在 astd::vectorstd::deque, 中v
然后,您可以通过在v.
要抽取样本,只需选择一个随机索引iv使用

sample = v[i];
v[i] = v.back();
v.pop_back();

对于额外的存储成本,您将获得(在 的情况下摊销std::vector)恒定时间迭代。

C++ 中的“标准”数据结构是多集

http://www.cplusplus.com/reference/set/multiset/

基于平衡二叉树和 unordered_multiset

http://www.cplusplus.com/reference/unordered_set/unordered_multiset/

基于哈希表。

我会先尝试这些,看看它们的性能是否足以满足您的要求。

你说你希望有很多项目只有一次,几次。

虽然一般来说,像 Geoffrey 建议的那样的自平衡搜索树会是最快的,但有一种更容易实现的替代方案。

只需将仅在向量中出现一次的 $k$ 项保留在无序映射中,就像这样说 ak 建议的那样,而将其他 $m$ 唯一项保留在无序映射中。然后,您跟踪每个结构中的项目总数,当您必须对地图进行采样时,您接受 $O(m)$ 时间。但是,正如您所期望的 $k >> m$,这可能不是问题。k items that only occur once in a vector, like thus spake a.k. suggested, and the other m unique items in an unordered map. You then keep track of the total number of items in each structure, and when you have to sample the map, you accept the O(m) time. However as you expect k>>m, this is likely not a problem.