如何检查计算结果是否为自然数?

How to check if the result of a calculation is a natural number?

对于另一个 Project Euler 斗争(使用 SBCL 1.3.17),我想测试一个数字是否是 pentagonal number。如果

的结果,这可以很容易地测试
(/ (+ 1 (sqrt (+ 1 (* 24 number)))) 6)

是一个自然数。忽略自然数不仅仅是整数,因为 number 将只有正值,我开始使用 intergerp 作为谓词,但它不适用于所有测试的数字。所以我想出了以下内容:

(defun is-pentagonal-p (number)
  "Returns T if NUMBER is a pentagonal number."
  (multiple-value-bind (n m)
      (floor (/ (+ 1 (sqrt (+ 1 (* 24 number)))) 6))
    (declare (ignore n))
    (when (zerop m) t)))

对于简单的例子,它工作得很好,即低数字,但对于高数字再次失败,如 1533776805。过了一段时间后,我想起了我以前的 Fortran 日子,结果是:

(defun is-pentagonal-p (number)
  "Returns T if NUMBER is a pentagonal number."
  (multiple-value-bind (n m)
      (floor (/ (+ 1.0d0 (sqrt (+ 1.0d0 (* 24.0d0 number)))) 6.0d0))
    (declare (ignore n))
    (when (zerop m) t)))

它显着降低了舍入误差并给出了正确的结果,但让我觉得我一定是错过了一些更简单、更流畅的东西。这只是偏执狂吗?

物质

如果你使用CLISP,你会得到

(defun pentagonal-side (number)
  (/ (+ 1 (sqrt (+ 1 (* 24 number)))) 6))
(pentagonal-side 51)
==> 6
(pentagonal-side 1533)
==> 32.135838
(pentagonal-side 1533776805)
==> 31977

这是因为 ANSI CL 允许 sqrt to return rationals and CLISP does that. Thus you can use integerp:

(integerp (pentagonal-side 1533776805))
==> T

如果你口齿不清(SBCL) always returns a float, you need to use sufficient precision,例如:

(pentagonal-side 1533776805d0) ; double-float
==> 31977.0d0
(pentagonal-side 1533776805f0) ; single-float
==> 31977.0
(pentagonal-side 1533776805s0) ; short-float
==> 31976.8s0

因此,在您的情况下,只需传递适当的 float:

(zerop (mod (pentagonal-side 1533776805d0) 1))
==> T

警告

好像single-float是 够了吧?

(zerop (mod (pentagonal-side 1533776805f0) 1))
==> T

不!

(zerop (mod (pentagonal-side (1+ 1533776805f0)) 1))
==> T

使用"roundtrip"

提前猜出哪种浮点类型合适并不总是那么容易。 此外,可以想象,即使对于 Lisp 的 long-float,您的 number 也太大了。 (CLISP 有 arbitrary float precision,大多数 lisp 没有,即使那样你也需要提前决定使用哪个精度。)

因此更容易坚持使用整数:确保您计算的 pentagonal-side 与往返的原始数字相对应:

(defun pentagonal-side-int (area)
  (/ (+ 1 (isqrt (+ 1 (* 24 area)))) 6))
(defun pentagonal-area (side)
  (/ (- (* 3 side side) side) 2))
(pentagonal-side-int 1533776805)
==> 31977
(pentagonal-area 31977)
==> 1533776805
(defun pentagonal-number-p (number)
  (let ((side (pentagonal-side-int number)))
    (and (integerp side)
         (= number (pentagonal-area side)))))
(pentagonal-number-p 1533776805)
==> T
(pentagonal-number-p 1533776804)
==> NIL
(pentagonal-number-p 1533776806)
==> NIL

风格

名字

混合样式不是一个好主意。 is-... 是 C/Java 风格。 ...-p 是 Lisp 风格。我建议你坚持 后者用于您的 Lisp 代码。

Float contagion

无需将 所有 你的数字转换为浮点数:

(defun pentagonal-side-double (number)
  (/ (+ 1 (sqrt (+ 1 (* 24 (float number 1d0))))) 6))

应该让你所有的计算 使用 double-float.

Return值

使用 (zerop m) 而不是 (when (zerop m) t)。 一般来说,when是 在 "procedural context" 中使用,当 return 值被丢弃时。 如果您使用该值,则应改用 if(if (zerop m) t nil)(zerop m).

完全相同

Multiple values

你应该 使用 nth-value 代替 multiple-value-bind 加上 ignore.

标准函数

(1+ ...)比写(+ 1 ...)更具可读性。