在science museum in Norway中,我遇到了以下数学游戏:
目标是将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种可能的解决方案。
但是我想知道,是否有一种算法或更好的方法以非暴力方式解决这个问题?
答案 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