最接近零的两种产品之间的差异:非强力解决方案?

时间:2016-07-15 09:51:25

标签: python algorithm brute-force

science museum in Norway中,我遇到了以下数学游戏:

enter image description here

目标是将10位数字从0到9放置,使得两种产品之间的差异最接近于零。 (246是目前的最低分)。

回到家我写了下面的暴力代码:

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

如果我们不允许前导零,那么这将在大约12秒内打印出128种可能的解决方案。

但是我想知道,是否有一种算法或更好的方法以非暴力方式解决这个问题?

5 个答案:

答案 0 :(得分:2)

如果您假设存在差异为0的解决方案,则可以通过素数因子进行。

如果 b - c d = 0,那么 b和c d的素因子必须相同。你可以通过浏览所有3位数的素数(只有143个)开始搜索,看看他们是否有一个仅包含析取数字的倍数。如果是这种情况,你有两个三位数字,只需要检查2位数字。

如果找不到解决方案,请继续使用2位数的素数,并查找带有析取数字的2或3位数倍数。然后你只需要为剩下的2个数字进行排列,这样可以少得多。

答案 1 :(得分:2)

这个假设零差异是可能的(虽然它可以适应通过排序找到最小/ s - 谢谢你,m69,对于这个想法 - 每组120个排列并使用二分搜索,添加一个因子(log2 120)到时间复杂度):对三位数乘以两位数字的所有10 choose 5 * 5!组合进行多次散列,其中键是排序的五位数组合。然后,如果一个倒数键(包含其他五位数的键)指向相等的倍数,则输出匹配。共检查了30,240种组合。

JavaScript代码:

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]);
}

答案 2 :(得分:1)

12秒对我来说太过分了。我的C ++暴力攻击耗时约430毫秒,没有任何启发式或深度优化。无论如何,你需要添加一些启发式方法,例如:

乘法结果的位宽大约是操作数的位宽之和。

因此,您只需要测试结果的相同位宽组合。例如,如果a*b是这样的:

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

因此仅测试导致17位结果的c*d组合,例如

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

使其更清晰:

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

如果a,b,c,d的最高位为a0,b0,c0,d0,则:

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

这将消除大量迭代。它类似于子集和问题。加速从2177280次迭代到420480次迭代,但也增加了一些开销。

答案 3 :(得分:1)

有126种方法可以将10个数字分成2组,每组5个,没有重复。对于每组5个数字,有120种方式(排列)将它们排列成ab*cde形式,或者如果组包含0并且不允许前导零,则有72种方式。这意味着蛮力算法需要检查126×120×72 = 1,088,640种可能性。

但是,对于每对5位数组,如果您按顺序生成排列以便生成的产品从小到大排序,您可以在120 + 72 = 192步中找到最小的产品差异(或者更少,取决于范围重叠的数量而不是120×72 = 8640.最大总数变为24,192而不是1,088,640,这是少45倍。
(实际上,只计算了12,574个产品差异,并且在6679步之后找到了第一个零差异结果。)

您为每组采用最小乘积的排列,计算它们的差异,并在它小于目前为止找到的最小值时存储它。然后,用该组列表中的下一个排列替换产品最小的排列(但保持另一组的相同排列)并计算它们的差异,依此类推,直到达到其中一个的结尾为止。集。

在下面的JavaScript代码示例中,我使用该产品作为稀疏数组的索引(如可以按顺序读取的字典),以创建排列及其产品的有序列表(因为我无法立即找到一种简单的方法来按顺序生成它们。

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);

答案 4 :(得分:0)

作为启发式,您可以计算12345(约111)的平方根并从那里继续,搜索最近的值123和45,您可以使用剩余的数字创建。我没有实现这个,但它可能是一种更聪明的方法。

另一个例子:

sqrt(36189) - &gt;大约190

剩余数字:24570

尝试找到接近190的数字,您可以使用这些数字创建这些数字。例如70和245.但这应该更难实施。

361 * 89和245 * 70之间的距离:14979