【问题标题】:Subtract a number's digits from the number until it reaches 0从数字中减去一个数字的数字,直到它达到 0
【发布时间】:2014-12-29 22:23:41
【问题描述】:

谁能帮我解决这个问题的一些算法?

我们有一个大数字(19 位),在一个循环中,我们从数字本身中减去该数字的一位数字。

我们继续这样做,直到数字达到零。我们要计算使给定数字达到零的最小减法次数。

算法必须在两秒内快速响应 19 位数字 (10^19)。例如,提供36 的输入将给出7

1. 36 - 6 = 30
2. 30 - 3 = 27
3. 27 - 7 = 20
4. 20 - 2 = 18
5. 18 - 8 = 10
6. 10 - 1 =  9
7.  9 - 9 =  0

谢谢。

【问题讨论】:

  • 向我们展示您的尝试。给我们看一些代码。
  • 我认为您只能通过尝试所有可能的组合来强制执行此操作;我看不出有任何方法可以及早“预测”选择一个数字而不是另一个数字是否会带来更好的结果。 (尽管对于您给定的示例,似乎总是首先选择最大数字,但我怀疑这会在每种可能的情况下导致理想的解决方案。)顺便说一句,您需要这个做什么 - 这是一些什么样的学校运动?我怀疑这有任何实际应用......
  • 我不知道如何解决这个问题,除了蛮力。是的,这是一个算法设计练习。
  • 我不知道它是否有帮助,但任何数字(除了 1 位数字)都会出现在这个系列中: 10 , 9 ,0 比如:36 -> 30 ..... > 10 , 9 , 0 或 178 -> 170 ..... > 10 , 9 , 0
  • 有人在 codeforces 上发现了类似的问题:link 我不是故意在比赛中作弊,我只是需要它来做作业,我也在这里发布了这个问题:link跨度>

标签: algorithm bignum


【解决方案1】:

最小减法次数达到零,我怀疑这是一个非常棘手的问题,需要大量回溯潜在解决方案,这对于您的时间限制而言可能过于昂贵.

但是您应该做的第一件事是进行健全性检查。由于最大数字是 9,因此 19 位数字将需要大约 10<sup>18</sup> 减法才能达到零。编写一个简单的程序,从10<sup>19</sup> 中连续减去 9,直到小于 10。如果你不能在两秒内做到,那你就有麻烦了。

例如,以下程序(a)

#include <stdio.h>

int main (int argc, char *argv[]) {
    unsigned long long x = strtoull(argv[1], NULL, 10);
    x /= 1000000000;
    while (x > 9)
        x -= 9;
    return x;
}

当使用参数 10000000000000000000 (10<sup>19</sup>) 运行时,即使在 gcc 疯狂的优化级别 -O3 下,也需要一秒半的时钟时间(以及 CPU 时间,因为这都是计算):

real    0m1.531s
user    0m1.528s
sys     0m0.000s

这是在 while 循环之前的十亿除数,这意味着全部迭代次数大约需要 48 年。

因此,蛮力方法在这里无济于事,您需要进行一些严肃的数学分析,这可能意味着您应该在https://math.stackexchange.com/ 上发布类似的问题,让数学天才试一试。


(a) 如果你想知道为什么我从用户那里获取值而不是使用 10000000000000000000ULL 的常量,这是为了防止 gcc 在编译时计算它并把它变成这样的东西:

mov  $1, %eax

return x 同上,这将防止它注意到我没有使用x 的最终值,因此完全优化循环不存在。

【讨论】:

  • 谢谢。除了那个时间限制,我也有 64mb RAM 限制,所以我不认为回溯是完美的答案。我可以通过将数字除以 9 (num/9 + 1) 来计算我应该从 9 中减去该数字多少次,但这有帮助吗?
  • @RHB,是的,这将为您提供减法次数的下限。或者你可以除以5 给出一个粗略的估计,假设你平均减去 ,错误,平均值,我猜。但是,可以肯定的是,您需要实际测试它,根据我的更新,这无法在合理的时间范围内完成(除非您的计算机比我的计算机快 10 亿倍)。
  • 一定是数学题,我应该试试“数学天才”,谢谢:)
【解决方案2】:

我没有可以在 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 上的运行时是:

  • 贪心:3 秒
  • 动态:36 秒

另一方面,动态规划方法在计算所有值时显然要快得多,因为它的增量方法无论如何都需要它们作为中间结果。用于计算从 10 到 1,000,000 的所有值:

  • 贪心:19 分钟
  • 动态:0.03 秒

如上面的运行时所示,贪心算法在 2 秒的目标运行时内获得高达 9 位的输入值。实现并没有真正调整,当然可以挤出更多时间,但这将是部分改进。

想法

正如在另一个答案中已经探讨的那样,通过逐位减去数字,不可能在 2 秒内获得 19 位数字的结果。由于我们在每一步中最多减去 9,因此为 10^19 的值完成此操作需要超过 10^18 步。我们主要使用大约 10^9 次操作/秒的计算机,这意味着大约需要 10^9 秒。

因此,我们需要一些可以走捷径的东西。我可以想到可能的场景,但到目前为止还无法将其概括为完整的策略。

例如,如果您的当前值为 9999,则您知道可以减去 9 直到达到 9000。因此您可以计算出您将执行 112 步 ((9999 - 9000) / 9 + 1) 减去 9 ,这可以在几个操作中完成。

【讨论】:

    【解决方案3】:

    正如 cmets 已经说过的,并且同意 @paxdiablo 的其他答案,我不确定是否有一种算法可以在没有回溯的情况下找到理想的解决方案;而且数量的大小和时间限制也可能很困难。

    但一般考虑:您可能希望找到一种方法来决定始终减去最高位(显然,这将尽可能减少您当前的数字)和查看您当前的数字并减去哪个这些会给你最大的“新”数字。

    假设您当前的号码仅包含 05 之间的数字 - 那么您可能会想减去 5 以将您的号码减少可能的最大值,然后继续下一步。但是,如果您当前号码的最后一位数字是 3,那么您可能需要减去 4,因为这会给您以 9 作为数字末尾的新数字,而不是“仅”@987654327 @如果你减去5,你会得到。

    而如果您的数字中已经有2两个 9,并且最后一个数字是1,那么您可能还是想减去9,因为您将在结果中留下第二个9(至少在大多数情况下;在某些极端情况下,它也可能会从结果中消失),因此减去2 不会有给出的优势你是一个“高”9,否则你将不会在下一步中拥有它,并且缺点是不会像减去 9 那样将你的数字降低...

    但是,您减去的每个数字不仅会直接影响下一步,还会间接影响后续步骤 - 所以我再次怀疑是否有一种方法可以始终为当前步骤选择理想的数字,而无需任何回溯或类似措施。

    【讨论】:

    • 我用现实生活中的程序做了一些计算,似乎表明即使是 最好的 没有回溯的情况仍需要大约半个世纪 :-) 不是说你的观点是无效(它们是),但我认为没有数学分析是可行的。
    • @paxdiablo:问题是,你真的需要对整数数进行实际数学运算吗?在大多数情况下,仅减去一位数字只会影响当前数字的最后一位或两位数字(边缘情况,例如从 10000…0 中减去 1)——因此将数字的所有数字保留在一个数组中(而不是使用实际的数值数据类型),然后只“从右侧”操作数组元素,只需要尽可能多的数字,就像在纸上执行减法运算一样,可能会更快。
    • 好点。但是,我怀疑基于数组的解决方案会更慢。暂时忽略边缘情况,无论如何它们都不太可能发生,但是现在每个减法运算基本上都会影响一位或两位数,所以现在您可以在相同数量或两倍数量的减法之间达到零。它不会是ULL 数据类型,因此可能会提高一些速度,但是,即使我们假设这些减法的最佳情况是快十倍(不太可能)并且所有减法只影响一位数,那仍然是五年左右。我怀疑数学极客将不得不处理这个:-)
    • 我也这么想,一定是数学公式什么的,或者我们只需要对数字的一小部分做一些工作,同样需要一点计算(比如除法)。这个问题来自我的一个练习,TA 自己在之前(2 秒内)解决了这个问题,因为他给了我一些输入和输出,例如:in:2984719979875 out: 347562320055 , in:2654179425975195出:304516201082766,入:987631579787564399出:112128174907159069
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2016-08-18
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2022-11-13
    • 2017-10-21
    • 1970-01-01
    相关资源
    最近更新 更多