在遵守一系列约束的同时,我需要生成从0到999999(无重复)的所有可能数字(整数)。
为更好地理解要求,请想象每个数字由2位数字的前缀和4位数字的后缀组成。就像000000被读为00-0000和999999被读为99-9999。现在来看规则:
到目前为止,我已经写了一些可以满足除第一个条件以外的所有要求的代码:
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万个数字),以便于洗牌,但我相信这是更好,更简单的方法。
答案 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());
}