Difference between two products nearest to zero: non brute-force solution?
Asked Answered
N

5

15

In a science museum in Norway I came across the following mathematical game:

enter image description here

The goal is to place the 10 digits from 0 to 9 such that the difference between the two products is nearest to zero. (The 246 is the current lowest score).

Back home I wrote the following brute-force code:

import time
from itertools import permutations


def form_number(x, y, z, a, b):
    # not explicitly stated, but presume that leading zeroes are not allowed
    if x == 0 or a == 0:
        return 0
    return ((100 * x) + (10 * y) + z) * ((10 * a) + b)

def find_nearest_zero(*args):
    assert len(args) == 10
    return form_number(*args[:5]) - form_number(*args[5:])

if __name__ == '__main__':
    start = time.time()
    count = 0
    for p in permutations(range(10), 10):
        result = find_nearest_zero(*p)
        if result == 0:
            count += 1
            print '{}{}{} * {}{} = {n}'.format(*p[:5], n=form_number(*p[:5]))
            print '{}{}{} * {}{} = {n}'.format(*p[5:], n=form_number(*p[5:]))
            print
    print 'found {} solutions'.format(count)
    print time.time() - start

If we don't allow leading zeroes, then this prints 128 possible solutions in about 12 seconds.

But I'm left wondering, is there an algorithm or a better way to solve this in a non-brute force way?

Nonparticipation answered 15/7, 2016 at 9:51 Comment(1)
You can save a factor 4, if you remove combinations where either the 2 3-digit numbers or the 2 2-digit numbers are interchanged. If your calculation is ab-cd you know that this is the same as -(cd-ab) and that if a>c and b>d this is bigger than ad-cb, so you should only consider the latter.Uvulitis
U
2

If you assume there is a solution with difference 0, you can do via prime factors.

If ab - cd = 0, then the prime factors of ab and cd must be the same. You can start the search by going through all 3-digit primes (there are only 143) and see whether they have a multiple that only contains disjunctive digits. If this is the case you have your 2 three-digit numbers and only have to check the 2-digit ones.

If you don't find a solution, continue with the 2-digit primes and look for 2-or-3-digit multiples with disjunct digits. Then you only have to do the permutations for the remaining 2 numbers, which is a lot less.

Uvulitis answered 15/7, 2016 at 10:46 Comment(1)
I'm not sure this is as straightforward as you make it sound. Could you run through a practical example of how you find a solution in more detail?Gnome
W
2

This one assumes a difference of zero is possible (although it could be adapted to finding the minimum/s by sorting — thank you, m69, for that idea — each group of 120 permutations and using binary search, adding a factor of (log2 120) to the time complexity): hash the multiples for all 10 choose 5 * 5! combinations of three-digit times two-digit numbers, where the key is the sorted five-digit combination. Then if a reciprocal key (one that contains the other five digits) points to an equal multiple, output that match. Altogether 30,240 combinations are checked.

JavaScript code:

function choose(ns,r){
  var res = [];
  
  function _choose(i,_res){
    if (_res.length == r){
      res.push(_res);
      return;
      
    } else if (_res.length + ns.length - i == r){
      _res = _res.concat(ns.slice(i));
      res.push(_res);
      return
    }
    
    var temp = _res.slice();
    temp.push(ns[i]);
    
    _choose(i + 1,temp);
    _choose(i + 1,_res);
  }
  
  _choose(0,[]);
  return res;
}

function missingDigits(str){
  var res = "";

  for (var i=0; i<=9; i++){
    if (str.indexOf(i) === -1){
      res += i;
    }
  }
  
  return res;
}

var digitsHash = {};
    
function permute(digits){
  var stack = [[String(digits[0])]];
  
  for (var i=1; i<5; i++){
    var d = digits[i],
        perms = stack.shift(),
        _perms = [];
    
    for (var j=0; j<perms.length; j++){
      var temp = perms[j];
    
      for (var k=0; k<=perms[0].length; k++){
        if (d == 0 && (k == 0 || k == 3)){
          continue;
        }
        var _temp = temp;
        _temp = temp.split("");
        _temp.splice(k,0,d);
        _temp = _temp.join("")
        _perms.push(_temp);
      }
    }
    
    stack.push(_perms);
  }
  
  var reciprocalKey = missingDigits(stack[0][0]),
      key = stack[0][0].split("");

  key.sort();
  key = key.join("");

  digitsHash[key] = {};
  
  for (var i=0; i<stack[0].length; i++){
    var mult = Number(stack[0][i].substr(0,3)) * Number(stack[0][i].substr(3,2));
    
    digitsHash[key][mult] = stack[0][i];
    
    if (digitsHash[reciprocalKey] && digitsHash[reciprocalKey][mult]){
      console.log(stack[0][i].substr(0,3) + " * " + stack[0][i].substr(3,2)
        + ", " + digitsHash[reciprocalKey][mult].substr(0,3) + " * " 
        +  digitsHash[reciprocalKey][mult].substr(3,2));
    }
  }
}

var fives = choose([1,2,3,4,5,6,7,8,9,0],5);

for (var i=0; i<fives.length; i++){
  permute(fives[i]);
}
Windham answered 18/7, 2016 at 0:40 Comment(7)
Nice. I guess you could also build on it in case there are no zero-solutions.Gnome
@m69 I actually like your idea of sorting the 5-digit multiples better.Liberati
I was hoping to find a way to generate the permutations in order, but I can only find methods that are far too complicated to improve efficiency. And it needs more work if you want it to list all 0-solutions, because the permutations dictionary only stores one solution per product.Gnome
Btw, can you see an efficient algorithm for the prime factors idea? It looks simple, but in the end I think it boils down to checking a huge number of possibilities.Gnome
@m69 I think there might be something to the prime idea but I need to think about it more to understand. Not sure about generating the permutations in order, but sorting 30,000 items isn't too bad. Then you can binary search the closest match/es if you group them by 5-digit-sorted-combination.Liberati
My second comment was about my answer. I'm using a sparse array as an in-order dictionary, but that doesn't translate to all languages. And as it is, my code will find the best solution, but it won't necessarily find all solutions if there are several with the minimum result.Gnome
@m69 I was referring to your answer: although I didn't familiarize myself carefully with your implementation, the idea of sorting the permutations after generating all of them still seems efficient enough for finding all minimum solutions, if they are grouped by 5-digit-sorted key.Liberati
L
1

12 seconds is too much for my taste. My C++ brute force attack took ~430ms without any heuristics or deep optimizations. Anyway you need to add some heuristics for example:

Bitwidth of multiplication result is around the sum of bitwidth of the operands.

So you need to test only the same bit width combinations of the results. for example if a*b are like this:

1xx * 9x dec = 1 xxxx xxxx * 1001 xxxx bin -> 17 bit

so test only c*d combinations that lead to 17 bit results too for example

4xx * 2x dec = 100 xxxx xxxx * 10 xxxx bin -> 17 bit

to make it more clear:

dec  bin bits
 0  0000  0
 1  0001  1
 2  0010  2
 3  0011  2
 4  0100  3
 5  0101  3
 6  0110  3
 7  0111  3
 8  1000  4
 9  1001  4

If highest digit of a,b,c,d is a0,b0,c0,d0 then:

bits(a0)+bits(b0) = bits(c0)+bits(d0)

This will eliminate a lot of iterations. It is similar to subset sum problem. The speed up is from 2177280 iterations to 420480 iterations but also adds some overhead.

Loreeloreen answered 15/7, 2016 at 11:51 Comment(2)
the sum of bits is not really equal, but there can be a +1 change. If you include this you end up saving only half the iterations.Uvulitis
@RobinRoth That is if you want all the solutions but the problem need just one ...Loreeloreen
L
1

There are 126 ways to split 10 digits into 2 sets of 5 without duplicates. For each set of 5 digits, there are 120 ways (permutations) to arrange them into the form ab*cde, or 72 ways if the group contains zero and a leading zero is not allowed. That means a brute-force algorithm would need to check 126 × 120 × 72 = 1,088,640 possibilities.

However, for each pair of sets of 5 digits, if you generate the permutations in the order so that the resulting products are ordered from small to large, you can find the smallest difference of products in 120 + 72 = 192 steps (or fewer depending on how much the ranges overlap) instead of 120 × 72 = 8640. The maximum total becomes 24,192 instead of 1,088,640, which is 45 times less.
(In practice, only 12,574 product differences are calculated, and the first zero-difference result is found after 6679 steps).

You take the permutations with the smallest product for each set, calculate their difference, and store it if it is smaller than the minimum found so far. Then, you replace the permutation whose product is smallest by the next permutation in the list for that set (but stay on the same permutation for the other set) and calculate their difference, and so on, until you reach the end of one of the sets.

In the JavaScript code example below I'm using the product as the index of a sparse array (like a dictionary that can be read in order) to create an ordered list of permutations and their products (because I couldn't immediately find a simple way to generate them in order).

Array.prototype.remove = function() {      // returns first element of sparse array
    for (var key in this) {
        if (!this.hasOwnProperty(key)) return false;
        var value = this[key];
        delete this[key];
        return {prod: key, perm: value};
    }
}
function NorwegianMath() {
    var div = [1,1,1,1,1,0,0,0,0,0];          // which numbers 0-9 go in set 0 or 1
    var result, min = 99999;
    while (div[0]) {                    // keep zero in group 1 to avoid duplicates
        var set = [[],[0]];
        for (var i = 1; i < 10; i++) set[div[i]].push(i);   // distribute over sets
        var dict = [[],[]];
        for (var i = 0; i < 2; i++) {
            permute(set[i], dict[i]);         // generate permutations for each set
        }
        var x = dict[0].remove(), y = dict[1].remove();      // start with smallest
        while (x && y) {
            var diff = Math.abs(x.prod - y.prod);
            if (diff < min) {
                result = {x: x.perm, y: y.perm, d: diff};      // store new minimum
                /* if (diff == 0) return result */      // possible early exit here
                min = diff;
            }
            if (x.prod < y.prod) x = dict[0].remove();
            else y = dict[1].remove();    // replace smallest premutation with next
        }
        revLexi(div);                     // get next distribution into 2 sets of 5
    }
    return result;

    function permute(s, dict) {// there are better permutation algorithms out there
        for (var a = 0; a < 5; a++) {
            if (s[a] == 0) continue;
            for (var b = 0; b < 5; b++) {
                if (a == b) continue;
                for (var c = 0; c < 5; c++) {
                    if (a == c || b == c || s[c] == 0) continue;
                    for (var d = 0; d < 5; d++) {
                        if (a == d || b == d || c == d) continue;
                        for (var e = 0; e < 5; e++) {
                            if (a == e || b == e || c == e || d == e) continue;
                            var p = (s[a]*10 + s[b]) * (s[c]*100 + s[d]*10 + s[e]);
                            dict[p] = "" + s[a] + s[b] + "*" + s[c] + s[d] + s[e];
                        }
                    }
                }
            }
        }
    }
    function revLexi(seq) {       // next binary sequence (reverse lexicographical)
        var max = true, pos = seq.length, set = 1;
        while (pos-- && (max || !seq[pos])) if (seq[pos]) ++set; else max = false;
        if (pos < 0) return false;
        seq[pos] = 0;
        while (++pos < seq.length) seq[pos] = set-- > 0 ? 1 : 0;
        return true;
    }
}
var result = NorwegianMath();
document.write("|" + result.x + " - " + result.y + "| = " + result.d);
Larose answered 16/7, 2016 at 3:5 Comment(0)
S
0

As a heuristic you could compute the square root of 12345 (about 111) and go on from there, searching for the nearest values to 123 and 45 which you can create with the remaining numbers. I did not implement this but it could be a more intelligent approach.

Another example:

sqrt(36189) -> About 190

Remaining numbers: 24570

Try to find numbers close to 190 which you can create with these numbers. Such as 70 and 245. This should however be harder to implement.

Distance between 361 * 89 and 245 * 70: 14979

Scrumptious answered 15/7, 2016 at 10:4 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.