我提出的算法不会遍历所有数字排列。它会尽可能快地消除可能性,以便只有一小部分排列实际上会被测试。
通过示例解释算法
以下是基于数字125460的示例说明其工作原理。如果您可以直接阅读代码,则可以跳过此(长)部分:
首先,两个“牙齿”(即吸血鬼因子)显然是未知的,可以这样表示问题:
?**
X ?**
-------
=125460
对于第一个因数最左边的数字(用
?
标记),我们可以选择数字0、1、2、5、4或6中的任意一个。但仔细分析后,0不是可行的可能性,因为乘积永远不会超过5位数。因此,浪费时间去遍历以0开头的所有数字排列。
对于第二个因数最左边的数字(同样用
?
标记),情况也是如此。但是,在查看组合时,我们可以再次筛选出一些无法达到目标乘积的数字对。例如,应该丢弃这个组合:
1**
X 2**
-------
=125460
这些数字可以组成的最大数是199x299 = 59501(忽略我们甚至没有9的事实),这还不到所需数字的一半。因此,我们应该拒绝组合(1,2)。出于同样的原因,对于这些位置,可以丢弃对(1,5)的选择。同样,对于产生太大的乘积(>= 200000)的对(4,5)、(4,6)和(5,6),也应该予以拒绝。我将称这种测试——确定目标数字是否在某个选定数字对的范围内——为“范围测试”。
在此阶段,第一方和第二方没有区别,因此我们也不必调查第二个数字小于第一个数字的情况,因为它们反映了已经被调查(或拒绝)的一对。
因此,在所有可能占据这个第一位置的对中(从6个数字的集合中取2个数字有30种可能性),只需要调查以下4个:
(1, 6), (2, 4), (2, 5), (2, 6)
在更详细的符号表示中,这意味着我们将搜索限制在这些数字模式中:
1** 2** 2** 2**
X 6** X 4** X 5** X 6**
------- ------- ------- -------
=125460 =125460 =125460 =125460
A B C D
很明显,即使在考虑其他位置之前就减少了这些可能性,在搜索树上也会大大减少。
该算法将按顺序检查这4个可能性,并针对每个可能性检查下一个数字位置的可行性。因此,首先分析A配置:
1?*
X 6?*
-------
=125460
可用于标记为
?
的位置的选项有以下12种:
(0, 2), (0, 4), (0, 5)
(2, 0), (2, 4), (2, 5)
(4, 0), (4, 2), (4, 5)
(5, 0), (5, 2), (5, 4)
再次,我们可以通过应用范围测试来消除一些数字对。例如,取数字对(5, 4)。这意味着我们有因子15*和64*(其中*是一个未知数字)。这两个数的乘积将在159*和649中达到最大值,即103191(忽略我们甚至没有一个9可用的事实):这对于达到目标太低了,因此可以忽略这一对。通过进一步应用范围测试,所有这12对数字都可以被丢弃,因此在配置A内搜索停止:那里没有解决方案。
然后算法移动到配置B:
2?*
X 4?*
-------
=125460
再次,对于第二位可能的数字对进行取值范围测试,同样发现没有任何数字对通过测试:例如 (5, 6) 永远不能代表大于259 * 469 = 121471的积,这个值(勉强)还是太小了。
然后算法转向选项C:
2?*
X 5?*
-------
=125460
在所有12个可能的组合中,只有以下几个通过了范围测试:(4, 0),(4, 1),(6, 0),(6, 1)。现在我们有了以下第二级配置:
24* 24* 26* 26*
X 50* X 51* X 50* X 51*
------- ------- ------- -------
=125460 =125460 =125460 =125460
Ca Cb Cc Cd
在配置Ca中,没有通过范围测试的配对。
在配置Cb中,配对(6,0)经过测试,导致了一个解决方案:
246
X 510
-------
=125460
在这一点上,算法停止搜索。结果是清晰的。总的来说,与暴力排列检查算法相比,所查看的配置数量非常少。以下是搜索树的可视化:
*-+-- (1, 6)
|
+-- (2, 4)
|
+-- (2, 5) -+-- (4, 0)
| |
| +-- (4, 1)
/ /
| +-- (6, 0)
| |
| +-- (6, 1)
|
+-- (2, 6)
下面
/
后的变量仅用于显示算法在没有找到解决方案时会执行的操作。但在实际情况中,搜索树的这部分实际上从未被追随。
我对时间复杂度没有清楚的想法,但似乎对于更大的数字运行得很好,这表明在早期阶段消除数字使搜索树的宽度相当窄。
这是一个实时的JavaScript实现,激活时还运行一些测试用例(并且它有一些其他优化--请参见代码注释)。
function vampireFangs(vampire) {
function recurse(vampire, prevA, prevB, counts, divider) {
if (divider < 1) {
return prevA * prevB === vampire && (prevA % 10 + prevB % 10 > 0)
? [prevA, prevB]
: false;
}
var v = Math.floor(vampire/divider);
prevA *= 10;
prevB *= 10;
var minDigA = Math.floor(v / (prevB + 10)) - prevA;
var maxDigA = prevB ? Math.floor((v + 1) / prevB) - prevA : 9;
if (maxDigA > 9) maxDigA = 9;
for (var digA = minDigA; digA <= maxDigA; digA++) {
if (!counts[digA]) continue;
var fangA = prevA + digA;
counts[digA]--;
var minDigB = Math.floor(v / (fangA + 1)) - prevB;
var maxDigB = fangA ? (v + 1) / fangA - prevB : 9;
if (prevA === prevB && digA > minDigB) minDigB = digA;
if (maxDigB > 9) maxDigB = 9;
for (var digB = minDigB; digB <= Math.min(maxDigB, 9); digB++) {
if (!counts[digB]) continue;
var fangB = prevB + digB;
counts[digB]--;
var result = recurse(vampire, fangA, fangB, counts, divider / 100);
if (result) return result;
counts[digB]++;
}
counts[digA]++;
}
}
if (typeof vampire !== 'number') return false;
if (vampire < 0 || vampire % 1 !== 0) return false;
if (vampire > 9007199254740991) return null;
var digits = vampire.toString(10).split('').map(Number);
if (!digits.length || digits.length % 2 > 0) return false;
var counts = [0,0,0,0,0,0,0,0,0,0];
for (var i = 0; i < digits.length; i++) {
counts[digits[i]]++;
}
return recurse(vampire, 0, 0, counts, Math.pow(10, digits.length - 2));
}
function Timer() {
function now() {
return performance ? performance.now() : new Date().getTime();
}
var start = now();
this.spent = function () { return Math.round(now() - start); }
}
var button = document.querySelector('button');
var input = document.querySelector('input');
var output = document.querySelector('pre');
button.onclick = function () {
var str = input.value;
var vampire = parseInt(str);
var timer = new Timer();
var result = vampire.toString(10) !== str ? null
: vampireFangs(vampire);
output.textContent = (result
? 'Vampire number. Fangs are: ' + result.join(', ')
: result === null
? 'Input is not an integer or too large for JavaScript'
: 'Not a vampire number')
+ '\nTime spent: ' + timer.spent() + 'ms';
}
var tests = [
[1, 999, 126000, 1023],
[1260, 1395, 1435, 1530, 1827, 2187, 6880,
102510, 104260, 105210, 105264, 105750, 108135,
110758, 115672, 116725, 117067, 118440,
120600, 123354, 124483, 125248, 125433, 125460, 125500,
13078260,
16758243290880,
24959017348650]
];
tests.forEach(function (vampires, shouldBeVampire) {
vampires.forEach(function (vampire) {
var isVampire = vampireFangs(vampire);
if (!isVampire !== !shouldBeVampire) {
output.textContent = 'Unexpected: vampireFangs('
+ vampire + ') returns ' + JSON.stringify(isVampire);
throw 'Test failed';
}
});
});
output.textContent = 'All tests passed.';
N: <input value="1047527295416280"><button>Vampire Check</button>
<pre></pre>
由于 JavaScript 使用 64 位浮点数表示,因此上面的代码片段只接受最多为 253-1 的两个数字。超过该限制会导致精度丢失,从而得到不可靠的结果。
由于 Python 没有这样的限制,我还在 eval.in 上放了一个 Python 实现。该网站对执行时间有限制,如果出现问题,您需要在其他地方运行它。