5

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.

esac
  • 22,369
  • 35
  • 114
  • 171
  • 2
    Check out how numbers from 0 to 15 (4 groups - 15=2^4-1) look in binary notation: 0000, 0001, 0010, 0011,.. 1110, 1111. – DK. Feb 01 '11 at 21:49
  • @DK: very nice. Far easier than what I was thinking. – John Feb 01 '11 at 21:50
  • It doesn't look to me like what you've listed is really all subsets. What about `10 + 20`, for example? Any reason that's not included in the output? – Dan Tao Feb 01 '11 at 22:02
  • @Dan, sorry just didn't want to type it out, thought it would be assumed. I'm lazy. Will update it with that. – esac Feb 01 '11 at 22:04
  • How many groups are there in reality? – Eric Lippert Feb 01 '11 at 22:05
  • @esac: That's fine, just wanted to make sure I wasn't missing something or somehow misunderstanding your definition of "all subsets." – Dan Tao Feb 01 '11 at 22:06
  • 1
    15 groups has 32000 subsets. Are you OK with checking them all, or do you need to be more clever about it? The reason I ask is because this is starting to sound like a variation of the famous "subset sum" problem, and that is known to be NP-complete. That is, it is known to be not solvable *exactly* in a reasonable amount of time once the problem size gets even slightly large. 15 groups, no problem. 150 groups, forget it, you'll wait until the universe ends before you get an answer. – Eric Lippert Feb 01 '11 at 22:17
  • I am ok, because I could easily add 'stop if the value equals to the delta that I have calculated' and print out if no groups match to see if i am off base. I agree, I would only run it for 10-15 groups and not much more. – esac Feb 01 '11 at 22:30

8 Answers8

6

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.

Eric Lippert
  • 612,321
  • 166
  • 1,175
  • 2,033
  • The question did say floats, but that would probably be a harder problem. –  Feb 01 '11 at 23:22
  • 1
    @Moron: What difference does it make whether its floats or integers? Floats are just a fancy way to notate an integer, same way as dollars are just a fancy way to notate pennies. That is, a float is an integer, plus "where to put the decimal place", the same way dollars are just pennies with a decimal place stuck in there. There's no fundamental difference between them. The only tricky bit is dealing with equality, since floats can have rounding error. – Eric Lippert Feb 01 '11 at 23:39
  • @Eric: If you look at it that way: Floats represent only a subset of integers. That Subset-Sum is NP-Hard for integer input does not carry over in an "obvious" way to floats. For instance MAX-CUT is in P for planar graphs, a subset of general graphs (for which it is NP-Hard). Also, once you come to floats, you are into 'approximation' algorithm zone and if you are there, there might as well be some polynomial time algorithms (dependent on the error threshold, of course) worth considering. Of course, it might still be NP-Hard for floats, but your answer does not _immediately_ answer that. –  Feb 02 '11 at 00:12
  • Also, with floats, addition etc are tricky too (not just equality checks). You would really need be very precise to even define the SUBSET-SUM problem in a meaningful way for floats, to talk about NP-Hardness etc. btw, If you start assuming arbitrary precision reals/floats, then P = NP :-) –  Feb 02 '11 at 00:19
  • http://harrisonbrown.wordpress.com/2009/12/13/the-importance-of-choosing-the-right-model/ –  Feb 02 '11 at 00:35
  • @Moron: You make an excellent point; however, I think it would be surprising in the extreme if there were a poly solution to some appropriately defined analog of subset sum on floats. And it seems reasonable to suppose (lacking any information to the contrary from the original poster) that the floats in question are going to be roughly the same magnitude and therefore probably all integral multiples of some reasonably sized quantity. – Eric Lippert Feb 02 '11 at 01:45
  • @Eric: There are approximation algorithms which give an approximate result, in the range [(1-c)T, T] (where T is the target) in polynomial time, in 1/c. If the numbers were integers, we have pseudo-polynomial time dynamic programming algorithms for subset-sum, which in fact, I suppose will be what OP would probably need to use if 10-15 grows larger (that DP algo would actually be practical enough if the numbers involved will be small). It is not clear if that would carry over to floats. Anyway, practically speaking, I agree with you. It should not really matter :-) –  Feb 02 '11 at 02:00
2

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);
    }
}
MarioVW
  • 1,945
  • 2
  • 19
  • 27
0

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.

OGHaza
  • 4,683
  • 7
  • 21
  • 29
bleepzter
  • 8,463
  • 11
  • 38
  • 62
  • 1
    This solution wouldn't scale well when he has, say 10 groups. You can't expect him to manually add all permutations and combinations. – xbonez Feb 01 '11 at 21:56
  • @xbonez, Yea sorry. I didn't realize it was an algorithm issue. I just edited it. – bleepzter Feb 01 '11 at 22:18
0

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

Community
  • 1
  • 1
bobber205
  • 11,636
  • 25
  • 71
  • 97
  • Where are all of these seemingly nonsense answers coming from. A) i am using floats, not integers as per my edit earlier, B) the language you linked to is not C# (and i think there is value in solving it in the language that was specified), C) it doesn't even seem to solve the same problem? – esac Feb 01 '11 at 22:01
  • You use the bit representation to determine which numbers in your array are "on" and then add all the "on" ones as your nth result. – bobber205 Feb 02 '11 at 00:25
  • Do 2^number of items. For each number, convert to byte representation, then do your addition based on that info. – bobber205 Feb 02 '11 at 00:26
0

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 );
    }
}
Twitch
  • 46
  • 3
  • Unless I am reading this incorrectly, this is giving A + B, A + C, A + D, but not A + B + C which I also need. – esac Feb 01 '11 at 22:14
  • You were absolutely right; I've updated my answer with some code that actually solves the problem. – Twitch Feb 02 '11 at 01:27
0

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
Chris Haas
  • 47,821
  • 10
  • 127
  • 248
  • You don't need to play around with strings (assuming that VB has a % operator or a bitwise & operator). Loop through a copy of i testing the bottom bit for 0 or 1 and then dividing by 2. – Peter Taylor Feb 01 '11 at 22:35
  • Agreed, but it makes a lot more sense to look at (at least for me) and perf isn't an issue – Chris Haas Feb 01 '11 at 22:38
0

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.

Community
  • 1
  • 1
patros
  • 7,203
  • 2
  • 25
  • 36
0

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
m0sa
  • 10,082
  • 3
  • 38
  • 85