找到对象的唯一排列

时间:2017-07-25 04:00:11

标签: c# algorithm knapsack-problem

我有一个具有ID和数量的产品列表,我需要找到一个将填充一定数量的产品组合列表。

E.g。

ProductID | Quantity
1         | 5
2         | 5
3         | 8
4         | 15

如果我需要15的数量,那么我想得到一个包含以下组合的列表:

Products: {1, 2, 3}, {1, 3, 2}, {1, 2, 4}, {1, 3, 4}, {1, 4}
          {2, 1, 3}, {2, 1, 4}, {2, 3, 1}, {2, 3, 4}, {2, 4}
          {3, 1, 2}, {3, 1, 4}, {3, 2, 1}, {3, 2, 4}, {3, 4}
          {4}

这几乎是一种排列,但它过滤掉的条目总和超过了要求。我需要停止采取进一步的项目,如果在任何时候,当前的总和值超过15.这样,如果我有所有排列,那么我将有24个结果,但我只有16个。

E.g。如果我拿产品4然后我不需要将它与任何东西组合起来15。同样,如果我拿产品1然后拿产品4,我不需要再拿起产品,因为总和已超过15( 5 + 15 = 20)。

我能够通过获取所有排列(例如here)然后将其过滤到我关心的那些来获得代码,但是一旦你开始获得大量产品(例如30)最终导致4.3亿个组合导致内存异常。

如何在C#中仅创建所需的排列?

3 个答案:

答案 0 :(得分:1)

看起来只有两条规则:
1.挑选的元素是不同的。
2.拾取元素的总和数量必须大于目标,而不仅仅等于目标。

我的示例添加了一些用于排序的界面。列出了可以达到目标的各种组合。但我试图以独特的形式列出阅读。您可以在每个组合中扩展工作
PS。为了订购目的,我添加IComparable,不是很重要。

class Product: IComparable
{
    public int ID { get; set; }
    public uint Qty { get; set; }

    public int CompareTo(object obj)
    {
        if (obj is Product)
            return this.ID.CompareTo(((Product)obj).ID);
        else
            return -1;
    }

    public override string ToString()
    {
        return string.Format("Product: {0}", this.ID);
    }
}

class Combination : List<Product>, IComparable
{
    public int Goal { get; private set; }

    public bool IsCompleted
    {
        get
        {
            return this.Sum(product => product.Qty) >= Goal;
        }
    }

    public Combination(int goal)
    {
        Goal = goal;
    }

    public Combination(int goal, params Product[] firstProducts)
        : this(goal)
    {
        AddRange(firstProducts);
    }

    public Combination(Combination inheritFrom)
        : base(inheritFrom)
    {
        Goal = inheritFrom.Goal;
    }

    public Combination(Combination inheritFrom, Product firstProduct)
        : this(inheritFrom)
    {
        Add(firstProduct);
    }

    public int CompareTo(object obj)
    {
        if (obj is Combination)
        {
            var destCombination = (Combination)obj;
            var checkIndex = 0;
            while (true)
            {
                if (destCombination.Count - 1 < checkIndex && this.Count - 1 < checkIndex)
                    return 0;
                else if (destCombination.Count - 1 < checkIndex)
                    return -1;
                else if (this.Count - 1 < checkIndex)
                    return 1;
                else
                {
                    var result = this[checkIndex].CompareTo(destCombination[checkIndex]);
                    if (result == 0)
                        checkIndex++;
                    else
                        return result;
                }
            }
        }
        else
            return this.CompareTo(obj);
    }

    public override int GetHashCode()
    {
        unchecked
        {
            return this.Select((item, idx) => item.ID * (10 ^ idx)).Sum();
        }
    }

    public override bool Equals(object obj)
    {
        if (obj is Combination)
            return ((Combination)obj).GetHashCode() == this.GetHashCode();
        else
            return base.Equals(obj);
    }
}

测试部分提供产品清单和目标。

public static void Test()
    {
        var goal = 25;
        var products = new[]
        {
            new Product() { ID = 1, Qty = 5 },
            new Product() { ID = 2, Qty = 5 },
            new Product() { ID = 3, Qty = 8 },
            new Product() { ID = 4, Qty = 15 },
            new Product() { ID = 5, Qty = 17 },
            new Product() { ID = 6, Qty = 1 },
            new Product() { ID = 7, Qty = 4 },
            new Product() { ID = 8, Qty = 6 },
        };

        var orderedProducts = products.OrderBy(prod => prod.ID);

        //one un-completed combination, can bring back muliple combination..
        //that include completed or next-staged-uncompleted combinations
        Func<Combination, IEnumerable<Combination>> job = null;

        job = (set) =>
        {
            if (set.IsCompleted)
                return new[] { set }.ToList();
            else
            {
                return orderedProducts
                    .Where(product => set.Contains(product) == false && product.ID >= set.Last().ID)
                    .Select(product => new Combination(set, product))
                    .SelectMany(combination => job(combination));
            }
        };

        var allPossibility = orderedProducts
            .Select(product => new Combination(goal, product))
            .SelectMany(combination => job(combination))
            .Where(combination => combination.IsCompleted)
            .Select(combination => new Combination(goal, combination.OrderBy(product => product.ID).ToArray()))
            .OrderBy(item => item)
            .ToList();

        foreach (var completedCombination in allPossibility)
        {
            Console.WriteLine(string.Join<int>(", ", completedCombination.Select(prod => prod.ID).ToArray()));
        }
        Console.ReadKey();
    }

答案 1 :(得分:0)

这可能不是最有效的答案,但确实给出了正确答案:

void Main()
{
    List<Product> products = new List<Product> {    new Product { ProductID = 1, Quantity = 5 },
                                                    new Product { ProductID = 2, Quantity = 5 },
                                                    new Product { ProductID = 3, Quantity = 8 },
                                                    new Product { ProductID = 4, Quantity = 15 },
                                                    };


    decimal requiredQuantity = 15;
    if (requiredQuantity < products.Sum(p => p.Quantity))
    {
        var output = Permutations(products, requiredQuantity);

        output.Dump();
    }
    else
    {
        products.Dump();
    }
}

// Define other methods and classes here
private List<Queue<Product>> Permutations(List<Product> list, decimal requiredValue, Stack<Product> currentList = null)
{
    if (currentList == null)
    {
        currentList = new Stack<Product>();
    }
    List<Queue<Product>> returnList = new List<System.Collections.Generic.Queue<Product>>();

    foreach (Product product in list.Except(currentList))
    {
        currentList.Push(product);
        decimal currentTotal = currentList.Sum(p => p.Quantity);
        if (currentTotal >= requiredValue)
        {
            //Stop Looking. You're Done! Copy the contents out of the stack into a queue to process later. Reverse it so First into the stack is First in the Queue
            returnList.Add(new Queue<Product>(currentList.Reverse()));
        }
        else
        {
            //Keep looking, the answer is out there
            var result = Permutations(list, requiredValue, currentList);
            returnList.AddRange(result);
        }
        currentList.Pop();  
    }


    return returnList;
}


struct Product
{
    public int ProductID;
    public int Quantity;
}

答案 2 :(得分:0)

我将根据Python讨论解决方案,因为我没有在这台Mac上安装C#,但C#有迭代器,所以我所说的将会有效。

首先,正如您所发现的,您不想返回整个列表。它消耗了大量的内存。而是返回https://msdn.microsoft.com/en-us/library/65zzykke(v=vs.100).aspx中的迭代器,它将依次返回列表中的每个元素。

其次,您可以使用迭代器构建迭代器。第一个是执行子集的位置,其中最后一个元素将您推到阈值以及超出阈值:

def minimal_subset_iter (product, threshold):
    # Sort smallest to the front so that we skip no combinations that
    # will lead to our threshold in any order.
    ids = list(sorted(product.keys(), key=lambda key: (product[key], key)))

    # Figure out the size of the trailing sums.
    remaining_sum = []
    total_sum = sum(product.values())
    for i in range(len(ids)):
        remaining_sum.append(
            total_sum - sum(product[ids[j]] for j in range(i)))
    remaining_sum.append(0)

    # We modify this in place and yield it over and over again.
    # DO NOT modify it in the return, make a copy of it there.
    chosen_set = []
    def _choose (current_sum, i):
        if threshold <= current_sum:
            yield chosen_set
        elif threshold <= current_sum + remaining_sum[i]:
            # Can I finish without this element?
            for x in _choose(current_sum, i+1):
                yield x

            # All of the ways to finish with this element.
            chosen_set.append(ids[i])
            current_sum = current_sum + product[ids[i]]
            for x in _choose(current_sum, i+1):
                yield x
            # Cleanup!
            chosen_set.pop()

    return _choose(0, 0)


for x in minimal_subset_iter({1: 5, 2: 5, 3: 8, 4: 15}, 15):
    print(x)

现在你需要一个迭代器,它将最小的子集转换为该子集的所有排列,其中最后一个元素将你推到阈值。

我不会写那个,因为原则很简单。此外,你必须将它翻译成另一种语言。但是我的想法是为最后一个到达终点的元素提取每种可能性,运行其余元素的排列,并在产生它之前追加最后一个元素。

这种算法在内存上非常有效(它基本上保留了字典和当前的排列),并且在性能方面也非常高效(它有很多要创建的列表,但是会浪费很少的时间来创建它没有的#39 ;需要)。但是,它确实需要一些时间来围绕这种工作方式。