简体   繁体   中英

Find subset of numbers that add up to a given number

I have a problem I need to solve using C#. There is an array of decimal numbers (representing quantities of an item received by a warehouse at different times). This array is already sorted in the order in which the quantities were received. I need to be able to find the earliest combination of quantities that sum up to a specified total quantity.

So for example, say I have some quantities that came in chronologically as follows [13, 6, 9, 8, 23, 18, 4] and say my total quantity to match is 23. Then I should be able to get [13, 6, 4] as the matching subset although [6, 9, 8] and [23] are also matching but not the earliest.

What would be the best approach/algorithm for this?

I have so far come up with a rather naive approach using recursion.

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]);
        }
    }
}

The matching subset can be as short as one element and as long as the original set (containing all elements). But to test the worst case scenario, in my test program I am using an arbitrarily long set of which only the last two match the given number.

I see that with 20 numbers in the set, it calls the recursive function over a million times with a max recursion depth of 20. If I run into a set of 30 or more numbers in production, I am fearing it will consume a very long time.

Is there a way to further optimize this? Also, looking at the downvotes, is this the wrong place for such questions?

I was unable to end up with something revolutionary, so the presented solution is just a different implementation of the same brute force algorithm, with 2 optimizations. The first optimization is using iterative implementation rather than recursive. I don't think it is significant because you are more likely to end up with out of time rather than out of stack space, but still it's a good one in general and not hard to implement. The most significant is the second one. The idea is, during the "forward" step, anytime the current sum becomes greater than the target sum, to be able to skip checking the next items that have greater or equal value to the current item. Usually that's accomplished by first sorting the input set, which is not applicable in your case. However, while thinking how to overcome that limitation, I realized that all I need is to have for each item the index of the first next item which value is less than the item value, so I can just jump to that index until I hit the end.

Now, although in the worst case both implementations perform the same way, ie may not end in a reasonable time, in many practical scenarios the optimized variant is able to produce result very quickly while the original still doesn't end in a reasonable time. You can check the difference by playing with maxQtys and maxQty parameters.

Here is the implementation described, with test code:

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())) + "}");
        }
    }
}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM