[英]How to generate a sequence of numbers while respecting some constraints?
我需要生成從 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 個組,每個組有 10k 個數字)以使其更容易洗牌,但我相信這是更好、更簡單的方法。
[基於對問題的誤解,我已經覆蓋了一個早期的,錯誤的解決方案]。
我們首先創建一個輔助方法,根據給定的種子生成一個混洗范圍:
static IEnumerable<int> ShuffledRange(int size, int seed)
{
var rnd = new Random(seed);
return Enumerable.Range(0, size).OrderBy(p => rnd.Next());
}
接下來我們要做的是隨機化所有后綴並將它們全部放入序列中。 請注意,我們為每個shuffle使用不同的種子,但種子的值是可預測的。
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");
}
}
這應該做到這一點。
請注意,這也有一個很好的屬性,即洗牌算法不再是需要shuffle的代碼的關注點。 嘗試在輔助函數中封裝類似的細節。
使用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}")
此解決方案的靈感來自 Rodolfo Santos 的回答。 它通過改組共享相同后綴的每個組內的數字來改進他的解決方案,完成結果序列的隨機性。 該算法利用了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());
}
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.