Skip to content

Commit d4b8ef9

Browse files
committed
a(m,n,k) -> a_{n,m,k}
1 parent eee884f commit d4b8ef9

38 files changed

Lines changed: 1726 additions & 1211 deletions

File tree

content/post/three-var-recursive.md

Lines changed: 94 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -17,49 +17,49 @@ This post extends the generating-function technique from the [two-variable recur
1717

1818
We want to solve the recurrence
1919

20-
$a(m,n,k) = 2a(m-1,n-1,k-1) + a(m-1,n-1,k)$
21-
$ + a(m-1,n,k-1) + a(m,n-1,k-1)$
20+
$a_{n,m,k} = 2a_{n-1,m-1,k-1} + a_{n-1,m-1,k}$
21+
$ + a_{n,m-1,k-1} + a_{n-1,m,k-1}$
2222

2323
where $m$, $n$, $k$ are nonnegative integers, with boundary conditions:
2424

25-
- $a(0,0,0) = a(1,0,0) = a(0,1,0) = a(0,0,1) = 1$
26-
- $a(m,0,0) = a(0,m,0) = a(0,0,m) = 0$ for any $m > 1$
27-
- $a(m,n,k)$ is symmetric in $m$, $n$, $k$
25+
- $a_{0,0,0} = a_{0,1,0} = a_{1,0,0} = a_{0,0,1} = 1$
26+
- $a_{n,0,0} = a_{0,n,0} = a_{0,0,n} = 0$ for any $n > 1$
27+
- $a_{n,m,k}$ is symmetric in $n$, $m$, $k$
2828

29-
A subtlety: $a(0,1,1)$ is not defined by the recurrence alone, since it would require values like $a(-1,0,0)$. We take $a(m,n,k) = 0$ whenever any argument is negative.
29+
A subtlety: $a_{0,1,1}$ is not defined by the recurrence alone, since it would require values like $a_{-1,0,0}$. We take $a_{n,m,k} = 0$ whenever any argument is negative.
3030

3131
## The Generating Function
3232

3333
Define
3434

35-
$$\Phi(x,y,z) = \sum_{m,n,k \geq 0} a(m,n,k) \cdot x^m y^n z^k$$
35+
$$\Phi(x,y,z) = \sum_{n,m,k \geq 0} a_{n,m,k} \cdot x^n y^m z^k$$
3636

3737
Using the initial values above, we can write the recurrence including boundary terms as follow:
3838

39-
$a(m, n, k) = 2a(m-1, n-1, k-1) + a(m-1, n-1, k)$
40-
$ + a(m-1, n, k-1) + a(m, n-1, k-1)$
41-
$ + [m=n=k=0] + [m=n=0 \wedge k=1]$
42-
$ + [m=k=0 \wedge n=1] + [n=k=0 \wedge m=1]$
39+
$a_{n,m,k} = 2a_{n-1,m-1,k-1} + a_{n-1,m-1,k}$
40+
$ + a_{n,m-1,k-1} + a_{n-1,m,k-1}$
41+
$ + [n=m=k=0] + [n=m=0 \wedge k=1]$
42+
$ + [n=k=0 \wedge m=1] + [m=k=0 \wedge n=1]$
4343

4444
<!-- I believe there are still some initial conditions missing, since for example $a(0,1,1)$ is not well defined. Computing its value will result in negative arguments:
4545
46-
$a(0,1,1) = 2a(-1, 0, 0) + a(-1, 0, 1) + a(-1, 1, 0) + a(0, 0, 0)$
47-
$ = 2a(-1, 0, 0) + 2a(-1, 0, 1) + 1$
46+
$a_{1,0,1} = 2a_{0,-1,0} + a_{0,-1,1} + a_{1,-1,0} + a_{0,0,0}$
47+
$ = 2a_{0,-1,0} + 2a_{0,-1,1} + 1$
4848
49-
Adding the extra condition that $a(m,n,k)=0$ for any negative argument(s) solves the issue. -->
49+
Adding the extra condition that $a_{n,m,k}=0$ for any negative argument(s) solves the issue. -->
5050

5151
Substituting the recurrence into the generating function and collecting terms:
5252

53-
$\Phi(x,y,z) = \sum_{m,n,k} a(m,n,k) \cdot x^m y^n z^k $
54-
$ = 2\sum_{m,n,k} a(m,n,k) \cdot x^{m+1} y^{n+1} z^{k+1} $
55-
$ + \sum_{m,n,k} a(m,n,k) \cdot x^{m+1} y^{n+1} z^k $
56-
$ + \sum_{m,n,k} a(m,n,k) \cdot x^{m+1} y^n z^{k+1} $
57-
$ + \sum_{m,n,k} a(m,n,k) \cdot x^m y^{n+1} z^{k+1} $
53+
$\Phi(x,y,z) = \sum_{n,m,k} a_{n,m,k} \cdot x^n y^m z^k $
54+
$ = 2\sum_{n,m,k} a_{n,m,k} \cdot x^{n+1} y^{m+1} z^{k+1} $
55+
$ + \sum_{n,m,k} a_{n,m,k} \cdot x^{n+1} y^{m+1} z^k $
56+
$ + \sum_{n,m,k} a_{n,m,k} \cdot x^{n+1} y^m z^{k+1} $
57+
$ + \sum_{n,m,k} a_{n,m,k} \cdot x^n y^{m+1} z^{k+1} $
5858
$ + 1 + x + y + z $
5959
$ = 2 \Phi \cdot x y z + \Phi \cdot x y + \Phi \cdot x z + \Phi \cdot y z + 1 + x + y + z$
6060
$ = \Phi \cdot ( 2 x y z + x y + x z + y z ) + 1 + x + y + z$
6161

62-
where the boundary terms $1 + x + y + z$ come from $a(0,0,0)$, $a(1,0,0)$, $a(0,1,0)$, $a(0,0,1)$.
62+
where the boundary terms $1 + x + y + z$ come from $a_{0,0,0}$, $a_{1,0,0}$, $a_{0,1,0}$, $a_{0,0,1}$.
6363

6464
Solving for $\Phi$:
6565

@@ -109,19 +109,24 @@ $ = (1 + x + y + z) \sum_{k_1+k_2+k_3+k_4=N} \binom{N} {k_1,k_2,k_3,k_4} (2 x y
109109
$ = (1 + x + y + z) \sum_{k_1+k_2+k_3+k_4=N} \binom{N} {k_1,k_2,k_3,k_4} 2^{k_1} x^{k_1+k_2+k_3} y^{k_1+k_2+k_4} z^{k_1+k_3+k_4}$
110110
$ = (1 + x + y + z) \sum_{k_1+k_2+k_3\leq N} \binom{N} {k_1,k_2,k_3, N-k_1-k_2-k_3} 2^{k_1} x^{k_1+k_2+k_3} y^{N-k_3} z^{N-k_2}$ -->
111111

112-
Extracting the coefficient of $x^m y^n z^k$ gives the closed form. The full expression has four sums (from the numerator $1+x+y+z$):
112+
Extracting the coefficient of $x^n y^m z^k$ gives the closed form. The full expression has four sums (from the numerator $1+x+y+z$):
113113

114-
$$a(m,n,k) = \sum_{N=\max(m,n,k)}^{ (m+n+k)/2 } \binom{N}{m+n+k-2N, N-m, N-n, N-k} 2^{m+n+k-2N}$$
115-
$$ + \sum_{N=\max(m-1,n,k)}^{ (m+n+k-1)/2 } \binom{N}{m+n+k-2N-1, N-m+1, N-n, N-k} 2^{m+n+k-2N-1}$$
116-
$$ + \sum_{N=\max(m,n-1,k)}^{ (m+n+k-1)/2 } \binom{N}{m+n+k-2N-1, N-m, N-n+1, N-k} 2^{m+n+k-2N-1}$$
117-
$$ + \sum_{N=\max(m,n,k-1)}^{ (m+n+k-1)/2 } \binom{N}{m+n+k-2N-1, N-m, N-n, N-k+1} 2^{m+n+k-2N-1}$$
114+
$$a_{n,m,k} = \sum_{N=\max(n,m,k)}^{ (n+m+k)/2 } \binom{N}{n+m+k-2N, N-n, N-m, N-k} 2^{n+m+k-2N}$$
115+
$$ + \sum_{N=\max(n,m-1,k)}^{ (n+m+k-1)/2 } \binom{N}{n+m+k-2N-1, N-n, N-m+1, N-k} 2^{n+m+k-2N-1}$$
116+
$$ + \sum_{N=\max(n-1,m,k)}^{ (m+m+k-1)/2 } \binom{N}{n+m+k-2N-1, N-n+1, N-m, N-k} 2^{n+m+k-2N-1}$$
117+
$$ + \sum_{N=\max(n,m,k-1)}^{ (n+m+k-1)/2 } \binom{N}{n+m+k-2N-1, N-n, N-m, N-k+1} 2^{n+m+k-2N-1}$$
118118

119119
There may be room to simplify this further; the symmetry in $m,n,k$ could help.
120120

121121
## Complexity
122122

123-
- **Recursion with memoization (DP):** $\Theta(m \cdot n \cdot k)$ time and space.
124-
- **Closed form:** Precompute factorials, then loop over $N$; time and space $\Theta(m+n+k)$, ignoring the cost of arithmetic on large integers.
123+
**Recursion with memoization (DP):**
124+
- time and space: $\Theta(n \cdot m \cdot k)$.
125+
126+
**Closed form:**
127+
- Precompute factorials, then loop over $N$;
128+
- time and space: $\Theta(n+m+k)$,
129+
- we ignore the cost of arithmetic on large integers.
125130

126131
## Implementation
127132

@@ -130,52 +135,87 @@ Both the recursive and closed-form versions in Python:
130135
```python
131136
import functools
132137
import math
138+
```
133139

140+
```python
134141
@functools.lru_cache(maxsize=None)
135-
def a_rec(m: int, n: int, k: int) -> int:
136-
if min(m, n, k) < 0:
142+
def a_rec(n: int, m: int, k: int) -> int:
143+
if not (n <= m <= k):
144+
n,m,k = sorted([n,m,k])
145+
return a_rec(n,m,k)
146+
147+
if n < 0:
137148
return 0
138-
if m + n + k == 0 or m + n + k == 1:
149+
if n + m + k <= 1:
139150
return 1
140-
if m + n == 0 or m + k == 0 or n + k == 0:
151+
if n + m == 0 or n + k == 0 or m + k == 0:
141152
return 0
153+
if n == 0:
154+
return int(m + 1 >= k)
155+
142156
return (
143-
2 * a_rec(m - 1, n - 1, k - 1)
144-
+ a_rec(m - 1, n - 1, k)
145-
+ a_rec(m - 1, n, k - 1)
146-
+ a_rec(m, n - 1, k - 1)
157+
2 * a_rec(n - 1, m - 1, k - 1)
158+
+ a_rec(n - 1, m - 1, k)
159+
+ a_rec(n - 1, m, k - 1)
160+
+ a_rec(n, m - 1, k - 1)
147161
)
148162

163+
```
164+
165+
```python
166+
149167
@functools.lru_cache(maxsize=None)
150-
def _binom4(N: int, a: int, b: int, c: int) -> int:
151-
r = N - a - b - c
152-
vals = sorted([a, b, c, r])
153-
assert vals[0] >= 0
168+
def binom4(N: int, k1: int, k2: int, k3: int) -> int:
169+
k4 = N - k1 - k2 - k3
170+
154171
return math.factorial(N) // (
155-
math.factorial(vals[0]) * math.factorial(vals[1])
156-
* math.factorial(vals[2]) * math.factorial(vals[3])
172+
math.factorial(k1) * math.factorial(k2)
173+
* math.factorial(k3) * math.factorial(k4)
157174
)
158175

159-
def a_closed(m: int, n: int, k: int) -> int:
160-
if min(m, n, k) < 0:
176+
177+
def a_closed(n: int, m: int, k: int) -> int:
178+
if min(n, m, k) < 0:
161179
return 0
162180
s = 0
163-
for N in range(max(m, n, k), (m + n + k) // 2 + 1):
164-
s += _binom4(N, N - m, N - n, N - k) * 2 ** (m + n + k - 2 * N)
165-
for N in range(max(m - 1, n, k), (m + n + k - 1) // 2 + 1):
166-
s += _binom4(N, N - m + 1, N - n, N - k) * 2 ** (m + n + k - 2 * N - 1)
167-
for N in range(max(m, n - 1, k), (m + n + k - 1) // 2 + 1):
168-
s += _binom4(N, N - m, N - n + 1, N - k) * 2 ** (m + n + k - 2 * N - 1)
169-
for N in range(max(m, n, k - 1), (m + n + k - 1) // 2 + 1):
170-
s += _binom4(N, N - m, N - n, N - k + 1) * 2 ** (m + n + k - 2 * N - 1)
181+
for N in range(max(n, m, k), (n + m + k) // 2 + 1):
182+
s += binom4(N, N - n, N - m, N - k) * 2 ** (n + m + k - 2 * N)
183+
for N in range(max(n, m - 1, k), (n + m + k - 1) // 2 + 1):
184+
s += binom4(N, N - n, N - m + 1, N - k) * 2 ** (n + m + k - 2 * N - 1)
185+
for N in range(max(n - 1, m, k), (n + m + k - 1) // 2 + 1):
186+
s += binom4(N, N - n + 1, N - m, N - k) * 2 ** (n + m + k - 2 * N - 1)
187+
for N in range(max(n, m, k - 1), (n + m + k - 1) // 2 + 1):
188+
s += binom4(N, N - n, N - m, N - k + 1) * 2 ** (n + m + k - 2 * N - 1)
171189
return s
190+
```
191+
192+
```python
193+
import timeit
194+
setup_code = "from __main__ import a_rec, a_closed"
195+
196+
execution_time = timeit.timeit('a_rec(200, 300, 400)', setup=setup_code, number=1)
197+
print(f"Execution time for recursive: {execution_time} seconds")
198+
199+
execution_time = timeit.timeit('a_closed(200, 300, 400)', setup=setup_code, number=1)
200+
print(f"Execution time for closed form: {execution_time} seconds")
201+
```
202+
203+
```text
204+
Execution time for recursive: 8.308788761030883 seconds
205+
Execution time for closed form: 0.003819321980699897 seconds
206+
```
172207

208+
```python
173209
# Sanity check
174-
r, r1 = a_rec(100, 200, 210), a_closed(100, 200, 210)
175-
print(f"Recursive: {r}, Closed: {r1}, Match: {r == r1}")
210+
res0 = a_rec(100, 200, 210)
211+
res1 = a_closed(100, 200, 210)
212+
213+
print(f"Recursive: {res0}")
214+
print(f"Closed: {res1}")
215+
print(f"Match: {res0 == res1}")
176216
```
177217

178-
We did not exploit the symmetry $a(m,n,k) = a(\sigma(m,n,k))$ for permutations $\sigma$; it could speed up computation but does not obviously simplify the closed expression.
218+
We did not exploit the symmetry $a_{n,m,k} = a_{\sigma(n,m,k)}$ for permutations $\sigma$; it could speed up computation but does not obviously simplify the closed expression.
179219

180220
---
181221

index.html

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -988,15 +988,15 @@ <h1 class="mb-0">Recent Posts</h1>
988988
<p>This post extends the generating-function technique from the <a href="/post/two-var-recursive-func/">two-variable recursion</a> to a three-variable case. I originally wrote this as an answer to a <a href="https://math.stackexchange.com/questions/1093271/how-to-solve-this-multivariable-recursion/2730331#2730331" target="_blank" rel="noopener">Math Stack Exchange question</a>; here it is adapted for the blog with clearer exposition and code.</p>
989989
<h2 id="the-problem">The Problem</h2>
990990
<p>We want to solve the recurrence</p>
991-
<p>$a(m,n,k) = 2a(m-1,n-1,k-1) + a(m-1,n-1,k)$
992-
$ + a(m-1,n,k-1) + a(m,n-1,k-1)$</p>
991+
<p>$a_{n,m,k} = 2a_{n-1,m-1,k-1} + a_{n-1,m-1,k}$
992+
$ + a_{n,m-1,k-1} + a_{n-1,m,k-1}$</p>
993993
<p>where $m$, $n$, $k$ are nonnegative integers, with boundary conditions:</p>
994994
<ul>
995-
<li>$a(0,0,0) = a(1,0,0) = a(0,1,0) = a(0,0,1) = 1$</li>
996-
<li>$a(m,0,0) = a(0,m,0) = a(0,0,m) = 0$ for any $m &gt; 1$</li>
997-
<li>$a(m,n,k)$ is symmetric in $m$, $n$, $k$</li>
995+
<li>$a_{0,0,0} = a_{0,1,0} = a_{1,0,0} = a_{0,0,1} = 1$</li>
996+
<li>$a_{n,0,0} = a_{0,n,0} = a_{0,0,n} = 0$ for any $n &gt; 1$</li>
997+
<li>$a_{n,m,k}$ is symmetric in $n$, $m$, $k$</li>
998998
</ul>
999-
<p>A subtlety: $a(0,1,1)$ is not defined by the recurrence alone, since it would require values like $a(-1,0,0)$. We take $a(m,n,k) = 0$ whenever any argument is negative.</p>
999+
<p>A subtlety: $a_{0,1,1}$ is not defined by the recurrence alone, since it would require values like $a_{-1,0,0}$. We take $a_{n,m,k} = 0$ whenever any argument is negative.</p>
10001000
</div>
10011001
</a>
10021002

0 commit comments

Comments
 (0)