0

I am new to Swift Programming, and while I could figure the rest of this out for this app I have floating in my head, I'm having a hard time figuring out how to derive a algorithm that could handle this problem:

Given a set of 4 values (probably best to use Double because some can even be fractions), derive all possible combinations that return a result of a target value--in my case, 24.

For example, a+b+c+d, a+b+d+c, a+d+b+c, d+a+b+c, and all arrangements of that set, plus all possible mathematical operators such as (a*b)^(c-d) or (a+b+c)/d.

I was able to find this article, but it doesn't quite match what I'm looking for, especially as order doesn't matter for me Number of ways to sum the items in a set to get a target value - Order matters

so for example, I could manually do each combination this way:

if a + b + c + d == 24 {
  print("\a + \b + \c + \d")
}

My goal is for the user to enter 4 values and let IOS generate a list of all possible mathematical expressions that result in 24.

Patrick

Patrick Vellia
  • 399
  • 2
  • 9
  • 2
    This question has been answered many times in other languages, such as in https://stackoverflow.com/questions/27160339/building-a-math-game-in-java and https://stackoverflow.com/questions/43837842/efficient-algorithm-to-compose-valid-expressions-with-specific-target?rq=1 – juvian Jul 03 '18 at 17:14
  • Ha, that's a pretty good meta-programming exercise. It's not so easy in a static language that can't just `eval` arbitrary strings. Although most simply, you could make a program that creates a textual representation of every possbile mathematical equation, expressed in Swift, and then have your Swift program invoke the Swift compiler on each file, run the resulting program, and check the result. – Alexander Jul 03 '18 at 17:38
  • The seance suggestion that Juvian provided: https://stackoverflow.com/questions/43837842/efficient-algorithm-to-compose-valid-expressions-with-specific-target?rq=1 Seems to be even closer, except I think I need to use Double instead of String. Also that article is in Python 3, which I'm not familiar with and seems to use functions specific to it. And Alexander--yeah, I could run each possible combination manually which is what a friend of mine did years ago for Windows but I feel like the languages have gotten somewhere by now you could do it with a function and loop. Patrick – Patrick Vellia Jul 03 '18 at 18:18
  • @Alexander: even without an `eval` function there's still ways to evaluate expressions. One way I've handled this in the past is to use SQL (e.g. `SELECT 3 + (4 * 5) - 6 FROM DUAL`). – Bob Jarvis - Слава Україні Jul 04 '18 at 03:24
  • @BobJarvis You can also model expression trees with enums. Conceptually, it's the same as raw string manipulation, and definitely preferable from a design standpoint, but it can't compete with the "quick and dirty" solutions in dynamic languages – Alexander Jul 04 '18 at 05:09

2 Answers2

2

Here is one approach. Start with an array of expressions and an array of values. Initially, the expressions is just the String value of the values:

let expressions = ["1", "2", "3", "4"]
let values = [1, 2, 3, 4]

Pick two expressions (for example "1" and "2", and a binary operator ("+"), and combine them creating an expressions and values with 3 values:

expressions = ["3", "4", "(1 + 2)"]
values = [3, 4, 3]

Repeat this process combining again two of the expressions with an operation:

expressions = ["3", "(4 + (1 + 2))"]
values = [3, 7]

Finally, combine the last two expressions with "+":

expressions = ["(3 + (4 + (1 + 2)))"]
values = [10]

Once you reach a single expression, check to see if the value matches your target.


The following is a recursive function that tries all combinations of values and operations to create the expressions in search of a target:

import Foundation

// An array of tuples containing an operator name and a closure 
// that performs the operation
let ops: [(name: String, function: (Double, Double) -> Double)] = [
    ("+", { $0 + $1 }),
    ("-", { $0 - $1 }),
    ("*", { $0 * $1 }),
    ("/", { $0 / $1 }),
    ("^", { pow($0, $1) })
]


func compute(expressions: [String], values: [Double], target: Double) {
    
    // base case of the recursion: if there is only one
    // expression and one value, check if the value is the
    // target value we're looking for and print the expression
    // and value if the target is matched
    if expressions.count == 1 {
        if values[0] == target {
            print("\(expressions[0]) = \(values[0])")
        }
    } else if expressions.count >= 2 {

        // loop through all of the expressions choosing each
        // as the first expression
        for idx1 in expressions.indices {

            // copy the expressions and values arrays so that
            // we can remove the expression and value
            // without modifying the original arrays
            // which will be needed for the next try
            var expcopy = expressions
            var valcopy = values
            let exp1 = expcopy.remove(at: idx1)
            let val1 = valcopy.remove(at: idx1)
            
            // loop through the remaining expressions to find
            // the second one
            for idx2 in expcopy.indices {
                // again, copy the arrays to keep from modifying
                // the originals while searching
                var expcopy2 = expcopy
                var valcopy2 = valcopy
                
                let exp2 = expcopy2.remove(at: idx2)
                let val2 = valcopy2.remove(at: idx2)

                // now try all possible operations to combine
                // the two expressions
                for op in ops {
                    // use the closure to compute the value
                    let value = op.function(val1, val2)

                    // combine the expressions into a new string
                    // expression with the operator in the
                    // middle and surrounded by () if this is
                    // not the top level expression
                    var exp = "\(exp1) \(op.name) \(exp2)"
                    if !expcopy2.isEmpty {
                        exp = "(\(exp))"
                    }

                    // now that we've reduced the number of
                    // expressions by 1, recurse by calling
                    // compute again on the reduced list of
                    // expressions
                    compute(expressions: expcopy2 + [exp], values: valcopy2 + [value], target: target)
                }
            }
        }
    }
}


// This helper function creates the array of expressions from
// the array of values, and then calls the main function above
// to do the real work  
func search(values: [Double], target: Double) {
    compute(expressions: values.map { String($0) }, values: values, target: target)
}

Example 1:

search(values: [1, 2, 3, 4], target: 121)

Output:

(1.0 - (3.0 * 4.0)) ^ 2.0 = 121.0
((3.0 * 4.0) - 1.0) ^ 2.0 = 121.0
(1.0 - (4.0 * 3.0)) ^ 2.0 = 121.0
((4.0 * 3.0) - 1.0) ^ 2.0 = 121.0

Example 2:

search(values: [1, 2, 3], target: 1)

Output:

3.0 / (1.0 + 2.0) = 1.0
(1.0 + 2.0) / 3.0 = 1.0
3.0 - (1.0 * 2.0) = 1.0
(1.0 ^ 2.0) ^ 3.0 = 1.0
(1.0 * 3.0) - 2.0 = 1.0
2.0 - (1.0 ^ 3.0) = 1.0
(1.0 ^ 3.0) ^ 2.0 = 1.0
3.0 / (2.0 + 1.0) = 1.0
(2.0 + 1.0) / 3.0 = 1.0
(2.0 - 1.0) ^ 3.0 = 1.0
3.0 - (2.0 * 1.0) = 1.0
3.0 - (2.0 / 1.0) = 1.0
3.0 - (2.0 ^ 1.0) = 1.0
1.0 ^ (2.0 + 3.0) = 1.0
1.0 ^ (2.0 - 3.0) = 1.0
1.0 ^ (2.0 * 3.0) = 1.0
1.0 ^ (2.0 / 3.0) = 1.0
1.0 ^ (2.0 ^ 3.0) = 1.0
2.0 / (3.0 - 1.0) = 1.0
(3.0 - 1.0) / 2.0 = 1.0
(3.0 * 1.0) - 2.0 = 1.0
(3.0 / 1.0) - 2.0 = 1.0
(3.0 ^ 1.0) - 2.0 = 1.0
1.0 ^ (3.0 + 2.0) = 1.0
1.0 * (3.0 - 2.0) = 1.0
1.0 / (3.0 - 2.0) = 1.0
1.0 ^ (3.0 - 2.0) = 1.0
(3.0 - 2.0) * 1.0 = 1.0
(3.0 - 2.0) / 1.0 = 1.0
(3.0 - 2.0) ^ 1.0 = 1.0
1.0 ^ (3.0 * 2.0) = 1.0
1.0 ^ (3.0 / 2.0) = 1.0
1.0 ^ (3.0 ^ 2.0) = 1.0

Eliminating Duplicate Solutions

With 4 or more values, or even with fewer values that aren't unique, you can end up with duplicate expressions. The way to eliminate the duplicates is to use a Set<String> to keep track of the expressions you've already found and check if that set contains your new expression before printing it as a new solution.

import Foundation

// An array of tuples containing an operator name and a closure
// that performs the operation
let ops: [(name: String, function: (Double, Double) -> Double)] = [
    ("+", { $0 + $1 }),
    ("-", { $0 - $1 }),
    ("*", { $0 * $1 }),
    ("/", { $0 / $1 }),
    ("^", { pow($0, $1) })
]


func compute(expressions: [String], values: [Double], target: Double, solutions: inout Set<String>) {
    
    // base case of the recursion: if there is only one
    // expression and one value, check if the value is the
    // target value we're looking for and print the expression
    // and value if the target is matched and we don't already
    // have that expression in our set of solutions
    if expressions.count == 1 {
        if values[0] == target && !solutions.contains(expressions[0]) {
            print("\(expressions[0]) = \(values[0])")
            solutions.insert(expressions[0])
        }
    } else if expressions.count >= 2 {
        
        // loop through all of the expressions choosing each
        // as the first expression
        for idx1 in expressions.indices {
            
            // copy the expressions and values arrays so that
            // we can remove the expression and value
            // without modifying the original arrays
            // which will be needed for the next try
            var expcopy = expressions
            var valcopy = values
            
            let exp1 = expcopy.remove(at: idx1)
            let val1 = valcopy.remove(at: idx1)
            
            // loop through the remaining expressions to find
            // the second one
            for idx2 in expcopy.indices {
                // again, copy the arrays to keep from modifying
                // the originals while searching
                var expcopy2 = expcopy
                var valcopy2 = valcopy
                
                let exp2 = expcopy2.remove(at: idx2)
                let val2 = valcopy2.remove(at: idx2)
                
                // now try all possible operations to combine
                // the two expressions
                for op in ops {
                    // use the op's function to compute the value
                    let val = op.function(val1, val2)
                    
                    // combine the expressions into a new string
                    // expression with the operator in the
                    // middle and surrounded by () if this is
                    // not the top level expression
                    var exp = "\(exp1) \(op.name) \(exp2)"
                    if !expcopy2.isEmpty {
                        exp = "(\(exp))"
                    }
                    
                    // now that we've reduced the number of
                    // expressions by 1, recurse by calling
                    // compute again on the reduced list of
                    // expressions
                    let newexp = expcopy2 + [exp]
                    let newval = valcopy2 + [val]
                    compute(expressions: newexp, values: newval, target: target, solutions: &solutions)
                }
            }
        }
    }
}


// This helper function creates the array of expressions from
// the array of values, creates a Set to hold the solutions, and
// then calls the main function above to do the real work
func search(values: [Double], target: Double) {
    // create a set to keep track of solutions found so far
    var solutions = Set<String>()
    
    compute(expressions: values.map { String($0) }, values: values, target: target, solutions: &solutions)
    
    print("\n\(solutions.count) unique solutions were found")
}

Example:

search(values: [2, 2, 1], target: 5)

Output:

1.0 + (2.0 + 2.0) = 5.0
(2.0 + 2.0) + 1.0 = 5.0
1.0 + (2.0 * 2.0) = 5.0
(2.0 * 2.0) + 1.0 = 5.0
1.0 + (2.0 ^ 2.0) = 5.0
(2.0 ^ 2.0) + 1.0 = 5.0
2.0 + (2.0 + 1.0) = 5.0
(2.0 + 1.0) + 2.0 = 5.0
2.0 + (1.0 + 2.0) = 5.0
(1.0 + 2.0) + 2.0 = 5.0

10 unique solutions were found
vacawama
  • 150,663
  • 30
  • 266
  • 294
0

A simple approach not covered in the solutions referenced in the first few comments is to generate your candidate expressions in reverse polish notation (RPN). If you've studied CS, or owned a HP calculator, you might recall this. RPN has the advantage that it contains no parentheses and is evaluated in strict left-to-right order. Follow the link for a full description, here are a couple of examples:

Algebraic: (a+b)*c

RPN: ab+c*


Algebraic: a+(b*c)

RPN: abc*+

In outline to evaluate an RPN expression left-to-right you push any variable you find onto a stack. For any operator you pop 2 values of the stack, combine the with the operation, and push the result back onto the stack.

To generate a single expression for your problem an outline algorithm is:

while there are variables unused or fewer than (number of variables - 1) operators
   either:
      add any unused variable
   or:
      if the number of added variables is at least two greater than the
      number of added operators add any operator

That will give you a single expression, to generate all of them think recursion. At each stage you iterate through all the choices as above and for each one recurse passing in the partial expression, unused variables, etc. You can store the partial expression in an array, each element being a variable or an operator (think enum). As Swift passes arrays by value as you recurse each call can continue to add elements to the array without effecting other calls.

In pseudocode:

generate(expression: array, variables: variable collection) -> collection
   results <- empty collection
   for nextVar in variables
      add generate(expression + nextVar, variables - nextVar) to results
   if #variables in expression - #operators in expression >= 2
      for every possible operator
         add generate(expression + operator, variables) to results
   return results

When a complete expression is generated your can evaluate it and add it to solutions if the result is 24. As a possible optimisation you can evaluate as you go down the recursion to save recalculation of the partial expressions. RPN evaluation can use a stack, which you can build from an array in Swift and pass down in each recursive call. Exploring other optimisation is left to you.

If you get stuck after designing your algorithm and writing some code you can ask a new question - include a link to this one, your algorithm, your code, and your problem; someone will undoubtedly help you along.

HTH

Community
  • 1
  • 1
CRD
  • 52,522
  • 5
  • 70
  • 86