用于"合并"的算法N个项目进入K.

时间:2016-07-31 04:52:51

标签: c# .net algorithm

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

示例:

假设我有一个班级

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

以及ListN=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 }

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

2 个答案:

答案 0 :(得分:1)

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

我找到了一个代码片段,我认为它可以解决您的用例,但我不记得从哪里得到它。它必须来自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

答案 1 :(得分:0)

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

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

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

  • 序列中的第一个条目必须为1(因为第一个对象只能放在第一个分区中);和
  • 每个后续条目至少为1,且不超过该点之前的最大条目 (如果我们解释&#34;#34之前的最大条目,第一个条目为0,则可以合并这两个标准。)
  • 这还不够,因为它并没有将序列限制为 K 。如果我们想要找到所有分区,那就没问题,但如果我们想要所有大小都精确 K 的分区,那么我们需要将序列中的最后一个元素约束为 K ,这意味着倒数第二个元素必须至少 K -1,第三个最后一个元素至少 K -2,依此类推以及不允许任何元素大于 K
    i 位置的元素必须在[max(1, K + i - N )的范围内, ķ

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

这导致以下伪代码:

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, [])

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

  • 从按字典顺序排列的最小序列开始
  • 虽然可能:
    • 向后扫描以找到可以增加的最后一个元素。 (&#34;可能是&#34;意味着增加该元素仍会产生某些有效序列的前缀。)
    • 将该元素增加到下一个可能的最大值
    • 使用尽可能小的后缀填写序列的其余部分。

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

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

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

维护 M 向量很容易;一旦我们拥有它,我们就可以很容易地确定&#34;可递增的&#34;序列中的元素:如果 i &gt; 0, M [ i ]等于,则元素 i 可递增 M [ i -1], M [ i ]不等于 K 。 [注2]

注释

  1. 如果我们想要生成所有分区,我们会用更简单的方法替换上面的for循环:

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