[algorithm] find all subsets that sum to a particular value

Here is an efficient solution when there is a large set of inputs (i.e. 25 to 30)

I made efficiency gains in two ways:

  • Utilized a simple rolling wheel concept for binary counting through all possible iterations, rather than using mathematically expensive base conversion language features. This "wheel" is akin to an old mechanical counter or odometer. It recursively rolls forward as many positions as necessary until we surpass the number of binary digits we have (e.g., the count of numbers in our set).
  • The main gain is from not summing the entire candidate set each time. Instead, it maintains a running sum and each time it "rolls the wheel", it adjusts the running sum only for the pieces that changed from the last candidate set it tested. This saves a lot of computation since most "wheel rolls" only change a number or two.

This solution works with negative numbers, decimal amounts, and repeated input values. Due to the wonky way that floating point decimal math works in most languages, you will want to keep your input set to only a few decimal places or you may have some unpredictable behavior.

On my old 2012-era desktop computer the given code processes 25 input values in about 0.8 seconds in javascript/node.js, and 3.4 seconds in C#.

javascript

let numbers = [-0.47, -0.35, -0.19, 0.23, 0.36, 0.47, 0.51, 0.59, 0.63, 0.79, 0.85, 
0.91, 0.99, 1.02, 1.17, 1.25, 1.39, 1.44, 1.59, 1.60, 1.79, 1.88, 1.99, 2.14, 2.31];

let target = 24.16;

displaySubsetsThatSumTo(target, numbers);

function displaySubsetsThatSumTo(target, numbers)
{
    let wheel = [0];
    let resultsCount = 0;
    let sum = 0;
    
    const start = new Date();
    do {
        sum = incrementWheel(0, sum, numbers, wheel);
        //Use subtraction comparison due to javascript float imprecision
        if (sum != null && Math.abs(target - sum) < 0.000001) {
            //Found a subset. Display the result.
            console.log(numbers.filter(function(num, index) {
                return wheel[index] === 1;
            }).join(' + ') + ' = ' + target);
            resultsCount++;
        }
    } while (sum != null);
    const end = new Date();
    
    console.log('--------------------------');
    console.log(`Processed ${numbers.length} numbers in ${(end - start) / 1000} seconds (${resultsCount} results)`);
}

function incrementWheel(position, sum, numbers, wheel) {
    if (position === numbers.length || sum === null) {
        return null;
    }
    wheel[position]++;
    if (wheel[position] === 2) {
        wheel[position] = 0;
        sum -= numbers[position];
        if (wheel.length < position + 2) {
            wheel.push(0);
        }
        sum = incrementWheel(position + 1, sum, numbers, wheel);
    }
    else {
        sum += numbers[position];
    }
    return sum;
}

-----------------------------------------------------------------
Alternate, more efficient version using Gray Code binary counting
technique as suggested in comment
-----------------------------------------------------------------

const numbers = [-0.47, -0.35, -0.19, 0.23, 0.36, 0.47, 0.51, 
    0.59, 0.63, 0.79, 0.85, 0.91, 0.99, 1.02, 1.17, 1.25,
     1.39, 1.44, 1.59, 1.60, 1.79, 1.88, 1.99, 2.14, 2.31];
const target = 24.16;

displaySubsetsThatSumTo(target, numbers);

function displaySubsetsThatSumTo(target, numbers)
{
    let resultsCount = 0;
    let sum = 0;
    let wheel = []; //binary counter
    let changeEvery = []; //how often each binary digit flips
    let nextChange = []; //when each binary digit will next flip
    for(let i = 0; i < numbers.length; i++) {
        //Initialize wheel and wheel-update data. Using Gray Code binary counting technique,
        //    whereby only one binary digit in the wheel changes on each iteration. Then only
        //    a single sum operation is required each iteration.
        wheel.push(0);
        changeEvery.push(2 ** (numbers.length - i));
        nextChange.push(2 ** (numbers.length - i - 1));
    }
   
    const start = new Date();

    const numIterations = 2 ** numbers.length;
    for (counter = 1; counter < numIterations; counter++) {
        for (let i = nextChange.length - 1; i >= 0; i--) {
            if(nextChange[i] === counter) {
                nextChange[i] += changeEvery[i];
                if (wheel[i] === 1) {
                    wheel[i] = 0;
                    sum -= numbers[i];
                }
                else {
                    wheel[i] = 1;
                    sum += numbers[i];
                }
                
                break;
            }
        }

        //Use subtraction comparison due to javascript float imprecision
        if (Math.abs(target - sum) < 0.000001) {
            //Found a subset. Display the result.
            console.log(numbers.filter((num, index) => wheel[index] === 1)
                .join(' + ') + ' = ' + target);
            resultsCount++;
        }
    }

    const end = new Date();
    
    console.log('--------------------------');
    console.log(`Processed ${numbers.length} numbers in ${(end - start) / 1000} seconds (${resultsCount} results)`);
}

C#

    public class Program
    {
        static void Main(string[] args)
        {
            double[] numbers = { -0.47, -0.35, -0.19, 0.23, 0.36, 0.47, 0.51, 0.59, 0.63, 0.79, 0.85,
                0.91, 0.99, 1.02, 1.17, 1.25, 1.39, 1.44, 1.59, 1.60, 1.79, 1.88, 1.99, 2.14, 2.31 };

            double target = 24.16;

            DisplaySubsetsThatSumTo(target, numbers);
        }

        private static void DisplaySubsetsThatSumTo(double Target, double[] numbers)
        {
            var stopwatch = new System.Diagnostics.Stopwatch();

            bool[] wheel = new bool[numbers.Length];
            int resultsCount = 0;
            double? sum = 0;

            stopwatch.Start();

            do
            {
                sum = IncrementWheel(0, sum, numbers, wheel);
                //Use subtraction comparison due to double type imprecision
                if (sum.HasValue && Math.Abs(sum.Value - Target) < 0.000001F)
                {
                    //Found a subset. Display the result.
                    Console.WriteLine(string.Join(" + ", numbers.Where((n, idx) => wheel[idx])) + " = " + Target);
                    resultsCount++;
                }
            } while (sum != null);

            stopwatch.Stop();

            Console.WriteLine("--------------------------");
            Console.WriteLine($"Processed {numbers.Length} numbers in {stopwatch.ElapsedMilliseconds / 1000.0} seconds ({resultsCount} results). Press any key to exit.");
            Console.ReadKey();
        }

        private static double? IncrementWheel(int Position, double? Sum, double[] numbers, bool[] wheel)
        {
            if (Position == numbers.Length || !Sum.HasValue)
            {
                return null;
            }
            wheel[Position] = !wheel[Position];
            if (!wheel[Position])
            {
                Sum -= numbers[Position];
                Sum = IncrementWheel(Position + 1, Sum, numbers, wheel);
            }
            else
            {
                Sum += numbers[Position];
            }
            return Sum;
        }
    }

Output

-0.35 + 0.23 + 0.36 + 0.47 + 0.51 + 0.59 + 0.63 + 0.79 + 0.85 + 0.91 + 0.99 + 1.02 + 1.17 + 1.25 + 1.44 + 1.59 + 1.6 + 1.79 + 1.88 + 1.99 + 2.14 + 2.31 = 24.16
0.23 + 0.51 + 0.59 + 0.63 + 0.79 + 0.85 + 0.99 + 1.02 + 1.17 + 1.25 + 1.39 + 1.44 + 1.59 + 1.6 + 1.79 + 1.88 + 1.99 + 2.14 + 2.31 = 24.16
-0.47 + 0.23 + 0.47 + 0.51 + 0.59 + 0.63 + 0.79 + 0.85 + 0.99 + 1.02 + 1.17 + 1.25 + 1.39 + 1.44 + 1.59 + 1.6 + 1.79 + 1.88 + 1.99 + 2.14 + 2.31 = 24.16
-0.19 + 0.36 + 0.51 + 0.59 + 0.63 + 0.79 + 0.91 + 0.99 + 1.02 + 1.17 + 1.25 + 1.39 + 1.44 + 1.59 + 1.6 + 1.79 + 1.88 + 1.99 + 2.14 + 2.31 = 24.16
-0.47 + -0.19 + 0.36 + 0.47 + 0.51 + 0.59 + 0.63 + 0.79 + 0.91 + 0.99 + 1.02 + 1.17 + 1.25 + 1.39 + 1.44 + 1.59 + 1.6 + 1.79 + 1.88 + 1.99 + 2.14 + 2.31 = 24.16
0.23 + 0.47 + 0.51 + 0.63 + 0.85 + 0.91 + 0.99 + 1.02 + 1.17 + 1.25 + 1.39 + 1.44 + 1.59 + 1.6 + 1.79 + 1.88 + 1.99 + 2.14 + 2.31 = 24.16
--------------------------
Processed 25 numbers in 0.823 seconds (6 results)

Examples related to algorithm

How can I tell if an algorithm is efficient? Find the smallest positive integer that does not occur in a given sequence Efficiently getting all divisors of a given number Peak signal detection in realtime timeseries data What is the optimal algorithm for the game 2048? How can I sort a std::map first by value, then by key? Finding square root without using sqrt function? Fastest way to flatten / un-flatten nested JSON objects Mergesort with Python Find common substring between two strings

Examples related to recursion

List all the files and folders in a Directory with PHP recursive function Jquery Ajax beforeSend and success,error & complete Node.js - Maximum call stack size exceeded best way to get folder and file list in Javascript Recursive sub folder search and return files in a list python find all subsets that sum to a particular value jQuery - Uncaught RangeError: Maximum call stack size exceeded Find and Replace string in all files recursive using grep and sed recursion versus iteration Method to get all files within folder and subfolders that will return a list

Examples related to subset

how to remove multiple columns in r dataframe? Using grep to help subset a data frame in R Subset a dataframe by multiple factor levels creating a new list with subset of list using index in python subsetting a Python DataFrame Undefined columns selected when subsetting data frame How to select some rows with specific rownames from a dataframe? Subset data to contain only columns whose names match a condition find all subsets that sum to a particular value Subset and ggplot2