我没有可以在 2 秒内解决 19 位数字的解决方案。差远了。但我确实实现了一些算法(包括求解最优解的动态规划算法),并获得了一些我认为很有趣的见解。
贪心算法
作为基线,我实现了一个贪心算法,它只是在每一步中选择最大的数字:
uint64_t countGreedy(uint64_t inputVal) {
uint64_t remVal = inputVal;
uint64_t nStep = 0;
while (remVal > 0) {
uint64_t digitVal = remVal;
uint_fast8_t maxDigit = 0;
while (digitVal > 0) {
uint64_t nextDigitVal = digitVal / 10;
uint_fast8_t digit = digitVal - nextDigitVal * 10;
if (digit > maxDigit) {
maxDigit = digit;
}
digitVal = nextDigitVal;
}
remVal -= maxDigit;
++nStep;
}
return nStep;
}
动态规划算法
这样做的想法是我们可以逐步计算最优值。对于给定的值,我们选择一个数字,这会将减去该数字的值的最佳步数增加一个步数。
对于名为optSteps(val) 的给定值的目标函数(最佳步数)和名为d_i 的值的数字,以下关系成立:
optSteps(val) = 1 + min(optSteps(val - d_i))
这可以通过动态规划算法来实现。由于d_i 最多为 9,我们只需要前面的 9 个值来构建。在我的实现中,我保留了一个包含 10 个值的循环缓冲区:
static uint64_t countDynamic(uint64_t inputVal) {
uint64_t minSteps[10] = {1, 1, 1, 1, 1, 1, 1, 1, 1, 1};
uint_fast8_t digit0 = 0;
for (uint64_t val = 10; val <= inputVal; ++val) {
digit0 = val % 10;
uint64_t digitVal = val;
uint64_t minPrevStep = 0;
bool prevStepSet = false;
while (digitVal > 0) {
uint64_t nextDigitVal = digitVal / 10;
uint_fast8_t digit = digitVal - nextDigitVal * 10;
if (digit > 0) {
uint64_t prevStep = 0;
if (digit > digit0) {
prevStep = minSteps[10 + digit0 - digit];
} else {
prevStep = minSteps[digit0 - digit];
}
if (!prevStepSet || prevStep < minPrevStep) {
minPrevStep = prevStep;
prevStepSet = true;
}
}
digitVal = nextDigitVal;
}
minSteps[digit0] = minPrevStep + 1;
}
return minSteps[digit0];
}
结果比较
这可能被认为是一个惊喜:我对所有高达 1,000,000 的值都运行了这两种算法。结果完全相同。这表明贪心算法实际上计算了最优值。
我没有正式的证据证明这确实适用于所有可能的值。这在直觉上对我来说很有意义。如果在任何给定步骤中,您选择了一个小于最大值的数字,那么您会为了进入一个更有利的情况而牺牲即时进度,从而让您赶上并通过贪婪方法。但在我考虑过的所有场景中,采取次优步骤后的情况并没有变得更加有利。它可能会使下一步更大,但这最多足以再次平衡。
复杂性
虽然这两种算法在值的大小上看起来都是线性的,但它们也会遍历值中的所有数字。由于位数对应于log(n),所以我认为复杂度是O(n * log(n))。
我认为可以通过对每个数字的频率进行计数并逐步修改它们来使其成为线性。但我怀疑它实际上会更快。它需要更多逻辑,并将值中所有数字的循环(我们正在查看的值在 2-19 的范围内)转换为 10 个可能数字的固定循环。
运行时
毫不奇怪,贪心算法计算单个值的速度更快。例如,对于值 1,000,000,000,我的 MacBook Pro 上的运行时是:
另一方面,动态规划方法在计算所有值时显然要快得多,因为它的增量方法无论如何都需要它们作为中间结果。用于计算从 10 到 1,000,000 的所有值:
如上面的运行时所示,贪心算法在 2 秒的目标运行时内获得高达 9 位的输入值。实现并没有真正调整,当然可以挤出更多时间,但这将是部分改进。
想法
正如在另一个答案中已经探讨的那样,通过逐位减去数字,不可能在 2 秒内获得 19 位数字的结果。由于我们在每一步中最多减去 9,因此为 10^19 的值完成此操作需要超过 10^18 步。我们主要使用大约 10^9 次操作/秒的计算机,这意味着大约需要 10^9 秒。
因此,我们需要一些可以走捷径的东西。我可以想到可能的场景,但到目前为止还无法将其概括为完整的策略。
例如,如果您的当前值为 9999,则您知道可以减去 9 直到达到 9000。因此您可以计算出您将执行 112 步 ((9999 - 9000) / 9 + 1) 减去 9 ,这可以在几个操作中完成。