我在这里提出的算法不会遍历所有的数字排列。它将尽可能快地消除可能性,以便实际测试一小部分排列。
算法举例说明
这是基于示例编号 125460 的工作原理。如果您可以直接阅读代码,那么您可以跳过这个(长)部分:
起初这两个尖牙(即吸血鬼因素)显然是未知的,问题可以表示如下:
?**
X ?**
-------
=125460
对于第一个因素的最左边的数字(标有?),我们可以选择任何数字 0、1、2、5、4 或 6。但仔细分析,0 是不可行的,因为产品永远不会超过 5 位数字。因此,遍历所有以零开头的数字排列是浪费时间。
对于第二个因素的最左边的数字(也标有?),同样如此。但是,在查看组合时,我们可以再次过滤掉一些对达到目标产品没有帮助的配对。例如,应该丢弃这种组合:
1**
X 2**
-------
=125460
使用这些数字可以达到的最大数字是 199x299 = 59501(忽略我们甚至没有 9 的事实),这甚至不是所需数字的一半。所以我们应该拒绝组合 (1, 2)。出于同样的原因,可以丢弃 (1, 5) 对以占据这些位置。类似地,对 (4, 5)、(4, 6) 和 (5, 6) 也可以被拒绝,因为它们产生的乘积太大 (>= 200000)。我将这种测试称为“范围测试”,即确定目标数字是否在某个选定数字对的范围内。
在这个阶段,第一和第二牙之间没有区别,所以我们也不应该研究第二个数字小于第一个数字的对,因为它们反映了已经研究过的对(或拒绝)。
所以在可能占据第一个位置的所有可能对中(有 30 种可能性从一组 6 个数字中取 2 个数字),只有以下 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) = success: 246 * 510
/ /
| +-- (6, 0)
| |
| +-- (6, 1)
|
+-- (2, 6) ---- (0, 1) ---- (4, 5) = success: 204 * 615
/ 下面的变体仅用于显示如果没有找到解决方案,算法还会做什么。但在这个实际案例中,搜索树的那部分实际上从未被遵循。
我不清楚时间复杂度,但对于较大的数字,它似乎运行得相当好,这表明在早期阶段消除数字会使搜索树的宽度变得很窄。
这是一个实时的 JavaScript 实现,它在被激活时也会运行一些测试用例(并且它还有一些其他优化 - 请参阅代码 cmets)。
/*
Function: vampireFangs
Arguments:
vampire: number to factorise into two fangs, if possible.
Return value:
Array with two fangs if indeed the argument is a vampire number.
Otherwise false (not a vampire number) or null (argument too large to
compute)
*/
function vampireFangs(vampire) {
/* Function recurse: for the recursive part of the algorithm.
prevA, prevB: partial, potential fangs based on left-most digits of the given
number
counts: array of ten numbers representing the occurrence of still
available digits
divider: power of 100, is divided by 100 each next level in the search tree.
Determines the number of right-most digits of the given number that
are ignored at first in the algorithm. They will be considered in
deeper levels of recursion.
*/
function recurse(vampire, prevA, prevB, counts, divider) {
if (divider < 1) { // end of recursion
// Product of fangs must equal original number and fangs must not both
// end with a 0.
return prevA * prevB === vampire && (prevA % 10 + prevB % 10 > 0)
? [prevA, prevB] // Solution found
: false; // It's not a solution
}
// Get left-most digits (multiple of 2) of potential vampire number
var v = Math.floor(vampire/divider);
// Shift decimal digits of partial fangs to the left to make room for
// the next digits
prevA *= 10;
prevB *= 10;
// Calculate the min/max A digit that can potentially contribute to a
// solution
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; // this digit is not available
var fangA = prevA + digA;
counts[digA]--;
// Calculate the min/max B digit that can potentially contribute to
// a solution
var minDigB = Math.floor(v / (fangA + 1)) - prevB;
var maxDigB = fangA ? (v + 1) / fangA - prevB : 9;
// Don't search mirrored A-B digits when both fangs are equal until now.
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; // this digit is not available
var fangB = prevB + digB;
counts[digB]--;
// Recurse by considering the next two digits of the potential
// vampire number, for finding the next digits to append to
// both partial fangs.
var result = recurse(vampire, fangA, fangB, counts, divider / 100);
// When one solution is found: stop searching & exit search tree.
if (result) return result; // solution found
// Restore counts
counts[digB]++;
}
counts[digA]++;
}
}
// Validate argument
if (typeof vampire !== 'number') return false;
if (vampire < 0 || vampire % 1 !== 0) return false; // not positive and integer
if (vampire > 9007199254740991) return null; // beyond JavaScript precision
var digits = vampire.toString(10).split('').map(Number);
// A vampire number has an even number of digits
if (!digits.length || digits.length % 2 > 0) return false;
// Register per digit (0..9) the frequency of that digit in the argument
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() { // try performance object, else use Date
return performance ? performance.now() : new Date().getTime();
}
var start = now();
this.spent = function () { return Math.round(now() - start); }
}
// I/O
var button = document.querySelector('button');
var input = document.querySelector('input');
var output = document.querySelector('pre');
button.onclick = function () {
var str = input.value;
// Convert to number
var vampire = parseInt(str);
// Measure performance
var timer = new Timer();
// Input must be valid number
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';
}
// Tests (numbers taken from wiki page)
var tests = [
// Negative test cases:
[1, 999, 126000, 1023],
// Positive test cases:
[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 位浮点表示,上述 sn-p 只接受不超过 253-1 的数字。超过该限制会导致精度下降,从而导致结果不可靠。
由于 Python 没有这样的限制,我也在eval.in 上放了一个 Python 实现。该站点对执行时间有限制,因此如果出现问题,您必须在其他地方运行它。