如何在遵守一些约束的同时生成数字序列?

时间:2019-03-18 18:19:56

标签: c# linq combinatorics

在遵守一系列约束的同时,我需要生成从0到999999(无重复)的所有可能数字(整数)。

为更好地理解要求,请想象每个数字由2位数字的前缀和4位数字的后缀组成。就像000000被读为00-0000和999999被读为99-9999。现在来看规则:

  • 前缀必须为随机顺序
  • 后缀必须随机排列,同时确保序列中的每个10k数字都具有从0000到9999的所有数字。
  • 必须能够以给定种子的相同顺序再次生成数字。
  • 这不是真正的要求,但是如果使用Linq完成,那就太好了。

到目前为止,我已经写了一些可以满足除第一个条件以外的所有要求的代码:

var seed = 102111;
var rnd = new Random(seed);
var prefix = Enumerable.Range(0, 100).OrderBy(p => rnd.Next());
var suffix = Enumerable.Range(0, 10000).OrderBy(s => rnd.Next());
var result = from p in prefix
                from s in suffix
                select p.ToString("d2") + s.ToString("d4");

foreach(var entry in result)
{
    Console.WriteLine(entry);
}

使用此代码,我可以使用相同的种子来重现序列,前10000个数字的所有数字都从0000到9999,第二个10k等等,但是前缀并不是真正随机的,因为每个10k组将具有相同的前缀。

我还考虑过创建一个带有数字的班级,该班级是一个小组(100个小组,每个小组有1万个数字),以便于洗牌,但我相信这是更好,更简单的方法。

3 个答案:

答案 0 :(得分:8)

[基于对问题的误解,我已经覆盖了一个较早的,错误的解决方案]。


我们首先创建一个辅助方法,该方法根据给定的种子产生随机排列的范围:

static IEnumerable<int> ShuffledRange(int size, int seed)
{
  var rnd = new Random(seed);
  return Enumerable.Range(0, size).OrderBy(p => rnd.Next());
}

我们下一步要做的是将所有后缀随机化,并把它们全部排列成一个序列。请注意,每个随机播放都使用不同的种子,但是种子的值是可以预测的。

static IEnumerable<string> ShuffledIds(int seed)
{
  const int s = 10000;
  const int p = 100;
  var suffixes = Enumerable.Range(0, p)
    .Select(seedOffset => ShuffledRange(s, seed + seedOffset)
    .SelectMany(x => x);

我们已经满足了以下约束:每个10000的块都具有10000个后缀,并且顺序是随机的。现在我们必须分配10000个前缀。让我们为每个可能的后缀添加前缀序列。 (同样,每个随机播放都使用尚未使用的种子。)

  var dict = new Dictionary<int, IEnumerator<int>>();
  for (int suffix = 0; suffix < s; suffix += 1)
    dict[suffix] = ShuffledRange(p, seed + p + suffix).GetEnumerator();

现在我们可以分发它们了

  foreach(int suffix in suffixes)
  {
    dict[suffix].MoveNext();
    yield return dict[suffix].Current.ToString("d2") +
     suffix.ToString("d4");
  }
}

那应该做到的。

请注意,这也具有很好的特性,即改组算法不再是需要改组的代码的关注点。尝试将此类细节封装在辅助函数中。

答案 1 :(得分:3)

使用ckuri发表的想法并包括Eric Lippert建议的改进,您可以按后缀对数字列表进行分组:

var prefixLength = 100;
var suffixLength = 10000;

 Enumerable
  .Range(0, prefixLength * suffixLength)
  .OrderBy(number => rnd.Next())
  .GroupBy(number => number % suffixLength)

然后,您可以整理列表:

Enumerable
 .Range(0, prefixLength * suffixLength)
 .OrderBy(number => rnd.Next())
 .GroupBy(number => number % suffixLength)
 .SelectMany(g => g)

直到此处,您将拥有一个数字列表,其中每100行(prefixLength)中的前缀将相同。因此,您可以选择它们,获取每行的索引:

Enumerable
 .Range(0, prefixLength * suffixLength)
 .OrderBy(number => rnd.Next())
 .GroupBy(number => number % suffixLength)
 .SelectMany(g => g)
 .Select((g, index) => new { Index = index, Number = g })

使用索引信息,您可以使用prefixLength作为因子对应用mod函数的行进行分组:

Enumerable
 .Range(0, prefixLength * suffixLength)
 .OrderBy(number => rnd.Next())
 .GroupBy(number => number % suffixLength)
 .SelectMany(g => g)
 .Select((g, index) => new { Index = index, Number = g })
 .GroupBy(g => g.Index % prefixLength, g => g.Number)

最后,您可以再次拉平列表,并将值转换为字符串,以获得最终结果:

Enumerable
 .Range(0, prefixLength * suffixLength)
 .OrderBy(number => rnd.Next())
 .GroupBy(number => number % suffixLength)
 .SelectMany(g => g)
 .Select((g, index) => new { Index = index, Number = g })
 .GroupBy(g => g.Index % prefixLength, g => g.Number)
 .SelectMany(g => g)
 .Select(number => $"{number/suffixLength:d2}{number%suffixLength:d4}")

答案 2 :(得分:1)

此解决方案的灵感来自Rodolfo Santos的answer。通过对每个组中共享相同后缀的数字进行混洗,从而完成了结果序列的随机性,从而改善了他的解决方案。该算法利用了LINQ的OrderBy排序稳定的事实,因此按前缀对数字进行排序不会随机破坏先前的顺序。如果不是这种情况,则需要额外的分组和展平。

public static IEnumerable<int> RandomConstrainedSequence(
    int prefixLength, int suffixLength, int seed)
{
    var random = new Random(seed);
    return Enumerable
    .Range(0, prefixLength * suffixLength)
    .OrderBy(_ => random.Next()) // Order by random
    .OrderBy(n => n / suffixLength) // Order by prefix (randomness is preserved)
    .Select((n, i) => (n, i)) // Store the index
    .GroupBy(p => p.n % suffixLength) // Group by suffix
    // Suffle the numbers inside each group, and zip with the unsuffled stored indexes
    .Select(g => g.OrderBy(_ => random.Next()).Zip(g, (x, y) => (x.n, y.i)))
    .SelectMany(g => g) // Flatten the sequence
    .OrderBy(p => p.i) // Order by the stored index
    .Select(p => p.n); // Discard the index and return the number
}

用法示例:

int index = 0;
foreach (var number in RandomConstrainedSequence(5, 10, 0))
{
    Console.Write($"{number:00}, ");
    if (++index % 10 == 0) Console.WriteLine();
}

输出:

  

44、49、47、13、15、00、02、01、16、48,
  25、30、29、41、43、32、38、46、04、17,
  23,19,35,28,07,34,20,31,26,12,
  36,10,22,08,27,21,24,45,39,33,
  42,18,09,03,06,37,40,11,05,14,


更新:此解决方案可以推广解决更大范围的问题,其中将排序限制在序列的每个子组中。这是一种扩展方法,完全可以做到这一点:

public static IEnumerable<TSource> OrderGroupsBy<TSource, TGroupKey, TOrderKey>(
    this IEnumerable<TSource> source,
    Func<TSource, TGroupKey> groupByKeySelector,
    Func<TSource, TOrderKey> orderByKeySelector)
{
    return source
        .Select((x, i) => (Item: x, Index: i))
        .GroupBy(e => groupByKeySelector(e.Item))
        .Select(group =>
        {
            var itemsOrdered = group.Select(e => e.Item).OrderBy(orderByKeySelector);
            var indexesUnordered = group.Select(e => e.Index);
            return itemsOrdered.Zip(indexesUnordered, (x, i) => (Item: x, Index: i));
        })
        .SelectMany(group => group)
        .OrderBy(pair => pair.Index)
        .Select(pair => pair.Item);
}

通过另一个示例可以更清楚地看到此方法的效果。名称数组是有顺序的,但是顺序受每个以相同字母开头的名称子组的约束:

var source = new string[] { "Ariel", "Billy", "Bryan", "Anton", "Alexa", "Barby" };
Console.WriteLine($"Source: {String.Join(", ", source)}");
var result = source.OrderGroupsBy(s => s.Substring(0, 1), e => e);
Console.WriteLine($"Result: {String.Join(", ", result)}");
Source: Ariel, Billy, Bryan, Anton, Alexa, Barby
Result: Alexa, Barby, Billy, Anton, Ariel, Bryan

使用这种扩展方法,可以像这样解决原始问题:

public static IEnumerable<int> RandomConstrainedSequence(
    int prefixLength, int suffixLength, int seed)
{
    var random = new Random(seed);
    return Enumerable
        .Range(0, prefixLength * suffixLength)
        .OrderBy(_ => random.Next()) // Order by random
        .OrderBy(n => n / suffixLength) // Order again by prefix
        // Suffle each subgroup of numbers sharing the same suffix
        .OrderGroupsBy(n => n % suffixLength, _ => random.Next());
}