简体   繁体   中英

How to generate a sequence of numbers while respecting some constraints?

I need to generate all possible numbers (integers) from 0 to 999999 (without repetition) while respecting a series of constraints.

To better understand the requirements, imagine each number being formed by a 2 digit prefix and a suffix of 4 digits. Like 000000 being read as 00-0000 and 999999 as 99-9999. Now to the rules:

  • prefixes must be in random order
  • suffixes must be in random order while making sure that each 10k numbers in the sequence have all numbers from 0000 to 9999.
  • must be able to generate the numbers again in the same order, given a seed.
  • Not really a requirement but it would be great if it was done using Linq.

Thus far, I have written some code that meets all requirements but the first one:

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

Using this I am able to reproduce the sequence by using the same seed, the first 10000 numbers have all numbers from 0000 to 9999, as does the second 10k and so on, but the prefixes are not really random since each 10k group will have the same prefix.

I also thought of creating a class with the number and it's group (100 groups, each group having 10k numbers) to make it easier to shuffle but I believe that are better, simpler ways to do it.

[I've overwritten an earlier, wrong solution based on a misunderstanding of the problem].


We start by making a helper method that produces a shuffled range based on a given seed:

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

The next thing we're going to do is to randomize all the suffixes and get all of them into a sequence. Note that we use a different seed for each shuffle, but the value of the seed is predictable.

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

We've met the constraint that each chunk of 10000 has all 10000 suffixes, in random order. Now we have to distribute 10000 of each prefix. Let's make a sequence of prefixes for each possible suffix. (Again, we use a not-yet-used seed for each shuffle.)

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

And now we can distribute them

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

And that should do it.

Notice that this also has the nice property that the shuffling algorithm is no longer the concern of the code which needs shuffles. Try to encapsulate details like that in helper functions.

Using the idea posted by ckuri and including the imporvements suggested by Eric Lippert , you can group the list of numbers by suffix:

var prefixLength = 100;
var suffixLength = 10000;

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

Then, you can flatten the list:

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

Until here, you will have a list of numbers, where, in each 100 lines (prefixLength), the prefixes will be the same. So, you can select them, getting the index of each line:

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

Using the index information, you can group the lines applying the mod function, using the prefixLength as a factor:

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)

Finally, you can flatten the list again, and convert the values to string, in order to get the final result:

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}")

This solution is inspired by Rodolfo Santos's answer . It improves over his solution by shuffling the numbers inside each group that share the same suffix, completing the randomness of the resulting sequence. The algorithm takes advantage of the fact that LINQ's OrderBy sorting is stable, so ordering the numbers by prefix does not destroy the previous order by random. If this was not the case, an extra grouping and flattening would be required.

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
}

Usage example:

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

Output:

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,


Update: This solution can be generalized to solve a larger spectrum of problems, where the sorting is constrained into each subgroup of a sequence. Here is an extension method that does exactly that:

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

The effects of this method can be more clearly seen with a different example. An array of names are ordered, but the ordering is constrained inside each subgroup of names starting with the same letter:

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

Using this extension method, the original problem can be solved like this:

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

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM