Algorithm for “consolidating” N items into K

后端 未结 2 1921
無奈伤痛
無奈伤痛 2021-01-29 08:21

I was wondering whether there\'s a known algorithm for doing the following, and also wondering how it would be implemented in C#. Maybe this is a known type of problem.

2条回答
  •  广开言路
    2021-01-29 08:57

    If I'm not mistaken, the problem you're describing is Number of k-combinations for all k

    I found a code snippet which I believe addresses your use case but I just can't remember where I got it from. It must have been from StackOverflow. If anyone recognized this particular piece of code, please let me know and I'll make sure to credit it.

    So here's the extension method:

    public static class ListExtensions
    {
        public static List> GroupCombinations(this List 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>();
    
            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;
        }
    }
    

    And a little utility method that prints the output:

    public void PrintGoldmineCombinations(int count, List 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");
            }
        }
    }
    

    You would use it like so:

    var mines = new List
    {
        new GoldMine {TonsOfGold = 10},
        new GoldMine {TonsOfGold = 12},
        new GoldMine {TonsOfGold = 5}
    };
    
    PrintGoldmineCombinations(1, mines);
    PrintGoldmineCombinations(2, mines);
    PrintGoldmineCombinations(3, mines);
    

    Which will produce the following output:

    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
    

    Note: this does not take into account duplicates by the contents of the sets and I'm not sure if you actually want those filtered out or not. Is this what you need?

    EDIT

    Actually, looking at your comment it seems you don't want the duplicates and you also want the lower values of k included, so here is a minor modification that takes out the duplicates (in a really ugly way, I apologize) and gives you the lower values of k per group:

    public static List> GroupCombinations(this List 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>();
    
        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;
    }
    

    It produces the following output for 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
    

提交回复
热议问题