esac
esac

Reputation: 24685

How to calculate a summation of an arbitrary set of numbers, and all subsets of those numbers?

Say that I have a set of numbers:

Group1 = 10, Group2 = 15, Group3 = 20, Group4 = 30

I want to output the summation of all subsets of numbers

10 + 15 = 25
10 + 15 + 20 = 45
10 + 15 + 20 + 30 = 75
15 + 20 = 35
15 + 20 + 30 = 65
20 + 30 = 50
10 + 20 = 30
10 + 30 = 40
10 + 20 + 30 = 60
... (assumed the rest is typed out)

Each of these groups will have a name, so I would want to print out the names used in the calculation before the result:

Group1 + Group2 = 25

How to do such a thing?

EDIT: to JacobM who edited tags, this is NOT homework and would appreciate an ask before you start editing it as such. I am actually at a customer site who is trying to balance a set of numbers, and the result is coming up incorrectly. My thought was to identify which group of numbers is equal to the delta between the 2 sets, and that would identify the problem directly.

Note: this would be float values, not integers.

EDIT2: added arbitrary so that it is understood that I can not just type this out once with a bunch of string.format's .. I could easily use excel at that point.

Upvotes: 5

Views: 1610

Answers (8)

bleepzter
bleepzter

Reputation: 9985

If Group is a custom data type you can overload the +, -, *, /, =, ==, != and subsequently +=, -=, *=, and /= operators as shown here: MSDN: Operator Overloading Tutorial

If your data type is a native data type: int (Int32), long, decimal, double, or float you can do the operations you have.

To output the summation of your numbers you can use:

String.Format("{0} + {1} = {2}", Group1, Group2, (Group1 + Group2));

or

String.Format("{0} + {1} + {2} = {3}", Group1, Group2, Group3, (Group1 + Group2 + Group3));

Finally if in those examples Group is a custom data type, you would also have to overload the ToString() method so that it can display properly.

<bleepzter/>

OK, Part 2 - OO Algorithm Design?

So lets say you have the following:

public class Set: List<float>
{
    public Set():base(){}

    public static Set operator+(Set set1, Set set2)
    {
        Set result = new Set();
        result.AddRange(set1.ToArray());
        result.AddRange(set2.ToArray());
        return result;
    }

    public float Sum
    {
        get
        {
            if( this.Count == 0 )
                return 0F;

            return this.Sum();                
        }
    }

    public override string ToString()
    {
        string formatString = string.Empty;
        string result = string.Empty;

        for(int i=0; i<this.Count; i++)
        {
            formatString += "{" + i.ToString() + "} + ";
        }

        formatString = result.TrimEnd((" +").ToCharArray()); // remove the last "+ ";    
        float[] values = this.ToArray();       
        result = String.Format(formatString, values);

        return String.Format("{0} = {1}", result, this.Sum);  
    }
}

The object Set will have a Sum property, as well as a ToString() method that will display the sum and all of its content.

Upvotes: 0

Twitch
Twitch

Reputation: 46

Okay, the last one wasn't as straightforward as I thought. I actually tested it this time, and it gives the correct results.

void PrintInner( string output, float total, List<KeyValuePair<string, float>> children )
{
    var parent = children[0];
    var innerChildren = new List<KeyValuePair<string, float>>();
    innerChildren.AddRange( children );
    innerChildren.Remove( parent );

    output += parent.Key + ":" + parent.Value.ToString();
    total += parent.Value;

    if( output != "" ) // Will prevent outputting "Group1:10 = 10", comment out if desired.
        Console.WriteLine( output + " = " + total.ToString() );
    output += " + ";

    while( innerChildren.Count > 0 )
    {
        PrintInner( output, total, innerChildren );
        innerChildren.RemoveAt( 0 );
    }

}


void PrintAll()
{
    var items = new List<KeyValuePair<string,float>>()
    {
        new KeyValuePair<string,float>>( "Group1", 10 ),
        new KeyValuePair<string,float>>( "Group2", 15 ),
        new KeyValuePair<string,float>>( "Group3", 20 ),
        new KeyValuePair<string,float>>( "Group4", 30 )
    }

    while( items.Count > 0 )
    {
        PrintInner( "", 0, items );
        items.RemoveAt( 0 );
    }
}

Upvotes: 0

m0sa
m0sa

Reputation: 10940

Well as already said the key to your solution lies in getting all the possible combinations! You could put something like this in a static class to register it as an extension method:

public static IEnumerable<IEnumerable<T>> Combinations<T>(this IEnumerable<T> elements, int length = -1)
{
    switch (length)
    {
        case -1:
            foreach (var combination in Enumerable.Range(1, elements.Count()).Select(count => elements.Combinations(count)).SelectMany(c => c))
                yield return combination;
            break;
        case 0:
            yield return new T[0];
            break;
        default:
            if (length < -1) throw new ArgumentOutOfRangeException("length");
            foreach (var combination in 
                elements
                .SelectMany((element, index) => 
                    elements
                    .Skip(index + 1)
                    .Combinations(length - 1)
                    .Select(previous => (new[] { element }).Concat(previous))))
                yield return combination;
            break;
    }
}

... and use it like this:

static void Main(string[] args)
{
    var groups = new[]
                    {
                        new Tuple<string, int>("Group1", 15),
                        new Tuple<string, int>("Group2", 5),
                        new Tuple<string, int>("Group3", 17),
                    };

    foreach (var sum in groups
        .Combinations()
        .Select(x => 
            string.Join(" + ", x.Select(tuple => tuple.Item1)) + 
            " = " + 
            x.Sum(tuple => tuple.Item2)))
    {
        Console.WriteLine(sum);
    }
    Console.ReadLine();
}

Output:

Group1 = 15
Group2 = 5
Group3 = 17
Group1 + Group2 = 20
Group1 + Group3 = 32
Group2 + Group3 = 22
Group1 + Group2 + Group3 = 37

Upvotes: 0

Eric Lippert
Eric Lippert

Reputation: 660098

My thought was to identify which group of numbers is equal to the delta between the 2 sets, and that would identify the problem directly.

The problem "given an integer s, and a set of integers, does any non-empty subset of the set sum to s?" is known as the "subset sum problem". It is extremely well studied, and it is NP-Complete. (See this link for a related problem.)

That is to say it is amongst the hardest problems to solve in a reasonable amount of time. It is widely believed (though at present not proved) that no polynomial-time algorithm can possibly exist for this problem. The best you can do is something like O(2^n) for a set containing n elements.

(I note that your problem is in floats, not integers. It doesn't really matter, as long as you correctly handle the comparison of the calculated sum to the target sum to handle any rounding error that might have accrued in doing the sum.)

For a small number of elements -- you say you have only 15 or so in the set -- your best bet is to just try them all exhaustively. Here's how you do that.

The trick is to realize that there is one subset for each integer from 0 to 2^n. If you look at those numbers in binary:

0000
0001
0010
0011
...

each one corresponds to a subset. The first has no members. The second has just group 1. The third has just group 2. The fourth has group 1 and group 2. And so on.

The pseudocode is easy enough:

for each integer i from 1 to 2^n
{
  sum = 0;
  for each integer b from 1 to n
  {
    if the bth bit of i is on then sum = sum + group(b)
  }
  if sum == target then print out i in binary and quit
}
quit with no solution

Obviously this is O(n 2^n). If you can find an algorithm that always does better than O(c^n), or prove that you cannot find such an algorithm then you'll be famous forever.

The Wikipedia article has a better algorithm that gives an answer much faster most but not all of the time. I would go with the naive algorithm first since it will only take you a few minutes to code up; if it is unacceptably slow then go for the faster, more complex algorithm.

Upvotes: 6

MarioVW
MarioVW

Reputation: 2524

This matches every possible combination...

static void Main(string[] args)
{
    Dictionary<string, float> groups = new Dictionary<string, float>();
    groups.Add("Group1", 10);
    groups.Add("Group2", 15);
    groups.Add("Group3", 20);
    groups.Add("Group4", 30);

    for (int i=0; i < groups.Count - 1; i++)
    {
        Iterate(groups, i, 0, "");
    }

    Console.Read();
}

private static void Iterate(Dictionary<string, float> groups, int k, float sum, string s)
{
    KeyValuePair<string, float> g = groups.ElementAt(k);

    if (string.IsNullOrEmpty(s))
    {
        s = g.Key;
    }
    else
    {
        s += " + " + g.Key;
        Console.WriteLine(s + " = " + (sum + g.Value));
    }

    for (int i = k + 1; i < groups.Count; i++)
    {
        Iterate(groups, i, sum + g.Value, s);
    }
}

Upvotes: 2

patros
patros

Reputation: 7819

This is a fairly classic combination problem. See this post for more details:

Algorithm to return all combinations of k elements from n

Effectively what you want to do is iterate from N-choose-1 through N-choose-N and calculate the sums of each subset.

Upvotes: 0

Chris Haas
Chris Haas

Reputation: 55427

Here's my 10 cents. It uses the notion that I think @DK was hinting at. You take an integer and convert it to a binary number that represents a bitmask of groups to add. 1 means add it, 0 means skip it. Its in VB but should be convertible to C# pretty easily.

    '//Create the group of numbers
    Dim Groups As New List(Of Integer)({10, 15, 20, 30})
    '//Find the total number groups (Same as 2^Groups.Count() - 1 but reads better for me)
    Dim MaxCount = Convert.ToInt32(New String("1"c, Groups.Count), 2)

    '//Will hold our string representation of the current bitmask (0011, 1010, etc)
    Dim Bits As String
    '//Will hold our current total
    Dim Total As Integer
    '//Will hold the names of the groups added
    Dim TextPart As List(Of String)
    '//Loop through all possible combination
    For I = 0 To MaxCount
        '//Create our bitmask
        Bits = Convert.ToString(I, 2).PadLeft(Groups.Count, "0")
        '//Make sure we have got at least 2 groups
        If Bits.Count(Function(ch) ch = "1"c) <= 1 Then Continue For
        '//Re-initialize our group array
        TextPart = New List(Of String)
        '//Reset our total
        Total = 0
        '//Loop through each bit
        For C = 0 To Bits.Count - 1
            '//If its a 1, add it
            If Bits(C) = "1"c Then
                Total += Groups(C)
                TextPart.Add("Group" & (C + 1))
            End If
        Next
        '/Output
        Trace.WriteLine(Join(TextPart.ToArray(), " + ") & " = " & Total)
    Next

Outputs:

Group3 + Group4 = 50
Group2 + Group4 = 45
Group2 + Group3 = 35
Group2 + Group3 + Group4 = 65
Group1 + Group4 = 40
Group1 + Group3 = 30
Group1 + Group3 + Group4 = 60
Group1 + Group2 = 25
Group1 + Group2 + Group4 = 55
Group1 + Group2 + Group3 = 45
Group1 + Group2 + Group3 + Group4 = 75

Upvotes: 0

bobber205
bobber205

Reputation: 13372

I've asked a question about converting an integer to byte representation to solve a problem similar to this.

Converting integer to a bit representation

Upvotes: 0

Related Questions