C# 如何实现序列的约束洗牌

C# 如何实现序列的约束洗牌,c#,.net,algorithm,linq,shuffle,C#,.net,Algorithm,Linq,Shuffle,我需要模拟多线程场景的输出,其中多个线程并行处理有序序列。输出不再排序,但也没有完全洗牌。我认为实现这样的洗牌应该很简单,而且不会超过10-20分钟。但事实证明,这比我想象的要复杂得多。因此,在与这个问题斗争了很多个小时,并在此过程中细化了需求之后,我成功地生成了一个具有非最佳统计行为的复杂实现。让我们从说明要求开始: 1) 该方法应返回一个延迟的IEnumerable,以便可以对无限长的序列进行洗牌。 2) 每个单独元件的随机位移应有一个严格的上限。 3) 位移分布应大致平坦。例如,以maxD

我需要模拟多线程场景的输出,其中多个线程并行处理有序序列。输出不再排序,但也没有完全洗牌。我认为实现这样的洗牌应该很简单,而且不会超过10-20分钟。但事实证明,这比我想象的要复杂得多。因此,在与这个问题斗争了很多个小时,并在此过程中细化了需求之后,我成功地生成了一个具有非最佳统计行为的复杂实现。让我们从说明要求开始:

1) 该方法应返回一个延迟的
IEnumerable
,以便可以对无限长的序列进行洗牌。
2) 每个单独元件的随机位移应有一个严格的上限。
3) 位移分布应大致平坦。例如,以
maxDisplacement=2洗牌的100个元素的序列应该有~20个元素被-2置换、~20个元素被-1置换、~20个元素未被置换、~20个元素被+1置换、~20个元素被+2置换。
4) 洗牌应该是随机的。方法的不同调用通常应返回不同的无序序列

输入和输出的示例。一个由20个元素组成的序列用
maxDisplacement=5
洗牌

输入:0、1、2、3、4、5、6、7、8、9、10、11、12、13、14、15、16、17、18、19
输出:0、3、2、5、7、1、4、6、8、12、9、11、13、10、15、16、19、14、17、18

以下是我迄今为止最好的尝试:

public static IEnumerable<TSource> ConstrainedShuffle<TSource>(
    this IEnumerable<TSource> source, Random random, int maxDisplacement)
{
    if (maxDisplacement < 1)
        throw new ArgumentOutOfRangeException(nameof(maxDisplacement));
    random = random ?? new Random();
    var buffer = new SortedDictionary<int, TSource>();

    IEnumerable<(int Index, int BufferMaxIndex)> EnumerateInternal()
    {
        int index = -1;
        int bufferMaxIndex = -1;
        foreach (var item in source)
        {
            bufferMaxIndex++;
            buffer.Add(bufferMaxIndex, item);
            if (bufferMaxIndex >= maxDisplacement)
            {
                // Start yielding when buffer has maxDisplacement + 1 elements
                index++;
                yield return (index, bufferMaxIndex);
            }
        }
        while (buffer.Count > 0) // Yield what is left in the buffer
        {
            while (!buffer.ContainsKey(bufferMaxIndex)) bufferMaxIndex--;
            index++;
            yield return (index, bufferMaxIndex);
        }
    }

    foreach (var (index, bufferMaxIndex) in EnumerateInternal())
    {
        int bufferMinIndex = buffer.First().Key;
        int selectedKey;
        if (index - bufferMinIndex >= maxDisplacement)
        {
            // Forced picking of the earliest element
            selectedKey = bufferMinIndex;
        }
        else
        {
            // Pick an element randomly (favoring earlier elements)
            int bufferRange = bufferMaxIndex - bufferMinIndex + 1;
            while (true)
            {
                var biasedRandom = Math.Pow(random.NextDouble(), 2.0);
                var randomIndex = (int)(biasedRandom * bufferRange);
                selectedKey = bufferMinIndex + randomIndex;
                if (buffer.ContainsKey(selectedKey)) break;
            }
        }
        yield return buffer[selectedKey];
        buffer.Remove(selectedKey);
    }
}
负/正位移:437841/512130

我可能错过了解决这个问题的更简单的方法


更新:我实现了一个基于的解决方案,效果非常好!在正负位移方面,混洗是对称的,在混洗块连接的点上没有可见的接缝,位移的分布几乎是平坦的(较小的位移稍微有利,但我同意)。它也很快

public static IEnumerable<TSource> ConstrainedShuffle_Probabilistic<TSource>(
    this IEnumerable<TSource> source, Random random, int maxDisplacement)
{
    if (maxDisplacement < 1)
        throw new ArgumentOutOfRangeException(nameof(maxDisplacement));
    random = random ?? new Random();
    int chunkSize = Math.Max(100, maxDisplacement);
    int seamSize = maxDisplacement;
    int chunkSizePlus = chunkSize + 2 * seamSize;
    var indexes = new List<int>(chunkSizePlus);
    var chunk = new List<TSource>(chunkSizePlus + seamSize);
    int chunkOffset = 0;
    int indexesOffset = 0;
    bool firstShuffle = true;
    int index = -1;
    foreach (var item in source)
    {
        index++;
        chunk.Add(item);
        indexes.Add(index);
        if (indexes.Count >= chunkSizePlus)
        {
            if (firstShuffle)
            {
                ShuffleIndexes(0, indexes.Count - seamSize);
            }
            else
            {
                ShuffleIndexes(seamSize, seamSize + chunkSize);
            }
            for (int i = 0; i < chunkSize; i++)
            {
                yield return chunk[indexes[i] - chunkOffset];
            }
            if (!firstShuffle)
            {
                chunk.RemoveRange(0, chunkSize);
                chunkOffset += chunkSize;
            }
            indexes.RemoveRange(0, chunkSize);
            indexesOffset += chunkSize;
            firstShuffle = false;
        }
    }
    if (firstShuffle)
    {
        ShuffleIndexes(0, indexes.Count);
    }
    else
    {
        ShuffleIndexes(seamSize, indexes.Count);
    }
    for (int i = 0; i < indexes.Count; i++)
    {
        yield return chunk[indexes[i] - chunkOffset];
    }

    void ShuffleIndexes(int suffleFrom, int suffleToExclusive)
    {
        var range = Enumerable
            .Range(suffleFrom, suffleToExclusive - suffleFrom).ToList();
        Shuffle(range);
        foreach (var i in range)
        {
            int index1 = indexes[i];
            int randomFrom = Math.Max(0, index1 - indexesOffset - maxDisplacement);
            int randomToExclusive = Math.Min(indexes.Count,
                index1 - indexesOffset + maxDisplacement + 1);
            int selectedIndex;
            int collisions = 0;
            while (true)
            {
                selectedIndex = random.Next(randomFrom, randomToExclusive);
                int index2 = indexes[selectedIndex];
                if (Math.Abs(i + indexesOffset - index2) <= maxDisplacement) break;
                collisions++;
                if (collisions >= 20) // Average collisions is < 1
                {
                    selectedIndex = -1;
                    break;
                }
            }
            if (selectedIndex != i && selectedIndex != -1)
            {
                var temp = indexes[i];
                indexes[i] = indexes[selectedIndex];
                indexes[selectedIndex] = temp;
            }
        }
    }

    void Shuffle(List<int> list)
    {
        for (int i = 0; i < list.Count; i++)
        {
            int j = random.Next(i, list.Count);
            if (i == j) continue;
            var temp = list[i];
            list[i] = list[j];
            list[j] = temp;
        }
    }
}

执行时间:450毫秒

我有一个想法,应该在有限数组上工作

假设最大位移为2:

  • 可以将索引0移动到索引1或2
  • 可以将索引1移动到索引0、2或3
  • 索引2可以移动到索引0、1、3或4
  • 索引8可以移动到6、7或9
  • 索引9可以移动到7或8
这是我的想法。让我们使用一个包含10项的数组:

working = [0,1,2,3,4,5,6,7,8,9]
available = [0,1,2,3,4,5,6,7,8,9]  // make a copy of the initial array
avail_count = 10
现在执行以下操作,直到avail_count<2:

  • 从可用的
    数组中随机选择一项
  • 选择一个介于-2和+2之间(包括-2和+2)的随机数(0、1、8和9的特殊情况除外,其中您的范围是有限的)
  • 将偏移量添加到选定的数字。这将为您提供一个索引,您将使用该索引交换在步骤1中选择的项。(这并不总是有效,请参见下文。)
  • 工作
    数组中交换这两个项目
  • 可用
    数组中,用最后一个项目替换并减少计数,以移除两个交换的项目
  • 让我举例说明

  • 从0和9(含0和9)之间选择一个随机数,然后从可用的
    数组中提取该项。假设随机数是5。
    中可用的[5]
    项为5
  • 选择一个随机偏移。假设你选了-2
  • 将-2添加到5,得到3:要交换的索引
  • 交换这两项,结果是:
    working=[0,1,2,5,4,3,6,7,8,9]
  • 步骤5,从可用的
    数组中删除3和5,并相应减少计数:

    available = [0,1,2,3,4,9,6,7,8]  count = 9
    available = [0,1,2,8,4,9,6,7]    count = 8
    
    下一次迭代将说明我在步骤3中提到的问题

  • 选择一个介于0和7(含)之间的随机数。假设我们选了2个。那里的项目是2
  • 选择一个随机偏移。假设我们选了1个
  • 将1和2相加,得到3。现在我们有一个问题。
    working[3]
    中的项目是5。我们不能用5来交换2,因为这样做会导致位移为3,这高于你所说的最大位移
  • 我可以想出两种方法来解决这个问题。第一个很简单。如果
    working[index]
    处的项不等于
    index
    ,则假定您无法交换:将其视为随机偏移量为0。只需从
    可用
    数组中删除第一个索引,减少计数,然后继续

    另一种方法是构建一个包含
    -max_displacement..+max_displacement
    范围内所有合格项目的数组,然后随机选择一个。这有O(最大位移*2)的缺点,但可以工作

    不管是什么情况,如果继续执行此操作直到
    count<2
    ,则将对数组进行洗牌,从而保持置换规则。这是否会给你你想要的位移分布是另一个问题。我得把它编好代码,试着确定一下


    现在,让它在溪流上工作?我的第一次尝试是把它分成大块。必须对此进行更多思考。

    想法:为大小为n的缓冲区预计算所有可能的选项?e、 g.对于-1,0、+1和缓冲区为3的情况,假设之前没有结转,则得到[0,1,2]、[0,2,1]、[1,0,3进位2]。所以

     [0,1,2]         has a total shift of 0
     [0,2,1]         has a total shift of 2
     [1,0,2]         has a total shift of 2
     [1,0,3] carry 2 has a total shift of 3
    
    当你有一个结转时,对这个例子做同样的事情(你有两种状态,一种是没有结转的,一种是有结转的,在这个简化的例子中结转必须在第一个单元格中)

    因此,现在您可以为每个模式分配概率,以满足平坦分布,并可以相应地随机选择一个。这将输出所有N个next值,然后进行进位并重新开始

    显然,对于大于-1,0,1的事物,您将有更多的可能性,并且您还可能有更多的项目需要推进

    现在,你能简化吗
    available = [0,1,2,3,4,9,6,7,8]  count = 9
    available = [0,1,2,8,4,9,6,7]    count = 8
    
     [0,1,2]         has a total shift of 0
     [0,2,1]         has a total shift of 2
     [1,0,2]         has a total shift of 2
     [1,0,3] carry 2 has a total shift of 3