【问题标题】:Algorithm for "consolidating" N items into K将 N 项“合并”为 K 的算法
【发布时间】:2016-07-31 04:52:50
【问题描述】:

我想知道是否有已知的算法来执行以下操作,并且还想知道如何在 C# 中实现它。也许这是一种已知类型的问题。

示例:

假设我有一堂课

class GoldMine 
{
    public int TonsOfGold { get; set; }
}

List 中的N=3 这样的项目

var mines = new List<GoldMine>() { 
    new GoldMine() { TonsOfGold = 10 }, 
    new GoldMine() { TonsOfGold = 12 },  
    new GoldMine() { TonsOfGold = 5 }
};  

然后将矿场合并为K=2 矿场将是合并

{ {Lines[0],Lines[1]}, {Lines[2]} }, // { 22 tons, 5 tons }
{ {Lines[0],Lines[2]}, {Lines[1]} }, // { 15 tons, 12 tons }
{ {Lines[1],Lines[2]}, {Lines[0]} }  // { 17 tons, 10 tons }

合并到K=1 矿场将是单一合并

{ Lines[0],Lines[1],Lines[2] } // { 27 tons }

我感兴趣的是整合过程的算法。

【问题讨论】:

  • 我不完全确定您正在寻找哪种算法(不是数学家),但这看起来确实是您可以使用复合设计模式解决的问题:codeproject.com/Articles/185797/Composite-Design-Pattern(也许如果您可以更具体地阐明您要解决的问题,我可以在代码示例方面提供更多帮助)
  • @FredKleuver 基本上,我希望将大小为N 的集合S 的所有分区放入KS 子集集合中。比如S = {A, B, C}(大小3),K = 2 --> { {{A,B}, {C}}, {{A,C}, {B}}, {{B,C}, {A}} }

标签: c# .net algorithm


【解决方案1】:

如果我没记错的话,你描述的问题是所有 k 的 k 组合数

我找到了一个代码 sn-p,我相信它可以解决您的用例,但我只是不记得我从哪里得到它。它一定来自 StackOverflow。如果有人认出了这段特定的代码,请告诉我,我会确保将其归功于它。

所以这里是扩展方法:

public static class ListExtensions
{
    public static List<ILookup<int, TItem>> GroupCombinations<TItem>(this List<TItem> items, int count)
    {
        var keys = Enumerable.Range(1, count).ToList();
        var indices = new int[items.Count];
        var maxIndex = items.Count - 1;
        var nextIndex = maxIndex;
        indices[maxIndex] = -1;
        var groups = new List<ILookup<int, TItem>>();

        while (nextIndex >= 0)
        {
            indices[nextIndex]++;

            if (indices[nextIndex] == keys.Count)
            {
                indices[nextIndex] = 0;
                nextIndex--;
                continue;
            }

            nextIndex = maxIndex;

            if (indices.Distinct().Count() != keys.Count)
            {
                continue;
            }

            var group = indices.Select((keyIndex, valueIndex) =>
                                        new
                                        {
                                            Key = keys[keyIndex],
                                            Value = items[valueIndex]
                                        })
                .ToLookup(x => x.Key, x => x.Value);

            groups.Add(group);
        }
        return groups;
    }
}

还有一个打印输出的小实用方法:

public void PrintGoldmineCombinations(int count, List<GoldMine> mines)
{
    Debug.WriteLine("count = " + count);
    var groupNumber = 0;
    foreach (var group in mines.GroupCombinations(count))
    {
        groupNumber++;
        Debug.WriteLine("group " + groupNumber);
        foreach (var set in group)
        {
            Debug.WriteLine(set.Key + ": " + set.Sum(m => m.TonsOfGold) + " tons of gold");
        }
    }
}

你会这样使用它:

var mines = new List<GoldMine>
{
    new GoldMine {TonsOfGold = 10},
    new GoldMine {TonsOfGold = 12},
    new GoldMine {TonsOfGold = 5}
};

PrintGoldmineCombinations(1, mines);
PrintGoldmineCombinations(2, mines);
PrintGoldmineCombinations(3, mines);

这将产生以下输出:

count = 1
group 1
1: 27 tons of gold
count = 2
group 1
1: 22 tons of gold
2: 5 tons of gold
group 2
1: 15 tons of gold
2: 12 tons of gold
group 3
1: 10 tons of gold
2: 17 tons of gold
group 4
2: 10 tons of gold
1: 17 tons of gold
group 5
2: 15 tons of gold
1: 12 tons of gold
group 6
2: 22 tons of gold
1: 5 tons of gold
count = 3
group 1
1: 10 tons of gold
2: 12 tons of gold
3: 5 tons of gold
group 2
1: 10 tons of gold
3: 12 tons of gold
2: 5 tons of gold
group 3
2: 10 tons of gold
1: 12 tons of gold
3: 5 tons of gold
group 4
2: 10 tons of gold
3: 12 tons of gold
1: 5 tons of gold
group 5
3: 10 tons of gold
1: 12 tons of gold
2: 5 tons of gold
group 6
3: 10 tons of gold
2: 12 tons of gold
1: 5 tons of gold

注意:这并没有考虑到集合内容的重复,我不确定你是否真的想要过滤掉那些。 这是你需要的吗?

编辑

实际上,查看您的评论,您似乎不想要重复项,并且您还希望包含 k 的较低值,所以这里是一个小的修改,取出重复项(在非常丑陋的方式,我道歉)并为您提供每组 k 的较低值:

public static List<ILookup<int, TItem>> GroupCombinations<TItem>(this List<TItem> items, int count)
{
    var keys = Enumerable.Range(1, count).ToList();
    var indices = new int[items.Count];
    var maxIndex = items.Count - 1;
    var nextIndex = maxIndex;
    indices[maxIndex] = -1;
    var groups = new List<ILookup<int, TItem>>();

    while (nextIndex >= 0)
    {
        indices[nextIndex]++;

        if (indices[nextIndex] == keys.Count)
        {
            indices[nextIndex] = 0;
            nextIndex--;
            continue;
        }

        nextIndex = maxIndex;

        var group = indices.Select((keyIndex, valueIndex) =>
                                    new
                                    {
                                        Key = keys[keyIndex],
                                        Value = items[valueIndex]
                                    })
            .ToLookup(x => x.Key, x => x.Value);

        if (!groups.Any(existingGroup => group.All(grouping1 => existingGroup.Any(grouping2 => grouping2.Count() == grouping1.Count() && grouping2.All(item => grouping1.Contains(item))))))
        {
            groups.Add(group);
        }
    }
    return groups;
}

它为 k = 2 产生以下输出:

group 1
1: 27 tons of gold
group 2
1: 22 tons of gold
2: 5 tons of gold
group 3
1: 15 tons of gold
2: 12 tons of gold
group 4
1: 10 tons of gold
2: 17 tons of gold

【讨论】:

  • 添加了一个稍微不同的实现
【解决方案2】:

这实际上是枚举一组 N 个对象的所有 K 分区的问题,通常被描述为枚举将 N 个标记的对象放入 K 个未标记的框的方式。

几乎总是这样,解决涉及枚举未标记或无序备选方案的问题的最简单方法是创建规范排序,然后找出如何仅生成规范排序的解决方案。在这种情况下,我们假设对象具有某种总排序,以便我们可以通过 1 到 N 之间的整数来引用它们,然后我们将对象按顺序放入分区中,并根据第一个对象的索引对分区进行排序在每一个。很容易看出这种排序不会产生重复,并且每个分区都必须对应于一些规范的排序。

然后我们可以用 N 个整数的序列来表示给定的规范排序,其中每个整数是相应对象的分区数。然而,并非每个 N 个整数序列都有效;我们需要约束序列,以使分区处于规范顺序(按第一个元素的索引排序)。约束很简单:序列中的每个元素必须是先前出现在序列中的某个整数(放置在已经存在的分区中的对象),或者它必须是下一个分区的索引,比最后一个分区已经存在。总结:

  • 序列中的第一个条目必须为1(因为第一个对象只能放入第一个分区);和
  • 每个后续条目至少比该点之前的最大条目多 1 且不大于 1。
    (如果我们将“在第一个条目之前的最大条目”解释为 0,则可以将这两个标准结合起来。)
  • 这还不够,因为它没有将序列严格限制为K。如果我们想找到所有的分区,那很好,但是如果我们想要所有大小正好为 K 的分区,那么我们需要将序列中的最后一个元素限制为 K,表示倒数第二个元素至少为K-1,倒数第三个元素至少为K-2,以此类推,如并且不允许任何元素大于K
    i 位置的元素必须在 [max(1, K+iN) 范围内, K]

根据上述一组简单的约束生成序列可以很容易地递归完成。我们从一个空序列开始,然后依次添加每个可能的下一个元素,递归调用这个过程来填充整个序列。只要生成可能的下一个元素的列表很简单,递归过程就很简单。在这种情况下,我们需要三个信息来生成这个列表:NK,以及到目前为止生成的最大值。

这导致以下伪代码:

GenerateAllSequencesHelper(N, K, M, Prefix):
  if length(Prefix) is N:
     Prefix is a valid sequence; handle it
  else:
    # [See Note 1]
    for i from  max(1, length(Prefix) + 1 + K - N)
          up to min(M + 1, K):
      Append i to Prefix            
      GenerateAllSequencesHelper(N, K, max(M, i), Prefix)
      Pop i off of Prefix

GenerateAllSequences(N, K):
  GenerateAllSequencesHelper(N, K, 0, [])

由于对于此过程的任何实际应用,递归深度都将受到极大限制,因此递归解决方案应该没问题。但是,即使不使用堆栈,生成迭代解决方案也非常简单。这是约束序列的标准枚举算法的一个实例:

  • 从字典序上可能最小的序列开始
  • 尽可能:
    • 向后扫描以找到最后一个可以增加的元素。 (“可能”意味着增加该元素仍会导致某些有效序列的前缀。)
    • 将该元素增加到下一个可能的最大值
    • 用尽可能小的后缀填写序列的其余部分。

在迭代算法中,向后扫描可能涉及检查 O(N) 个元素,这显然使其比递归算法慢。然而,在大多数情况下,它们将具有相同的计算复杂性,因为在递归算法中,每个生成的序列也会产生递归调用和返回所需的成本。如果每个(或至少,大多数)递归调用产生多个备选方案,则递归算法仍将是每个生成序列的 O(1)。

但在这种情况下,迭代算法很可能也会为每个生成的序列 O(1),只要扫描步骤可以在 O(1) 中执行;也就是说,只要它可以在不检查整个序列的情况下执行。

在这种特殊情况下,计算序列到给定点的最大值不是 O(1),但我们可以通过保持累积最大值向量来生成 O(1) 迭代算法。 (实际上,该向量对应于上述递归过程中的 M 参数堆栈。)

维护 M 向量很容易;一旦我们有了它,我们就可以很容易地识别序列中的“可增加”元素:元素 i 是可增加的,如果 i>0, M[i] 等于 M[i-1],并且 M[i ] 不等于 K。 [注2]

注意事项

  1. 如果我们想生成所有分区,我们将上面的 for 循环替换为更简单的循环:

     for i from 1 to M+1:
    
  2. 此答案主要基于this answer,但该问题要求所有分区;在这里,您要生成 K 分区。如前所述,算法非常相似。

【讨论】:

    猜你喜欢
    • 2019-05-22
    • 2017-08-08
    • 2010-11-04
    • 2011-06-30
    • 1970-01-01
    • 2020-12-16
    • 1970-01-01
    • 2018-05-16
    相关资源
    最近更新 更多