Intro
(The previous/initial iteration is there.)
(The next iteration is there.)
This time, I have incorporated a nice answer by Martin R.
Updated code
package io.github.coderodde.math.simulation.volumes;
public class NdimensionalBallVolumes {
private static final int LARGEST_N = 10;
public static void main(String[] args) {
System.out.println("$$");
System.out.println("\\begin{aligned}");
int maxLineNumberLength = Integer.toString(LARGEST_N).length();
String lineFormat =
String.format(
"V_{%%%dd} &= %%-30s & \\\\ %%%% n = %%%dd\n",
maxLineNumberLength,
maxLineNumberLength);
for (int n = 1; n <= LARGEST_N; ++n) {
Volume v = getVolume(n);
System.out.printf(lineFormat,
n,
v.toString(),
n);
}
System.out.println("\\end{aligned}");
System.out.println("$$");
}
static Volume getVolume(int n) {
if (n == 1) {
return new Volume(2, 1, 0, n);
}
if (n == 2) {
return new Volume(1, 1, 1, n);
}
Volume previous = getVolume(n - 2);
return new Volume(previous.numerator * 2,
previous.denominator * n,
previous.piExponent + 1,
n);
}
static final class Volume {
long numerator;
long denominator;
int piExponent;
int n;
Volume(long numerator,
long denominator,
int piExponent,
int n) {
long gcd = gcd(numerator,
denominator);
this.numerator = numerator / gcd;
this.denominator = denominator / gcd;
this.piExponent = piExponent;
this.n = n;
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
if (denominator == 1) {
appendNumeratorString(sb);
} else {
sb.append("\\frac{");
appendNumeratorString(sb);
sb.append("}{");
sb.append(denominator);
sb.append("}");
}
return sb.toString();
}
private void appendNumeratorString(StringBuilder sb) {
if (numerator > 1) {
sb.append(numerator)
.append(" ");
}
if (piExponent > 0) {
sb.append("\\pi");
if (piExponent > 1) {
sb.append("^{")
.append(piExponent)
.append("}");
}
}
sb.append("R");
if (n > 1) {
sb.append("^{")
.append(n)
.append("}");
}
}
}
static long gcd(long a, long b) {
while (b != 0) {
long t = b;
b = a % b;
a = t;
}
return a;
}
}
Program output
$$
\begin{aligned}
V_{ 1} &= 2 R & \\ % n = 1
V_{ 2} &= \piR^{2} & \\ % n = 2
V_{ 3} &= \frac{4 \piR^{3}}{3} & \\ % n = 3
V_{ 4} &= \frac{\pi^{2}R^{4}}{2} & \\ % n = 4
V_{ 5} &= \frac{8 \pi^{2}R^{5}}{15} & \\ % n = 5
V_{ 6} &= \frac{\pi^{3}R^{6}}{6} & \\ % n = 6
V_{ 7} &= \frac{16 \pi^{3}R^{7}}{105} & \\ % n = 7
V_{ 8} &= \frac{\pi^{4}R^{8}}{24} & \\ % n = 8
V_{ 9} &= \frac{32 \pi^{4}R^{9}}{945} & \\ % n = 9
V_{10} &= \frac{\pi^{5}R^{10}}{120} & \\ % n = 10
\end{aligned}
$$
Critique request
How can I improve it further?
2 Answers 2
You compute the parameters for the volume of an \$ n \$-dimensional ball recursively, using the recurrence relation $$ V_1(R) = 2R ,円 , ,円 V_2(R) = \pi R^2 ,円 , \\ V_n(R) = \frac{2 \pi}{n} R^2 \cdot V_{n-2}(R) \text{ for $n \ge 3$.} $$
An alternative would be to compute this iteratively, using the general formula
$$
V_n(R) = \frac{\pi^{n/2}}{\Gamma(n/2+1)} R^n
$$
which is
$$
V_n(R) = \frac{\pi^{n/2}}{(n/2)!} R^n
$$
if \$ n \$ is even, and
$$
V_n(R) = \frac{\pi^{(n-1)/2} 2^{(n+1)/2}}{1 \cdot 3 \cdot 5 \cdots n} R^n
$$
if \$ n \$ is odd. The numerator and denominator are now relative prime so that the gcd
function is not needed anymore, and the constructor would be
Volume(int n) {
this.n = n;
this.piExponent = n / 2;
this.numerator = 1;
this.denominator = 1;
if (n % 2 == 0) {
for (int k = 1; k <= n/2; k++) {
this.denominator *= k;
}
} else {
for (int k = 1; k <= n; k += 2) {
this.numerator *= 2;
this.denominator *= k;
}
}
}
No temporary Volume
objects are created. The getVolume
method is no longer needed and
Volume v = new Volume(n);
is called instead. (And I would still prefer the variable name "dimension" over "n".)
Some remarks about the generated \$ \LaTeX \$ code:
- There must be a space between
\pi
andR
if\pi
is not followed by an exponent, otherwise invalid code\piR
is generated. - The last
&
in each line is not necessary. - "Aligning" the \$ \LaTeX \$ code does not work for dimensions \$ n \ge 11 \$ because the volume strings do not fit into 30 characters anymore. The denominators grow quickly! But fixing that is probably not worth the hassle because the alignment only makes the emitted code a bit easier to read, but does not affect the final output.
-
\$\begingroup\$ Funny. How did you calculate \2ドル\pi / n\$? \$\endgroup\$coderodde– coderodde2025年07月09日 10:16:23 +00:00Commented Jul 9 at 10:16
-
\$\begingroup\$ @coderodde: Sorry, I don't get what you mean. Which \$ 2 \pi/n \$ are you talking about? My suggested code uses \$ V_n(R) = \frac{\pi^{n/2}}{(n/2)!} R^n \$ for even \$ n \$ and \$ V_n(R) = \frac{\pi^{(n-1)/2} 2^{(n+1)/2}}{1 \cdot 3 \cdot 5 \cdots n} R^n \$ for odd \$ n\$. \$\endgroup\$Martin R– Martin R2025年07月09日 12:49:46 +00:00Commented Jul 9 at 12:49
-
\$\begingroup\$ \$V_n(R) = (2\pi)/n ´R^2 V_{n - 2}(R)\$ \$\endgroup\$coderodde– coderodde2025年07月09日 15:29:17 +00:00Commented Jul 9 at 15:29
-
\$\begingroup\$ @coderodde: That is the recursion formula that your code uses. The suggestion of this answer is to replace the recursion by an iterative computation, based on the general formula \$ V_n(R) = \frac{\pi^{n/2}}{\Gamma(n/2+1)} R^n \$. The results are the same, of course. \$\endgroup\$Martin R– Martin R2025年07月09日 15:38:40 +00:00Commented Jul 9 at 15:38
-
Final class
Why are you declaring class Volume
as final
?
static final class Volume {
long numerator;
long denominator;
int piExponent;
int n;
...
Here, final
does not mean Volume
is immutable. Rather, it means it cannot be extended; it is the final
class in the hierarchy, so class FooBar extends Volume
is not possible.
If your goal was to make the Volume
class immutable, you should make the members final as well ...
static final class Volume {
final long numerator;
final long denominator;
final int piExponent;
final int n;
...
Alternately, you could use a record
(preview feature added in Java 14, and officially added in Java 17).
static record Volume(long numerator,
long denominator,
int piExponent,
int n) {
Volume(long numerator,
long denominator,
int piExponent,
int n) { ... }
@Override
public String toString() {...}
private void appendNumeratorString(StringBuilder sb) {...}
}
toString
Why are you calling .toString()
?
System.out.printf(lineFormat,
n,
v.toString(),
n);
The format code for v
is %%-30s
, where the s
means "Displays the default string representation of the argument. If "S" is used then the string will be converted to uppercase where possible." So, you are converting v
to a string, and passing that string object to printf
which takes that object (the string object) and will call .toString()
on it anyway.
You could simply:
System.out.printf(lineFormat, n, v, n);