Для повышения производительности ключ заключается в сокращении количества ненужных итераций:
static List<Dictionary<string, int>> GetValidPermutations(int target, Dictionary<string, List<int>> data)
{
return GetValidPermutations(target, data, 0, new int[data.Count])
.Select(perm => CreateDictionary(data.Keys, perm))
.ToList();
}
static Dictionary<string, int> CreateDictionary(IEnumerable<string> keys, IEnumerable<int> values)
{
return keys.Zip(values, KeyValuePair.Create)
.ToDictionary(pair => pair.Key, pair => pair.Value);
}
static IEnumerable<int[]> GetValidPermutations(int target, Dictionary<string, List<int>> data, int level, int[] sequence)
{
if (level < sequence.Length)
{
var currentList = data.ElementAt(level).Value;
var subsequentLists = data.Skip(level + 1).Select(x => x.Value);
int start = Math.Max(currentList[0], target - subsequentLists.Sum(x => x.Last()));
int end = Math.Min(currentList.Last(), target - subsequentLists.Sum(x => x[0]));
for (sequence[level] = start; sequence[level] <= end; sequence[level]++)
{
int subTarget = target - sequence[level];
foreach (var perm in GetValidPermutations(subTarget, data, level + 1, sequence))
{
yield return perm;
}
}
}
else
{
var perm = sequence.Append(target);
System.Diagnostics.Debug.Assert(perm.Sum() == 100);
yield return perm.ToArray();
}
}
Вышеприведенные значения start
и end
тщательно рассчитываются для включения только необходимых итераций. Другие значения пропускаются, потому что они не могут сформировать перестановку.
Затем вы можете вызвать метод следующим образом:
var p4 = GetValidPermutations(100, data);
Console.WriteLine($"Found (Recursion): {p4.Count()}");
Может быть, трудно сначала понять версию рекурсии, там это эквивалент для l oop, вы можете видеть, что некоторые разделы кода повторяются:
const int TARGET = 100;
var permutations3 = new List<Dictionary<string, int>>();
int aStart = Math.Max(data["A"][0], TARGET - data["B"].Last() - data["C"].Last() - data["D"].Last());
int aEnd = Math.Min(data["A"].Last(), TARGET - data["B"][0] - data["C"][0] - data["D"][0]);
for (int a = aStart; a <= aEnd; a++)
{
int bStart = Math.Max(data["B"][0], TARGET - a - data["C"].Last() - data["D"].Last());
int bEnd = Math.Min(data["B"].Last(), TARGET - a - data["C"][0] - data["D"][0]);
for (int b = bStart; b <= bEnd; b++)
{
int cStart = Math.Max(data["C"][0], TARGET - a - b - data["D"].Last());
int cEnd = Math.Min(data["C"].Last(), TARGET - a - b - data["D"][0]);
for (int c = cStart; c <= cEnd; c++)
{
var perm = new Dictionary<string, int>
{
{ "A", a },
{ "B", b },
{ "C", c },
{ "D", TARGET - a - b - c }
};
System.Diagnostics.Debug.Assert(perm["D"] >= data["D"][0] && perm["D"] <= data["D"].Last());
permutations3.Add(perm);
}
}
}
Console.WriteLine($"Found (for): {permutations3.Count()}");
Пропускающая логика c может быть проиллюстрирована следующими примерами:
Предположим, что максимальные значения B, C, D равны 10, 20, 30 соответственно, тогда A должно быть не менее 40, чтобы иметь сумму 100. Таким образом, A может начинаться с 40, а 0-39 пропускаются (если доступно) .
Аналогичные логи c могут применяться для пропуска более высоких диапазонов. Предположим, что минимальные значения B, C, D равны 5, 10, 15 соответственно, тогда A не может превышать 70. Поскольку сумма будет превышать 100, если это так. Поэтому мы можем прекратить зацикливание, когда A превысит 70.
Применение лога пропуска c для всех категорий может привести к приведенному выше коду. Кроме того, последняя категория может быть рассчитана напрямую, без циклов.