【问题标题】:How to find out all permutations of numbers that sum to 100如何找出总和为100的数字的所有排列
【发布时间】:2020-07-31 16:33:10
【问题描述】:

我正在尝试找出一种有效的方法来创建一个方法,该方法采用包含多个顺序整数列表的字典(每个列表必须从 0 或更高开始,以 100 或更低结束,但确切的数字可能会有所不同)和返回一个字典列表,其中包含所有数字之和为 100 的所有排列。

例如,对于 4 个类别:10 + 20 + 10 + 60 = 100

结果列表中的每个字典都应该为每个键存储一个整数值。

这是我想出的一些代码来说明我的问题:

using System;
using System.Collections.Generic;
using System.Linq;

namespace recursiveTest
{
    class Program
    {
        static void Main(string[] args)
        {
            Dictionary<string, List<int>> data = new Dictionary<string, List<int>>();
            data.Add("A", Enumerable.Range(0, 100).ToList());
            data.Add("B", Enumerable.Range(0, 100).ToList());
            data.Add("C", Enumerable.Range(0, 100).ToList());
            data.Add("D", Enumerable.Range(0, 100).ToList());
            // I would like to add a few entries more...

            List<Dictionary<string, int>> permutations = new List<Dictionary<string, int>>();

            foreach (var a in data["A"])
            {
                foreach (var b in data["B"])
                {
                    foreach (var c in data["C"])
                    {
                        foreach (var d in data["D"])
                        {
                            if (a + b + c + d == 100)
                            {
                                var current = new Dictionary<string, int>()
                                {
                                    ["A"] = a,
                                    ["B"] = b,
                                    ["C"] = c,
                                    ["D"] = d,
                                };
                                permutations.Add(current);
                            }
                        }
                    }
                }
            }

            Console.WriteLine($"Found (foreach): {permutations.Count()}");
            Console.ReadKey();
        }
    }
}

使用 LINQ 的替代方法:

            List<Dictionary<string, int>> permutations2 = (from a in data["A"]
                                                           from b in data["B"]
                                                           from c in data["C"]
                                                           from d in data["D"]
                                                           where a + b + c + d == 100
                                                           let current = new Dictionary<string, int>()
                                                           {
                                                               ["A"] = a,
                                                               ["B"] = b,
                                                               ["C"] = c,
                                                               ["D"] = d,
                                                           }
                                                           select current).ToList();

            Console.WriteLine($"Found (LINQ): {permutations2.Count()}");
            Console.ReadKey();

在类别(字典键)和数字开始增长之前,这并不是一项非常复杂的任务......由于字典键(类别)的数量可能会有所不同,这似乎是递归的潜在候选者,但是我无法让它工作。这两个版本有一些明显的缺点:

  1. 一旦项目和/或类别的数量增加,性能就会突然下降。
  2. 箭头形状的代码似乎是灾难的秘诀。
  3. 它会尝试遍历所有可能的组合,而实际上只有少数是有用的(总和为 100 的组合)。

用简短易读的代码和良好的性能实现预期结果的最佳方法是什么?

在尝试找出这 100 个总和值时,有没有办法过滤掉不必要的循环?


编辑: 为了澄清起见,我的想法是能够定义一个带有这样签名的方法:

private static List<Dictionary<string, int>> GetValidPermutations(Dictionary<string, List<int>> data)

然后这样称呼它:

List<Dictionary<string, int>> permutations = GetValidPermutations(data);

【问题讨论】:

  • 所以你想从每个列表中只取一个数字作为总和,对吗?
  • 对。每个类别都有一个列表,因此输入是字符串键(类别)和 List 值的字典。该方法应返回一个字典列表,其中每个字典都应具有相同的一组键(类别)和一个单独的 int 值。此输出列表中每个字典的值的总和应始终为 100。

标签: c# performance linq recursion readability


【解决方案1】:

要提升性能,关键是减少不必要的迭代次数:

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

上面的startend 经过仔细计算,仅包含必要的迭代。其他值被跳过,因为它们不能形成排列。

然后你可以这样调用方法:

var p4 = GetValidPermutations(100, data);
Console.WriteLine($"Found (Recursion): {p4.Count()}");

递归版本可能一开始就很难理解,有for循环等价物,可以看到有些代码段是重复的:

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

跳过的逻辑可以用下面的例子来说明:

假设B、C、D的最大值分别为10、20、30,那么A需要至少40才能有100之和。这样A可以从40开始,跳过0-39(如果可用)。

类似的逻辑可以应用于跳过更高的范围。假设 B、C、D 的最小值分别为 5、10、15,那么 A 不能超过 70。因为这样总和会超过 100。所以我们可以在 A 超过 70 时停止循环。

对所有类别应用跳过逻辑可能会导致上述代码。另外,最后一个类别可以直接计算,无需循环。

【讨论】:

  • 它似乎与我的意图非常接近,并且确实提高了性能。然而,这个解决方案不允许每个类别都有可变的开始/结束值,它只是假设 1-100。我的意图是允许每个类别的起始值可能为 0 或更高,而最终值可能为 100 或更低。是否可以修改它以接受提供的 Dictionary (data) 作为递归方法的输入参数?
  • @VictorDomingos 是的,您可以查看更新版本。
  • 在第一种方法中,我在return GetValidPermutations(target, data, 0, new int[data.Count]) .Select(perm =&gt; data.Keys.Zip(perm, KeyValuePair.Create) .ToDictionary(pair =&gt; pair.Key, pair =&gt; pair.Value)) .ToList(); 上收到语法错误,其中似乎找不到KeyValuePair.Create。这部分的预期结果是什么?
  • @VictorDomingos 该部分用于形成字典格式输出,因为递归方法返回 int 数组。 data.Keys.Zip(perm, KeyValuePair.Create) 将返回 IEnumerable&lt;KeyValuePair&lt;string, int&gt;&gt; 然后转换为字典。
  • @VictorDomingos 代码为了清晰而修改。
猜你喜欢
  • 1970-01-01
  • 2014-11-19
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多