如何实现序列的约束改组

时间:2019-07-09 14:48:31

标签: c# .net algorithm linq shuffle

我需要模拟多线程方案的输出,在这种情况下,多个线程正在并行处理有序序列。输出不再有序,但也没有完全混洗。我认为实施这样的混洗应该是微不足道的,并且不会超过10到20分钟。但是事实证明,这比我要难得多。因此,经过许多小时的努力,并在此过程中不断完善了要求,现在,我设法产生了一个具有非最佳统计行为的复杂实现。让我们先说明一下要求:

1)该方法应返回一个延迟的IEnumerable,以便可以对无限长的序列进行重新排序。
2)每个元素的随机位移应有一个硬上限。
3)位移分布应大致平坦。例如,用maxDisplacement = 2改组的100个元素序列应使〜20个元素偏移-2,〜20个偏移-1,〜20个不偏移,〜20个+1和〜20个+2。 > 4)改组应该是随机的。通常,该方法的不同调用应返回不同的改组序列。

输入和输出示例。 maxDisplacement = 5将20个元素的序列改组。

  

输入: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);
    }
}

此实现不符合第三项要求。位移的分布是一条奇怪的曲线,在最大正位移处具有一个峰值(对于较大的maxDisplacement值会大大放大)。这是一个以maxDisplacement = 10改组的1,000,000个元素序列的分布:

-10: 44,188
 -9: 44,199
 -8: 43,701
 -7: 43,360
 -6: 43,134
 -5: 43,112
 -4: 42,870
 -3: 43,628
 -2: 44,170
 -1: 45,479
  0: 50,029
 +1: 58,611
 +2: 67,077
 +3: 71,663
 +4: 70,175
 +5: 62,914
 +6: 52,835
 +7: 40,974
 +8: 30,553
 +9: 21,210
+10: 36,118
  

负/正位移:437,841 / 512,130

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


更新:我基于Jim Mischelsuggestion实现了一个解决方案,效果很好!混洗在正向和负向位移之间是对称的,在经过混洗的块连接的点处没有可见的接缝,并且位移的分布几乎是平坦的(较小的位移被稍许偏爱,但我可以接受)。这也非常快。

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;
        }
    }
}

位移的样本分布。用maxDisplacement = 1000对1,000,000个元素的序列进行混洗,然后对位移进行分组并显示平均出现次数:

[-1000..-801]: 443
 [-800..-601]: 466
 [-600..-401]: 496
 [-400..-201]: 525
   [-200..-1]: 553
          [0]: 542
   [+1..+200]: 563
 [+201..+400]: 546
 [+401..+600]: 514
 [+601..+800]: 475
[+801..+1000]: 418

执行时间:450毫秒

2 个答案:

答案 0 :(得分:1)

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

给出最大排量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:

  1. available数组中随机选择一个项目。
  2. 选择一个介于-2和+2之间的随机数(包括2和+2)(特殊情况下0、1、8和9会限制您的范围)。
  3. 将偏移量添加到您选择的号码。这样就为您提供了索引,您可以通过该索引交换在步骤1中选择的项目。(这并不总是起作用,请参见下文。)
  4. 交换working数组中的这两项。
  5. available数组中,通过替换为最后一个项目并减少计数来删除两个交换的项目。

让我举例说明。

  1. 选择0到9之间的一个随机数(包括0和9),然后从available数组中提取该项目。假设随机数为5。available[5]处的项目为5。
  2. 选择一个随机偏移量。假设您选择了-2。
  3. 将-2加到5,结果3:要交换的索引。
  4. 交换这两个项目,得到:working = [0,1,2,5,4,3,6,7,8,9]

步骤5,从available数组中删除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中提到的问题。

  1. 选择0到7之间的一个随机数(包括0和7)。假设我们选择了2。其中的项目是2。
  2. 选择一个随机偏移量。假设我们选择了1。
  3. 将1加2,得到3。现在我们遇到了问题。 working[3]处的项目为5。我们不能将2与5交换,因为doiong会导致3的位移,这比您声明的最大位移高。

我可以想到两种解决此问题的方法。首先很容易。如果working[index]处的项不等于index,则假定您不能交换:将其视为随机偏移量为0。只需从{{1}中删除第一个索引}数组,减少计数,然后继续。

另一种方法是构建一个available范围内所有合格项目的数组,并随机选择一个。这样做的缺点是O(max_displacement * 2),但是可以使用。

无论如何,如果继续执行此操作直到-max_displacement..+max_displacement,那么您将对数组进行混洗,并保持位移规则。这是否会给您所需的位移分布是另一个问题。我必须对其进行编码,然后旋转一下以确定它。

现在,使其在流上工作了吗?我的第一个尝试是将其大块地做。不得不再考虑一下。

答案 1 :(得分:1)

想法:预先计算大小为n的缓冲区的所有可能选项?例如对于-1,0,+ 1和3的缓冲区,假设没有,则得到[0,1,2],[0,2,1],[1,0,2],[1,0,3进位2]从以前发扬光大。所以...

table.redraw()

对于有结转的情况,请执行相同的操作(开始时有两种状态,无结转状态,有结转状态,结转必须降落在第一个单元格中在这种简化的情况下)。

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

显然,对于大于-1,0,1的东西,您将拥有更多的可能性,并且还可能有更多的项目可以结转。

现在,您可以简化一下吗?也许使用相对偏移量将选择放置在有向图中?重复时将其循环回图。将概率分配给每个分支以获得统一分布。也许把它变成一个有限状态机。

加分点:创建一个实现该算法但不知道转换概率的有限状态机。现在,使用机器学习来训练它,通过将概率分配给过渡来获得平坦的分布。

我会对此发表评论,因为这不是直接的答案,但是它的时间太长了;

相关问题