关于python:SPOJ`CUBERT`中的错误答案

Wrong answer in SPOJ `CUBERT`

对于我在SPOJ上解决这个问题的方法,我得到了一个错误的答案。

这个问题要求计算一个整数的立方根(最长可达150位),并输出截断到10位小数的答案。它还要求计算作为"校验和"值的应答模块10中所有数字的总和。

以下是确切的问题陈述:

Your task is to calculate the cube root of a given positive integer.
We can not remember why exactly we need this, but it has something in
common with a princess, a young peasant, kissing and half of a kingdom
(a huge one, we can assure you).

Write a program to solve this crucial task.

Input

The input starts with a line containing a single integer t <= 20, the number of test cases. t test cases follow.

The next lines consist of large positive integers of up to 150 decimal
digits. Each number is on its own separate line of the input file. The
input file may contain empty lines. Numbers can be preceded or
followed by whitespaces but no line exceeds 255 characters.

Output

For each number in the input file your program should output a line
consisting of two values separated by single space. The second value
is the cube root of the given number, truncated (not rounded!) after
the 10th decimal place. First value is a checksum of all printed
digits of the cube root, calculated as the sum of the printed digits
modulo 10.

Example

Input:
5
1

8

1000

2 33076161

Output:
1 1.0000000000
2 2.0000000000
1 10.0000000000
0 1.2599210498
6 321.0000000000

这是我的解决方案:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
from math import pow


def foo(num):
    num_cube_root = pow(num, 1.0 / 3)
    # First round upto 11 decimal places
    num_cube_root ="%.11f" % (num_cube_root)
    # Then remove the last decimal digit
    # to achieve a truncation of 10 decimal places
    num_cube_root = str(num_cube_root)[0:-1]

    num_cube_root_sum = 0
    for digit in num_cube_root:
        if digit != '.':
            num_cube_root_sum += int(digit)
    num_cube_root_sum %= 10

    return (num_cube_root_sum, num_cube_root)


def main():
    # Number of test cases
    t = int(input())
    while t:
        t -= 1
        num = input().strip()
        # If line empty, ignore
        if not num:
            t += 1
            continue

        num = int(num)
        ans = foo(num)
        print(str(ans[0]) +"" + ans[1])


if __name__ == '__main__':
    main()

它非常适合示例案例:实时演示。

有人能说出这个解决方案的问题是什么吗?


您的解决方案有两个问题,都与浮点运算的使用有关。第一个问题是,python float的精度只有大约16位有效的十进制数字,所以只要您的答案需要16位以上的有效数字(所以在该点之前超过6位,之后超过10位),您就很少希望得到正确的尾随数字。第二个问题更加微妙,甚至影响到n的小值。这就是说,将小数四舍五入到11位,然后再将最后一位舍入的方法会因为两次舍入而遭受潜在的错误。例如,以n = 33为例。n的立方根到小数点后20位左右是:好的。

1
3.20753432999582648755...

当这一点被四舍五入到11位时,你最终会得到好的。

1
3.20753433000

现在去掉最后一个数字就得到了3.2075343300,这不是你想要的。问题是,小数点后四舍五入到11位会影响到第11位数字左边的数字。好的。

那么你能做些什么来解决这个问题呢?好吧,您可以完全避免浮点运算,并将其简化为纯整数问题。我们需要整数n的立方根到小数点后10位(四舍五入到最后一位)。这相当于计算10**30 * n的立方根到最接近的整数,再次取整,然后除以10**10。所以这里的基本任务是计算任意给定整数n的立方根的底面。我找不到任何关于计算整数多维数据集根的现有堆栈溢出答案(在python中仍然比较少),所以我认为应该详细说明如何做。好的。

计算整数的立方根变得相当容易(借助于一点点数学知识)。有各种可能的方法,但一种既有效又易于实现的方法是使用纯整数版本的牛顿-拉斐逊方法。在实数上,牛顿求解方程x**3 = n的方法将x近似为n的立方根,并迭代以返回改进的近似。所需迭代为:好的。

1
x_next = (2*x + n/x**2)/3

在实际情况下,您将重复迭代,直到达到所需的公差。事实证明,在整数上,基本上相同的迭代是可行的,并且在正确的退出条件下,它将给出正确的答案(不需要公差)。整数情况下的迭代为:好的。

1
a_next = (2*a + n//a**2)//3

(注意使用楼层划分操作符//代替通常的真实划分操作符/)在数学上,a_next正好是(2*a + n/a**2)/3的楼层。好的。

下面是一些基于此迭代的代码:好的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
def icbrt_v1(n, initial_guess=None):
   """
    Given a positive integer n, find the floor of the cube root of n.

    Args:
        n : positive integer
        initial_guess : positive integer, optional. If given, this is an
            initial guess for the floor of the cube root. It must be greater
            than or equal to floor(cube_root(n)).

    Returns:
        The floor of the cube root of n, as an integer.
   """

    a = initial_guess if initial_guess is not None else n
    while True:
        d = n//a**2
        if a <= d:
            return a
        a = (2*a + d)//3

一些示例使用:好的。

1
2
3
4
5
6
7
8
9
>>> icbrt_v1(100)
4
>>> icbrt_v1(1000000000)
1000
>>> large_int = 31415926535897932384626433
>>> icbrt_v1(large_int**3)
31415926535897932384626433
>>> icbrt_v1(large_int**3-1)
31415926535897932384626432

icbrt_v1中有一些烦人和低效的地方,我们很快就会解决。但首先,简要解释上述代码的工作原理。注意,我们从一个假设大于或等于立方根地板的初始猜测开始。我们将证明这个属性是循环不变量:每次我们到达while循环的顶部时,a至少是floor(cbrt(n))。而且,每次迭代都会产生一个严格小于旧的a的值,因此我们的迭代最终会收敛到floor(cbrt(n))的值。为了证明这些事实,请注意,当我们进入while循环时,有两种可能性:好的。

案例1。a严格大于n的立方根。然后是a > n//a**2,代码进入下一个迭代。写a_next = (2*a + n//a**2)//3,然后我们有:好的。

  • a_next >= floor(cbrt(n))。这是因为(2*a + n/a**2)/3至少是n的立方根,而这个立方根又是aan/a**2的AM-GM不等式的结果:这三个量的几何平均数正好是n的立方根,所以算术平均数必须至少是EDOCX1的立方根。1〔7〕。所以我们的循环不变量被保留下来,以便下一次迭代。好的。

  • a_next < a:由于我们假设a大于立方根,n/a**2 < a,因此(2a + n/a**2) / 3小于a,因此floor((2a + n/a**2) / 3) < a。这保证了我们在每次迭代中都朝着解决方案前进。好的。

案例2。a小于或等于n的立方根。然后是a <= floor(cbrt(n)),但根据上面建立的循环不变量,我们也知道a >= floor(cbrt(n))。所以我们结束了:a是我们追求的价值。而while循环则在此时退出,因为a <= n // a**2。好的。

上面的代码有几个问题。首先,从最初对n的猜测开始是无效的:代码将花费它的前几次迭代(大致)将a的当前值除以3,直到进入解决方案的邻域。对于最初的猜测(以及在python中很容易计算的猜测),更好的选择是使用超过n的立方根的二的第一次幂。好的。

1
initial_guess = 1 << -(-n.bit_length() // 3)

更妙的是,如果n足够小以避免溢出,则使用浮点运算提供初始猜测,其中包括:好的。

1
initial_guess = int(round(n ** (1/3.)))

但这又带来了我们的第二个问题:算法的正确性要求初始猜测不小于实际的整数立方根,并且随着n变大,我们不能保证对于上面基于浮点的initial_guess(尽管对于足够小的n我们可以)。幸运的是,有一个非常简单的修复方法:对于任何正整数a,如果我们执行一次迭代,我们总是得到一个至少为floor(cbrt(a))的值(使用我们上面使用的相同am-gm参数)。所以我们要做的就是在开始测试收敛性之前至少执行一次迭代。好的。

有鉴于此,下面是上述代码的更有效版本:好的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
def icbrt(n):
   """
    Given a positive integer n, find the floor of the cube root of n.

    Args:
        n : positive integer

    Returns:
        The floor of the cube root of n, as an integer.
   """

    if n.bit_length() < 1024:  # float(n) safe from overflow
        a = int(round(n**(1/3.)))
        a = (2*a + n//a**2)//3  # Ensure a >= floor(cbrt(n)).
    else:
        a = 1 << -(-n.bit_length()//3)

    while True:
        d = n//a**2
        if a <= d:
            return a
        a = (2*a + d)//3

有了icbrt,很容易把所有的东西放在一起,计算出10位小数的立方根。这里,为了简单起见,我将结果输出为一个字符串,但您也可以轻松地构造一个Decimal实例。好的。

1
2
3
4
5
6
7
8
9
def cbrt_to_ten_places(n):
   """
    Compute the cube root of `n`, truncated to ten decimal places.

    Returns the answer as a string.
   """

    a = icbrt(n * 10**30)
    q, r = divmod(a, 10**10)
    return"{}.{:010d}".format(q, r)

示例输出:好的。

1
2
3
4
5
6
7
8
>>> cbrt_to_ten_places(2)
'1.2599210498'
>>> cbrt_to_ten_places(8)
'2.0000000000'
>>> cbrt_to_ten_places(31415926535897932384626433)
'315536756.9301821867'
>>> cbrt_to_ten_places(31415926535897932384626433**3)
'31415926535897932384626433.0000000000'

。好啊。


您可以尝试使用具有足够大精度值的decimal模块。

编辑:多亏了@dsm,我意识到decimal模块不会产生非常精确的立方根。我建议您检查所有数字是否都是9,如果是这样,请将其四舍五入为整数。

另外,我现在也用小数进行1/3除法,因为将1/3的结果传递给十进制构造函数会降低精度。

1
2
3
4
5
6
7
8
9
10
11
12
13
import decimal

def cbrt(n):
    nd = decimal.Decimal(n)
    with decimal.localcontext() as ctx:
        ctx.prec = 50
        i = nd ** (decimal.Decimal(1) / decimal.Decimal(3))
    return i

ret = str(cbrt(1233412412430519230351035712112421123121111))
print(ret)
left, right = ret.split('.')
print(left + '.' + ''.join(right[:10]))

输出:

1
2
107243119477324.80328931501744819161741924145124146
107243119477324.8032893150

cbrt(10)的输出为:

1
9.9999999999999999999999999999999999999999999999998