从排序列表中加权随机选择

时间:2015-04-23 09:57:38

标签: c# random data-structures sortedlist

我遇到一个问题,我有一个按“权重”排序的大项目列表。我需要能够从这个列表中随机选择项目,但是更接近开始的项目(更大的权重)必须更有可能根据“精英主义”因素被选中。

我意识到之前已经问过类似的问题,但是这里的问题是这个清单会随着时间的推移而改变。删除最后一项时,新值将被排序到列表中(以保持“优化”值的常量大小)。

首先,最有效的选择方式是什么?选择必须从50到1000项长的列表中实时进行。

其次,这里使用的最佳数据结构是什么?我正在使用C#。

我只想到了一个可能的解决方案,但我想对这个想法提出一些反馈意见。如果我在一定范围内生成一个随机浮点值,然后沿着平方线做一些事情怎么办?小值将返回小值,大值将返回更大的值。据我所知,将此结果映射到列表的长度应该会产生预期的效果。听起来不错吗?

4 个答案:

答案 0 :(得分:1)

我会做类似的事情:

string[] names = new[] { "Foo", "Bar", "Fix" };

// The weights will be 3, 2, 1
int[] weights = new int[names.Length];
for (int i = 0; i < names.Length; i++)
{
    weights[i] = names.Length - i;
}

int[] cumulativeWeights = new int[names.Length];

// The cumulativeWeights will be 3, 5, 6
// so if we generate a number, 1-3 Foo, 4-5 Bar, 6 Fiz
cumulativeWeights[0] = weights[0];
int totalWeight = weights[0];

for (int i = 1; i < cumulativeWeights.Length; i++)
{
    cumulativeWeights[i] = cumulativeWeights[i - 1] + weights[i];
    totalWeight += weights[i];
}

var rnd = new Random();

while (true)
{
    int selectedWeight = rnd.Next(totalWeight) + 1; // random returns 0..5, +1 == 1..6
    int ix = Array.BinarySearch(cumulativeWeights, selectedWeight);
    // If value is not found and value is less than one or more 
    // elements in array, a negative number which is the bitwise 
    // complement of the index of the first element that is 
    // larger than value.
    if (ix < 0)
    {
        ix = ~ix;
    }

    Console.WriteLine(names[ix]);
}

我已经构建了一个weight数组。我用过线性方法。第一个元素的权重等于(元素的数量),第二个元素的权重(元素的数量 - 1),依此类推。您可以使用算法,但如果权重是整数则更容易。

然后我计算了一个cumulativeWeights数组和一个totalWeight

然后我可以在1totalWeight之间提取二进制数,并找到cumulativeWeight的&lt; =随机数的索引。由cumulativeWeights排序(显然:-)),我可以使用Array.BinarySearch,这样做的好处是,如果找不到确切的数字,则会给出下一个最大数字的索引。

现在,对于double weightRandom部分会更加复杂:

string[] names = new[] { "Foo", "Bar", "Fix" };

// The weights will be 3.375, 2.25, 1.5
double[] weights = new double[names.Length];
for (int i = 0; i < names.Length; i++)
{
    weights[i] = Math.Pow(1.5, names.Length - i);
}

double[] cumulativeWeights = new double[names.Length];

// The cumulativeWeights will be 3.375, 3.375+2.25=5.625, 3.375+2.25+1.5=7.125
// so if we generate a number, 1-3.375 Foo, >3.375-5.625 Bar, >5.625-7.125 Fiz
// totalWeight = 7.125
cumulativeWeights[0] = weights[0];
double totalWeight = weights[0];

for (int i = 1; i < cumulativeWeights.Length; i++)
{
    cumulativeWeights[i] = cumulativeWeights[i - 1] + weights[i];
    totalWeight += weights[i];
}

var rnd = new Random();

while (true)
{
    // random returns (0..1 * totalWeight - 1) + 1 = (0...6.125) + 1 = 1...7.125
    double selectedWeight = (rnd.NextDouble() * (totalWeight - 1)) + 1; 

    int ix = Array.BinarySearch(cumulativeWeights, selectedWeight);
    // If value is not found and value is less than one or more 
    // elements in array, a negative number which is the bitwise 
    // complement of the index of the first element that is 
    // larger than value.
    if (ix < 0)
    {
        ix = ~ix;
    }

    Console.WriteLine(names[ix]);
}

Random.NextDouble()方法会返回一个我们必须转换为权重的数字0<=x<1

基于该原则,可以构建一个使用它的List<T>类:

public class ListWithWeight<T>
{
    private readonly List<T> List = new List<T>();

    private readonly List<double> CumulativeWeights = new List<double>();

    private readonly Func<int, double> WeightForNthElement;

    private readonly Random Rnd = new Random();

    public ListWithWeight(Func<int, double> weightForNthElement)
    {
        WeightForNthElement = weightForNthElement;
    }

    public void Add(T element)
    {
        List.Add(element);

        double weight = WeightForNthElement(List.Count);

        if (CumulativeWeights.Count == 0)
        {
            CumulativeWeights.Add(weight);
        }
        else
        {
            CumulativeWeights.Add(CumulativeWeights[CumulativeWeights.Count - 1] + weight);
        }
    }

    public void Insert(int index, T element)
    {
        List.Insert(index, element);

        double weight = WeightForNthElement(List.Count);

        if (CumulativeWeights.Count == 0)
        {
            CumulativeWeights.Add(weight);
        }
        else
        {
            CumulativeWeights.Add(CumulativeWeights[CumulativeWeights.Count - 1] + weight);
        }
    }

    public void RemoveAt(int index)
    {
        List.RemoveAt(index);
        CumulativeWeights.RemoveAt(List.Count);
    }

    public T this[int index]
    {
        get
        {
            return List[index];
        }

        set
        {
            List[index] = value;
        }
    }

    public int Count
    {
        get
        {
            return List.Count;
        }
    }

    public int RandomWeightedIndex()
    {
        if (List.Count < 2)
        {
            return List.Count - 1;
        }

        double totalWeight = CumulativeWeights[CumulativeWeights.Count - 1];
        double selectedWeight = (Rnd.NextDouble() * (totalWeight - 1.0)) + 1;

        int ix = CumulativeWeights.BinarySearch(selectedWeight);
        // If value is not found and value is less than one or more 
        // elements in array, a negative number which is the bitwise 
        // complement of the index of the first element that is 
        // larger than value.
        if (ix < 0)
        {
            ix = ~ix;
        }

        // We want to use "reversed" weight, where first items
        // weight more:

        ix = List.Count - ix - 1;
        return ix;
    }
}

var lst = new ListWithWeight<string>(x => Math.Pow(1.5, x));
lst.Add("Foo");
lst.Add("Bar");
lst.Add("Fix");
lst.RemoveAt(0);
lst.Insert(0, "Foo2");

while (true)
{
    Console.WriteLine(lst[lst.RandomWeightedIndex()]);
}

答案 1 :(得分:1)

这就是我要做的事情:

private static int GetPosition(double value, int startPosition, int maxPosition, double weightFactor, double rMin)
{
    while (true)
    {
        if (startPosition == maxPosition) return maxPosition;

        var limit = (1 - rMin)*weightFactor + rMin;
        if (value < limit) return startPosition;
        startPosition = startPosition + 1;
        rMin = limit;
    }
}

static void Main()
{
    const int maxIndex = 100;
    const double weight = 0.1;

    var r = new Random();
    for (var i = 0; i < 200; i++)
        Console.Write(GetPosition(r.NextDouble(), 0, maxIndex, weight, 0) + " ");
}

0.1权重系数意味着第一项有10%的机会被选中。 所有其他项目都有90%。

第二项有剩余90%的10%= 9%

第3项有剩余81%的10%= 8.1%

...

当您增加权重因子时,更有可能选择第一项而不是列表中的最后一项。 系数为1时,只选择第一项。

对于0.1和10项的权重,以下是每个指数的概率:

0: 10%
1: 9%
2: 8.1%
3: 7.29%
4: 6.56%
5: 5.9%
6: 5.31%
7: 4.78%
8: 4.3%
9: 3.87%

修改

当然,这只适用于许多索引(0.1至少为10),否则会为最后一个索引提供更大的概率。 例如,如果权重= 0.1且maxIndex = 1,则索引0的概率为10%,而索引1的概率为90%。

答案 2 :(得分:1)

不幸的是,我现在无法提供任何代码,但有些想法:

当您的列表从高加权到低加权排序时,您应该能够使用基于正态分布的随机数生成器。如果您手边没有这样的随机数生成器,您可以使用此处的代码将统一分布转换为正态分布:Random Gaussian Variables

我解释得很糟糕,但我试试: 您可以将偏差(平均值)定义为0,将西格玛(偏差)定义为,比如说3.然后从生成的数字中取绝对值,因为您可能得到负数。

这将为您提供一个数字生成器,它可能在偏差数字附近有很高的值(上例中为0),并且数字偏离该数字的概率较低。

正如我所说,我在解释时非常糟糕

答案 3 :(得分:1)

创建一个二进制树,按权重排序(除了问题中指定的排序外,不需要排序),并为每个节点记录所有子节点的总权重。在这个顶部,我们可以计算整个列表的总重量。

在零和所有内容的总重量之间选择一个随机值r。在每个节点,如果当前节点的权重大于r,那么这就是您的结果。否则从r中减去当前节点的权重。现在,如果所有左侧儿童的总重量小于r,则向左移动。否则从r减去所有左孩子的总重量并向右走。重复,直到得到结果。

插入和删除成本取决于您选择如何实现和平衡树,但您还必须遍历所有祖先以更新其权重。

如果您实际上并不需要对其进行排序,那么将其作为堆可能会改善快速行为。