Skip to content

Commit 4f7ab0f

Browse files
committed
Solving a Three-Variable Recursion via Generating Functions
1 parent daeeacc commit 4f7ab0f

48 files changed

Lines changed: 5323 additions & 240 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

404.html

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -568,6 +568,8 @@ <h1>Page not found</h1>
568568
<h2>Latest</h2>
569569
<ul>
570570

571+
<li><a href="/post/three-var-recursive/">Solving a Three-Variable Recursion via Generating Functions</a></li>
572+
571573
<li><a href="/post/">Posts</a></li>
572574

573575
<li><a href="/post/perfect-distribution/">Perfect Distribution: GCD in Disguise</a></li>
Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
+++
2+
date = "2018-04-10T03:14:00Z"
3+
math = true
4+
highlight = true
5+
tags = ["math", "python", "generating function", "recursion", "combinatorics", "dynamic programming", "multivariate function"]
6+
title = "Solving a Three-Variable Recursion via Generating Functions"
7+
draft = false
8+
9+
[header]
10+
image = ""
11+
caption = ""
12+
+++
13+
14+
This post extends the generating-function technique from the [two-variable recursion][two-var-recursive-func] to a three-variable case. I originally wrote this as an answer to a [Math Stack Exchange question][math-se]; here it is adapted for the blog with clearer exposition and code.
15+
16+
## The Problem
17+
18+
We want to solve the recurrence
19+
20+
$$a(m,n,k) = 2a(m-1,n-1,k-1) + a(m-1,n-1,k) + a(m-1,n,k-1) + a(m,n-1,k-1)$$
21+
22+
where $m$, $n$, $k$ are nonnegative integers, with boundary conditions:
23+
24+
- $a(0,0,0) = a(1,0,0) = a(0,1,0) = a(0,0,1) = 1$
25+
- $a(m,0,0) = a(0,m,0) = a(0,0,m) = 0$ for $m > 1$
26+
- $a(m,n,k)$ is symmetric in $m,n,k$
27+
28+
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+
30+
## The Generating Function
31+
32+
Define
33+
34+
$$\Phi(x,y,z) = \sum_{m,n,k \geq 0} a(m,n,k)\, x^m y^n z^k$$
35+
36+
Substituting the recurrence and collecting terms, we get
37+
38+
$$
39+
\begin{aligned}
40+
\Phi(x,y,z)
41+
&= \sum_{m,n,k} a(m,n,k)\, x^m y^n z^k \\
42+
&= 2\Phi\, xyz + \Phi\, xy + \Phi\, xz + \Phi\, yz + 1 + x + y + z
43+
\end{aligned}
44+
$$
45+
46+
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)$. Solving for $\Phi$:
47+
48+
$$\Phi(x,y,z) = \frac{1 + x + y + z}{1 - 2xyz - xy - xz - yz}$$
49+
50+
## From Generating Function to Closed Form
51+
52+
Using $\frac{1}{1-\rho} = \sum_{i \geq 0} \rho^i$ and the multinomial expansion
53+
54+
$$(x_1+x_2+x_3+x_4)^N = \sum_{k_1+k_2+k_3+k_4=N} \binom{N}{k_1,k_2,k_3,k_4} x_1^{k_1} x_2^{k_2} x_3^{k_3} x_4^{k_4}$$
55+
56+
with $\binom{N}{k_1,k_2,k_3,k_4} = \frac{N!}{k_1!\,k_2!\,k_3!\,k_4!}$, we expand the denominator. Let $\rho = 2xyz + xy + xz + yz$. Then
57+
58+
$$
59+
\Phi = (1+x+y+z) \sum_{N \geq 0} \rho^N
60+
$$
61+
62+
Writing $\rho^N$ with terms $(2xyz)^{k_1}(xy)^{k_2}(xz)^{k_3}(yz)^{k_4}$ where $k_1+k_2+k_3+k_4 = N$, and extracting the coefficient of $x^m y^n z^k$, yields the closed form:
63+
64+
$$
65+
\begin{aligned}
66+
a(m,n,k)
67+
&= \sum_{\max(m,n,k) \leq N \leq \frac{m+n+k}{2}}
68+
\binom{N}{m+n+k-2N,\, N-m,\, N-n,\, N-k} 2^{m+n+k-2N} \\
69+
&\quad + \text{three similar sums from the } 1,\, x,\, y,\, z \text{ terms}
70+
\end{aligned}
71+
$$
72+
73+
The full expression has four sums corresponding to the four terms in the numerator $1+x+y+z$. The exact formulas are:
74+
75+
$$
76+
\begin{aligned}
77+
a(m,n,k)
78+
&= \sum_{N=\max(m,n,k)}^{\lfloor (m+n+k)/2 \rfloor}
79+
\binom{N}{m+n+k-2N,\, N-m,\, N-n,\, N-k} 2^{m+n+k-2N} \\
80+
&\quad + \sum_{N=\max(m-1,n,k)}^{\lfloor (m+n+k-1)/2 \rfloor}
81+
\binom{N}{m+n+k-2N-1,\, N-m+1,\, N-n,\, N-k} 2^{m+n+k-2N-1} \\
82+
&\quad + \sum_{N=\max(m,n-1,k)}^{\lfloor (m+n+k-1)/2 \rfloor}
83+
\binom{N}{m+n+k-2N-1,\, N-m,\, N-n+1,\, N-k} 2^{m+n+k-2N-1} \\
84+
&\quad + \sum_{N=\max(m,n,k-1)}^{\lfloor (m+n+k-1)/2 \rfloor}
85+
\binom{N}{m+n+k-2N-1,\, N-m,\, N-n,\, N-k+1} 2^{m+n+k-2N-1}
86+
\end{aligned}
87+
$$
88+
89+
There may be room to simplify this further; the symmetry in $m,n,k$ could help.
90+
91+
## Complexity
92+
93+
- **Recursion with memoization (DP):** $\Theta(m \cdot n \cdot k)$ time and space.
94+
- **Closed form:** Precompute factorials, then loop over $N$; time and space $\Theta(m+n+k)$, ignoring the cost of arithmetic on large integers.
95+
96+
## Implementation
97+
98+
Both the recursive and closed-form versions in Python:
99+
100+
```python
101+
import functools
102+
import math
103+
104+
@functools.lru_cache(maxsize=None)
105+
def a_rec(m: int, n: int, k: int) -> int:
106+
if min(m, n, k) < 0:
107+
return 0
108+
if m + n + k == 0 or m + n + k == 1:
109+
return 1
110+
if m + n == 0 or m + k == 0 or n + k == 0:
111+
return 0
112+
return (
113+
2 * a_rec(m - 1, n - 1, k - 1)
114+
+ a_rec(m - 1, n - 1, k)
115+
+ a_rec(m - 1, n, k - 1)
116+
+ a_rec(m, n - 1, k - 1)
117+
)
118+
119+
@functools.lru_cache(maxsize=None)
120+
def _binom4(N: int, a: int, b: int, c: int) -> int:
121+
r = N - a - b - c
122+
vals = sorted([a, b, c, r])
123+
assert vals[0] >= 0
124+
return math.factorial(N) // (
125+
math.factorial(vals[0]) * math.factorial(vals[1])
126+
* math.factorial(vals[2]) * math.factorial(vals[3])
127+
)
128+
129+
def a_closed(m: int, n: int, k: int) -> int:
130+
if min(m, n, k) < 0:
131+
return 0
132+
s = 0
133+
for N in range(max(m, n, k), (m + n + k) // 2 + 1):
134+
s += _binom4(N, N - m, N - n, N - k) * 2 ** (m + n + k - 2 * N)
135+
for N in range(max(m - 1, n, k), (m + n + k - 1) // 2 + 1):
136+
s += _binom4(N, N - m + 1, N - n, N - k) * 2 ** (m + n + k - 2 * N - 1)
137+
for N in range(max(m, n - 1, k), (m + n + k - 1) // 2 + 1):
138+
s += _binom4(N, N - m, N - n + 1, N - k) * 2 ** (m + n + k - 2 * N - 1)
139+
for N in range(max(m, n, k - 1), (m + n + k - 1) // 2 + 1):
140+
s += _binom4(N, N - m, N - n, N - k + 1) * 2 ** (m + n + k - 2 * N - 1)
141+
return s
142+
143+
# Sanity check
144+
r, r1 = a_rec(100, 200, 210), a_closed(100, 200, 210)
145+
print(f"Recursive: {r}, Closed: {r1}, Match: {r == r1}")
146+
```
147+
148+
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.
149+
150+
---
151+
152+
*Originally answered on [Math Stack Exchange][math-se].*
153+
154+
[two-var-recursive-func]: /post/two-var-recursive-func/
155+
[math-se]: https://math.stackexchange.com/questions/1093271/how-to-solve-this-multivariable-recursion/2730331#2730331

index.html

Lines changed: 49 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -341,7 +341,7 @@
341341
<meta property="og:description" content="Software Engineer" /><meta property="og:image" content="/media/icon_hu_43cf117bf1a42c34.png" /><meta property="og:locale" content="en-us" />
342342

343343

344-
<meta property="og:updated_time" content="2026-02-08T19:47:43&#43;00:00" />
344+
<meta property="og:updated_time" content="2018-04-10T03:14:00&#43;00:00" />
345345

346346

347347

@@ -975,14 +975,23 @@ <h1 class="mb-0">Recent Posts</h1>
975975
<div class="media-body">
976976

977977
<div class="section-subheading article-title mb-0 mt-0">
978-
<a href="/post/perfect-distribution/" >Perfect Distribution: GCD in Disguise</a>
978+
<a href="/post/three-var-recursive/" >Solving a Three-Variable Recursion via Generating Functions</a>
979979
</div>
980980

981981

982-
<a href="/post/perfect-distribution/" class="summary-link">
982+
<a href="/post/three-var-recursive/" class="summary-link">
983983
<div class="article-style">
984-
<h2 id="perfect-distribution-gcd-in-disguise">Perfect Distribution: GCD in Disguise</h2>
985-
<p>We discuss an algorithm that distributes $a$ ones among $n$ positions so that the gaps between consecutive ones differ by at most one—a <strong>perfect distribution</strong>. I developed it while working on profiling, stress, and negative testing of a system that needed exactly this kind of uniform spread. I am not aware of prior art; if you know of related work, I would be interested to hear.</p>
984+
<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>
985+
<h2 id="the-problem">The Problem</h2>
986+
<p>We want to solve the recurrence</p>
987+
<p>$$a(m,n,k) = 2a(m-1,n-1,k-1) + a(m-1,n-1,k) + a(m-1,n,k-1) + a(m,n-1,k-1)$$</p>
988+
<p>where $m$, $n$, $k$ are nonnegative integers, with boundary conditions:</p>
989+
<ul>
990+
<li>$a(0,0,0) = a(1,0,0) = a(0,1,0) = a(0,0,1) = 1$</li>
991+
<li>$a(m,0,0) = a(0,m,0) = a(0,0,m) = 0$ for $m &gt; 1$</li>
992+
<li>$a(m,n,k)$ is symmetric in $m,n,k$</li>
993+
</ul>
994+
<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>
986995
</div>
987996
</a>
988997

@@ -1006,7 +1015,7 @@ <h2 id="perfect-distribution-gcd-in-disguise">Perfect Distribution: GCD in Disgu
10061015

10071016

10081017

1009-
Aug 1, 2017
1018+
Apr 10, 2018
10101019
</span>
10111020

10121021

@@ -1015,7 +1024,7 @@ <h2 id="perfect-distribution-gcd-in-disguise">Perfect Distribution: GCD in Disgu
10151024

10161025
<span class="middot-divider"></span>
10171026
<span class="article-reading-time">
1018-
7 min read
1027+
4 min read
10191028
</span>
10201029

10211030

@@ -1063,15 +1072,14 @@ <h2 id="perfect-distribution-gcd-in-disguise">Perfect Distribution: GCD in Disgu
10631072
<div class="media-body">
10641073

10651074
<div class="section-subheading article-title mb-0 mt-0">
1066-
<a href="/post/binomial-modulo-prime/" >Binomial Coefficients Modulo a Prime: Fermat&#39;s Theorem and the Non-Adjacent Selection Problem</a>
1075+
<a href="/post/perfect-distribution/" >Perfect Distribution: GCD in Disguise</a>
10671076
</div>
10681077

10691078

1070-
<a href="/post/binomial-modulo-prime/" class="summary-link">
1079+
<a href="/post/perfect-distribution/" class="summary-link">
10711080
<div class="article-style">
1072-
<p>In the <a href="/post/efficient-implementation-non-adjacent-selection/">previous post</a>, we implemented the closed form $F_{n,m} = \binom{n-m+1}{m}$ using Python&rsquo;s <code>math.factorial</code>, and with <code>scipy</code> and <code>sympy</code>. Here we cover the common competitive-programming case: computing the answer <strong>modulo a large prime</strong> $M$ (e.g. $M = 10^9+7$).</p>
1073-
<h2 id="why-modulo">Why modulo?</h2>
1074-
<p>In counting problems, the result can be huge even for moderate input. Often the problem asks for the answer modulo a big prime so that it fits in a standard integer type. We could compute the full number and then take the remainder, but that forces expensive long-integer arithmetic. Computing <strong>everything</strong> modulo $M$ from the start is much faster.</p>
1081+
<h2 id="perfect-distribution-gcd-in-disguise">Perfect Distribution: GCD in Disguise</h2>
1082+
<p>We discuss an algorithm that distributes $a$ ones among $n$ positions so that the gaps between consecutive ones differ by at most one—a <strong>perfect distribution</strong>. I developed it while working on profiling, stress, and negative testing of a system that needed exactly this kind of uniform spread. I am not aware of prior art; if you know of related work, I would be interested to hear.</p>
10751083
</div>
10761084
</a>
10771085

@@ -1095,7 +1103,7 @@ <h2 id="why-modulo">Why modulo?</h2>
10951103

10961104

10971105

1098-
Jul 8, 2017
1106+
Aug 1, 2017
10991107
</span>
11001108

11011109

@@ -1104,7 +1112,7 @@ <h2 id="why-modulo">Why modulo?</h2>
11041112

11051113
<span class="middot-divider"></span>
11061114
<span class="article-reading-time">
1107-
2 min read
1115+
7 min read
11081116
</span>
11091117

11101118

@@ -1152,17 +1160,15 @@ <h2 id="why-modulo">Why modulo?</h2>
11521160
<div class="media-body">
11531161

11541162
<div class="section-subheading article-title mb-0 mt-0">
1155-
<a href="/post/efficient-implementation-non-adjacent-selection/" >Efficient Implementation of the Non-Adjacent Selection Formula</a>
1163+
<a href="/post/binomial-modulo-prime/" >Binomial Coefficients Modulo a Prime: Fermat&#39;s Theorem and the Non-Adjacent Selection Problem</a>
11561164
</div>
11571165

11581166

1159-
<a href="/post/efficient-implementation-non-adjacent-selection/" class="summary-link">
1167+
<a href="/post/binomial-modulo-prime/" class="summary-link">
11601168
<div class="article-style">
1161-
<p>In the <a href="/post/two-var-recursive-func/">previous post</a>, we derived the closed form for the non-adjacent selection problem:</p>
1162-
<p>$$ F_{n, m} = {n - m + 1 \choose m} $$</p>
1163-
<p>Now we discuss how to implement this efficiently in Python—from a simple factorial-based solution to library implementations. For the common case of computing the answer <strong>modulo a large prime</strong> (e.g. in competitive programming), see the <a href="/post/binomial-modulo-prime/">next post</a>.</p>
1164-
<h2 id="fast-solutions-based-on-binomials">Fast Solutions Based on Binomials</h2>
1165-
<p>We can reflect the closed form in very trivial Python code:</p>
1169+
<p>In the <a href="/post/efficient-implementation-non-adjacent-selection/">previous post</a>, we implemented the closed form $F_{n,m} = \binom{n-m+1}{m}$ using Python&rsquo;s <code>math.factorial</code>, and with <code>scipy</code> and <code>sympy</code>. Here we cover the common competitive-programming case: computing the answer <strong>modulo a large prime</strong> $M$ (e.g. $M = 10^9+7$).</p>
1170+
<h2 id="why-modulo">Why modulo?</h2>
1171+
<p>In counting problems, the result can be huge even for moderate input. Often the problem asks for the answer modulo a big prime so that it fits in a standard integer type. We could compute the full number and then take the remainder, but that forces expensive long-integer arithmetic. Computing <strong>everything</strong> modulo $M$ from the start is much faster.</p>
11661172
</div>
11671173
</a>
11681174

@@ -1186,7 +1192,7 @@ <h2 id="fast-solutions-based-on-binomials">Fast Solutions Based on Binomials</h2
11861192

11871193

11881194

1189-
Jul 7, 2017
1195+
Jul 8, 2017
11901196
</span>
11911197

11921198

@@ -1195,7 +1201,7 @@ <h2 id="fast-solutions-based-on-binomials">Fast Solutions Based on Binomials</h2
11951201

11961202
<span class="middot-divider"></span>
11971203
<span class="article-reading-time">
1198-
4 min read
1204+
2 min read
11991205
</span>
12001206

12011207

@@ -1243,19 +1249,17 @@ <h2 id="fast-solutions-based-on-binomials">Fast Solutions Based on Binomials</h2
12431249
<div class="media-body">
12441250

12451251
<div class="section-subheading article-title mb-0 mt-0">
1246-
<a href="/post/two-var-recursive-func/" >Cracking Multivariate Recursive Equations Using Generating Functions</a>
1252+
<a href="/post/efficient-implementation-non-adjacent-selection/" >Efficient Implementation of the Non-Adjacent Selection Formula</a>
12471253
</div>
12481254

12491255

1250-
<a href="/post/two-var-recursive-func/" class="summary-link">
1256+
<a href="/post/efficient-implementation-non-adjacent-selection/" class="summary-link">
12511257
<div class="article-style">
1252-
<p>In this post, we return back to the combinatorial problem discussed in <a href="/post/intro-to-dp/">Introduction to Dynamic Programming and Memoization</a> post.
1253-
We will show that generating functions may work great not only for single variable case (see <a href="/post/gen-func-art/">The Art of Generating Functions</a>),
1254-
but also could be very useful for hacking two-variable relations (and of course, in general for multivariate case too).</p>
1255-
<p>For making the post self-contained, we repeat the problem definition here.</p>
1256-
<h2 id="the-problem">The Problem</h2>
1257-
<blockquote>
1258-
<p>Compute the number of ways to choose $m$ elements from $n$ elements such that selected elements in one combination are not adjacent.</p>
1258+
<p>In the <a href="/post/two-var-recursive-func/">previous post</a>, we derived the closed form for the non-adjacent selection problem:</p>
1259+
<p>$$ F_{n, m} = {n - m + 1 \choose m} $$</p>
1260+
<p>Now we discuss how to implement this efficiently in Python—from a simple factorial-based solution to library implementations. For the common case of computing the answer <strong>modulo a large prime</strong> (e.g. in competitive programming), see the <a href="/post/binomial-modulo-prime/">next post</a>.</p>
1261+
<h2 id="fast-solutions-based-on-binomials">Fast Solutions Based on Binomials</h2>
1262+
<p>We can reflect the closed form in very trivial Python code:</p>
12591263
</div>
12601264
</a>
12611265

@@ -1279,7 +1283,7 @@ <h2 id="the-problem">The Problem</h2>
12791283

12801284

12811285

1282-
Jul 6, 2017
1286+
Jul 7, 2017
12831287
</span>
12841288

12851289

@@ -1288,7 +1292,7 @@ <h2 id="the-problem">The Problem</h2>
12881292

12891293
<span class="middot-divider"></span>
12901294
<span class="article-reading-time">
1291-
3 min read
1295+
4 min read
12921296
</span>
12931297

12941298

@@ -1336,20 +1340,19 @@ <h2 id="the-problem">The Problem</h2>
13361340
<div class="media-body">
13371341

13381342
<div class="section-subheading article-title mb-0 mt-0">
1339-
<a href="/post/gen-func-art/" >The Art of Generating Functions</a>
1343+
<a href="/post/two-var-recursive-func/" >Cracking Multivariate Recursive Equations Using Generating Functions</a>
13401344
</div>
13411345

13421346

1343-
<a href="/post/gen-func-art/" class="summary-link">
1347+
<a href="/post/two-var-recursive-func/" class="summary-link">
13441348
<div class="article-style">
1345-
<p>The notion of generating functions and its application to solving recursive equations are very well-known.
1346-
For reader who did not have a chance to get familiar with the topics,
1347-
I recommend to take a look at very good book:
1348-
<a href="https://en.wikipedia.org/wiki/Concrete_Mathematics" target="_blank" rel="noopener">Concrete Mathematics: A Foundation for Computer Science, by Ronald L. Graham, Donald E. Knuth, Oren Patashnik</a>.</p>
1349-
<p>Generating functions are usually applied to single variable recursive equations.
1350-
But actually, the technique may be extended to multivariate recursive equations, or even to a system of recursive equations.
1351-
Readers who are familiar with one-variable case, may jump directly to the next post:
1352-
<a href="/post/two-var-recursive-func/">Cracking Multivariate Recursive Equations Using Generating Functions</a>.</p>
1349+
<p>In this post, we return back to the combinatorial problem discussed in <a href="/post/intro-to-dp/">Introduction to Dynamic Programming and Memoization</a> post.
1350+
We will show that generating functions may work great not only for single variable case (see <a href="/post/gen-func-art/">The Art of Generating Functions</a>),
1351+
but also could be very useful for hacking two-variable relations (and of course, in general for multivariate case too).</p>
1352+
<p>For making the post self-contained, we repeat the problem definition here.</p>
1353+
<h2 id="the-problem">The Problem</h2>
1354+
<blockquote>
1355+
<p>Compute the number of ways to choose $m$ elements from $n$ elements such that selected elements in one combination are not adjacent.</p>
13531356
</div>
13541357
</a>
13551358

@@ -1373,7 +1376,7 @@ <h2 id="the-problem">The Problem</h2>
13731376

13741377

13751378

1376-
Jul 5, 2017
1379+
Jul 6, 2017
13771380
</span>
13781381

13791382

@@ -1382,7 +1385,7 @@ <h2 id="the-problem">The Problem</h2>
13821385

13831386
<span class="middot-divider"></span>
13841387
<span class="article-reading-time">
1385-
4 min read
1388+
3 min read
13861389
</span>
13871390

13881391

0 commit comments

Comments
 (0)