将 N 项分配到一个集合中至少一次

Distribute N items over a Set at least once

给定的是字典中的一组 N 个项目及其关联的出现。 现在我必须根据每个项目的总体概率为每个项目准确分配 X 个插槽,但每个项目至少有 1 个插槽。

这是我想出的:

using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;

public static class Program
{
    public static void Main( string[] args )
    {
        var dict = new Dictionary<char,int>();

        dict.Add( 'a' , 10 );   dict.Add( 'b' , 0 );
        dict.Add( 'c' , 4 );    dict.Add( 'd' , 1 );
        dict.Add( 'e' , 9 );    dict.Add( 'f' , 0 );

        var distributionMap = Distribute( dict , 40 );
    }

    public static Dictionary<T,int> Distribute<T>( Dictionary<T,int> occurMap , int slots )
    {
        var freeSlots = slots - occurMap.Count;
        var total = occurMap.Sum( x => x.Value );
        var distMap = new Dictionary<T,int>();

        foreach( var pair in occurMap )
        {
            var probability = (double)pair.Value / total;
            var assignedSlots = probability * freeSlots;

            distMap[ pair.Key ] = (int)( 1 + assignedSlots );
        }

        Debug.Assert( distMap.Select( x => x.Value ).Sum() == slots );

        return distMap;
    }
}

然而断言触发,因为从 doubleint 的转换在某个点截断了概率。

如何根据物品的数量将所有插槽至少映射一次?

因为插槽数是整数而平均频率不是 - 在初始空闲插槽分配后,您可能还有空闲插槽(如果您将频率向下舍入)或者可能分配了比实际更多的插槽(如果您围捕)。那么合理的做法是:

  1. 首先根据四舍五入的频率分配所有插槽(您已经这样做了)
  2. 然后从出现次数最多的项目开始,一个一个地分配剩余的槽位(剩余的空闲槽位不能超过项目总数)。

实施示例:

public static Dictionary<T, int> Distribute<T>(Dictionary<T, int> occurMap, int slots)
{
    if(slots < occurMap.Count)
        throw new ArgumentException("Not enough slots");

    var freeSlots = slots - occurMap.Count;
    var total = occurMap.Sum(x => x.Value);
    var distMap = new Dictionary<T, int>();
    var keysByProb = new Queue<T>();
    foreach (var pair in occurMap.OrderByDescending(c => (double)c.Value / total))
    {
        var probability = (double)pair.Value / total;
        var assignedSlots = probability * freeSlots;

        distMap[pair.Key] = 1+ (int)Math.Floor(assignedSlots);
        keysByProb.Enqueue(pair.Key);
    }
    var left = slots - distMap.Select(x => x.Value).Sum();
    while (left > 0) {
        distMap[keysByProb.Dequeue()]++;
        left--;
    }
    Debug.Assert(distMap.Select(x => x.Value).Sum() == slots);
    return distMap;
}

之前的方法根据总计数分配剩余项目,而根据小数部分分配它们似乎更合理。例如,如果要分配最后一个槽位,则 0.8 的项目应该比 45.3 的项目(并且之前已经有 45 个槽位)得到最后一个槽位

我会:

  • 用计算的整数部分初始化插槽并跟踪剩余的小数部分
  • 然后将每个项目的小数部分按降序排列并分配,直到没有空位为止

示例实现如下所示:

public static Dictionary<T,int> Distribute<T>( Dictionary<T,int> occurMap , int slots )
{
    var freeSlots = slots -  occurMap.Count;
    var totalFreeSlots = freeSlots;
    var total = occurMap.Sum( x => x.Value );
    var distMap = new Dictionary<T,int>();
    var remainingSlots = new Dictionary<T,double>();

    foreach( var pair in occurMap )
    {
        var probability = (double)pair.Value / total;
        var assignedSlots = probability * totalFreeSlots;

        var integralPart = Math.Truncate(assignedSlots);
        var fractionalPart = assignedSlots - integralPart;                    

        distMap[ pair.Key ] = 1 + (int)integralPart;
        remainingSlots[pair.Key] = fractionalPart;
        freeSlots -= (int)integralPart;
    }   

    foreach (var pair in remainingSlots.ToList().OrderByDescending(x => x.Value))
    {
        if (freeSlots == 0)
            break;

        distMap[ pair.Key ]++;
        freeSlots -= 1;
    }             

    return distMap;
}