查找累加给定数字的数字子集

时间:2015-09-21 15:47:09

标签: c# algorithm sum subset

我有一个问题需要使用C#来解决。有一个十进制数字数组(表示仓库在不同时间收到的物品数量)。此数组已按接收数量的顺序排序。我需要能够找到总和达到指定总量的最早数量组合。

例如,假设我有一些按时间顺序排列的数量如下[13,6,9,8,23,18,4],并说我的总数量是23.然后我应该能得到[13,6,4]作为匹配子集,尽管[6,9,8]和[23]也匹配但不是最早的。

对此最好的方法/算法是什么?

到目前为止,我已经提出了一种使用递归的相当天真的方法。

public class MatchSubset
{
    private decimal[] qty = null;
    private decimal matchSum = 0;
    public int operations = 0;
    public int[] matchedIndices = null;
    public int matchCount = 0;
    private bool SumUp(int i, int n, decimal sum)
    {
        operations++;
        matchedIndices[matchCount++] = i;
        sum += qty[i];
        if (sum == matchSum)
            return true;
        if (i >= n - 1)
        {
            matchCount--;
            return false;
        }
        if (SumUp(i + 1, n, sum))
            return true;

        sum -= qty[i];
        matchCount--;
        return SumUp(i + 1, n, sum);
    }
    public bool Match(decimal[] qty, decimal matchSum)
    {
        this.qty = qty;
        this.matchSum = matchSum;
        matchCount = 0;
        matchedIndices = new int[qty.Count()];
        return SumUp(0, qty.Count(), 0);
    }
}

static void Main(string[] args)
{
    var match = new MatchSubset();
    int maxQtys = 20;
    Random rand = new Random(DateTime.Now.Millisecond);
    decimal[] qty = new decimal[maxQtys];
    for (int i = 0; i < maxQtys - 2; i++)
        qty[i] = rand.Next(1, 500);

    qty[maxQtys - 2] = 99910;
    qty[maxQtys - 1] = 77910;
    DateTime t1 = DateTime.Now;
    if (match.Match(qty, 177820))
    {
        Console.WriteLine(DateTime.Now.Subtract(t1).TotalMilliseconds);
        Console.WriteLine("Operations: " + match.operations);
        for (int i = 0; i < match.matchCount; i++)
        {
            Console.WriteLine(match.matchedIndices[i]);
        }
    }
}

匹配子集可以与一个元素一样短,只要原始集合(包含所有元素)。但是为了测试最糟糕的情况,在我的测试程序中,我使用的是任意长的集合,其中只有最后两个匹配给定的数字。

我看到在集合中有20个数字,它调用递归函数超过一百万次,最大递归深度为20.如果我在生产中遇到一组30个或更多的数字,我担心它会消耗很长一段时间。

有没有办法进一步优化这个?另外,看看这些问题,对于这些问题,这是错误的地方吗?

1 个答案:

答案 0 :(得分:0)

我无法最终获得革命性的东西,所以所提出的解决方案只是同一个强力算法的不同实现,有2个优化。第一个优化是使用迭代实现而不是递归。我不认为这很重要,因为你更有可能最终没有时间而不是没有堆栈空间,但它仍然是一个好的,并不难实现。最重要的是第二个。该想法是,在“前进”步骤期间,当前总和变得大于目标总和的任何时候,能够跳过检查具有与当前项目具有更大或相等值的下一个项目。通常这是通过首先对输入集进行排序来完成的,这在您的情况下是不适用的。然而,在思考如何克服这个限制时,我意识到我只需要为每个项目提供第一个下一个项目的索引值,该值小于项目值,所以我可以跳转到该索引,直到我点击结束。

现在,虽然在最坏的情况下两个实现都以相同的方式执行,即可能不会在合理的时间内结束,但在许多实际情况中,优化的变体能够非常快速地生成结果,而原始的仍然不会以合理的时间。您可以通过使用maxQtysmaxQty参数来检查差异。

以下是所描述的实现,包含测试代码:

using System;
using System.Diagnostics;
using System.Linq;

namespace Tests
{
    class Program
    {
        private static void Match(decimal[] inputQty, decimal matchSum, out int[] matchedIndices, out int matchCount, out int operations)
        {
            matchedIndices = new int[inputQty.Length];
            matchCount = 0;
            operations = 0;

            var nextLessQtyPos = new int[inputQty.Length];
            for (int i = inputQty.Length - 1; i >= 0; i--)
            {
                var currentQty = inputQty[i];
                int nextPos = i + 1;
                while (nextPos < inputQty.Length)
                {
                    var nextQty = inputQty[nextPos];
                    int compare = nextQty.CompareTo(currentQty);
                    if (compare < 0) break;
                    nextPos = nextLessQtyPos[nextPos];
                    if (compare == 0) break;
                }
                nextLessQtyPos[i] = nextPos;
            }

            decimal currentSum = 0;
            for (int nextPos = 0; ;)
            {
                if (nextPos < inputQty.Length)
                {
                    // Forward
                    operations++;
                    var nextSum = currentSum + inputQty[nextPos];
                    int compare = nextSum.CompareTo(matchSum);
                    if (compare < 0)
                    {
                        matchedIndices[matchCount++] = nextPos;
                        currentSum = nextSum;
                        nextPos++;
                    }
                    else if (compare > 0)
                    {
                        nextPos = nextLessQtyPos[nextPos];
                    }
                    else
                    {
                        // Found
                        matchedIndices[matchCount++] = nextPos;
                        break;
                    }
                }
                else
                {
                    // Backward
                    if (matchCount == 0) break;
                    var lastPos = matchedIndices[--matchCount];
                    currentSum -= inputQty[lastPos];
                    nextPos = lastPos + 1;
                }
            }
        }

        public class MatchSubset
        {
            private decimal[] qty = null;
            private decimal matchSum = 0;
            public int operations = 0;
            public int[] matchedIndices = null;
            public int matchCount = 0;
            private bool SumUp(int i, int n, decimal sum)
            {
                operations++;
                matchedIndices[matchCount++] = i;
                sum += qty[i];
                if (sum == matchSum)
                    return true;
                if (i >= n - 1)
                {
                    matchCount--;
                    return false;
                }
                if (SumUp(i + 1, n, sum))
                    return true;

                sum -= qty[i];
                matchCount--;
                return SumUp(i + 1, n, sum);
            }
            public bool Match(decimal[] qty, decimal matchSum)
            {
                this.qty = qty;
                this.matchSum = matchSum;
                matchCount = 0;
                matchedIndices = new int[qty.Count()];
                return SumUp(0, qty.Count(), 0);
            }
        }

        static void Main(string[] args)
        {
            int maxQtys = 3000;
            decimal matchQty = 177820;
            var qty = new decimal[maxQtys];
            int maxQty = (int)(0.5m * matchQty);
            var random = new Random();
            for (int i = 0; i < maxQtys - 2; i++)
                qty[i] = random.Next(1, maxQty);

            qty[maxQtys - 2] = 99910;
            qty[maxQtys - 1] = 77910;

            Console.WriteLine("Source: {" + string.Join(", ", qty.Select(v => v.ToString())) + "}");
            Console.WriteLine("Target: {" + matchQty + "}");

            int[] matchedIndices;
            int matchCount;
            int operations;
            var sw = new Stopwatch();

            Console.Write("#1 processing...");
            sw.Restart();
            Match(qty, matchQty, out matchedIndices, out matchCount, out operations);
            sw.Stop();
            ShowResult(matchedIndices, matchCount, operations, sw.Elapsed);

            Console.Write("#2 processing...");
            var match = new MatchSubset();
            sw.Restart();
            match.Match(qty, matchQty);
            sw.Stop();
            ShowResult(match.matchedIndices, match.matchCount, match.operations, sw.Elapsed);

            Console.Write("Done.");
            Console.ReadLine();
        }

        static void ShowResult(int[] matchedIndices, int matchCount, int operations, TimeSpan time)
        {
            Console.WriteLine();
            Console.WriteLine("Time: " + time);
            Console.WriteLine("Operations: " + operations);
            if (matchCount == 0)
                Console.WriteLine("No Match.");
            else
                Console.WriteLine("Match: {" + string.Join(", ", Enumerable.Range(0, matchCount).Select(i => matchedIndices[i].ToString())) + "}");
        }
    }
}
相关问题