まずはREDUCE関数についてのMicrosoftの公式ドキュメントやその他の解説記事を見てみましょう.多くの記事は基本的なことしか書いていないですね......
というわけで自分が普段数式を組む時に考えている事の理解の整理もかねて,REDUCE関数の使い方をまとめておこうと思います.
この記事では,本来REDUCE関数を使わずに簡潔に記述できることであっても,(敢えて)REDUCE関数を使って書かれている場合があります.例や数式の直前に記号$\color{red}{\bigodot}$を置くことでその目印とします.
REDUCE関数を使わずに記述するにはどうすれば良いのかを考えてみても楽しいかもしれません.
白状するとREDUCE関数はPython のreduce関数と同じなのですが,REDUCE関数はExcel上でより高度な計算を可能にする関数です.構文は次のようになっています.
=REDUCE([initial_value], array, LAMBDA(アキュムレータ, 値, 本文))
REDUCE関数は繰り返し処理に特化した関数です.1回目の処理では,「initial_value」と「array」の1番目の値がそれぞれ「アキュムレータ」と「値」に代入されます.それを元に「本文」が計算され,「アキュムレータ」に代入されます.
$i(>1)$回目の処理では「array」の$i$番目の値が「値」に代入され,「アキュムレータ」($i-1$回目の処理の結果)と「値」とを用いて「本文」が計算され再び「アキュムレータ」に代入されるというわけです.
処理がすべて終わったときの「アキュムレータ」の値がREDUCE関数の戻り値になります.
とまあこんなふうに書いているのを読んでもよく分からないと思うので,簡単な例で計算してみましょう.
以降では数式の戻り値を
-->
で表すことにします.
まずは簡単な例を挙げます.
次の式はは$0$を初期値として$1,2,3,4,5$を順に足す計算をしています.
=REDUCE(0, {1, 2, 3, 4, 5}, LAMBDA(a, b, a + b))
--> 15
また,「array」には任意の数式を入れることができ,縦配列にも対応しています.次の式はどちらも,$0$を初期値として$1,2,3,4,5$を順に足す計算をしています.
=REDUCE(0, {1; 2; 3; 4; 5}, LAMBDA(a, b, a + b))
--> 15
=REDUCE(0, SEQUENCE(5), LAMBDA(a, b, a + b))
--> 15
REDUCE関数の第$2$引数は「array」としていますが,配列の要素が$1$つしかないばあいにはこういう書き方もできます.
=REDUCE(3, 2, LAMBDA(a, b, a + b))
--> 5
そして「アキュムレータ」と「値」として指定する変数名には,ワイルドカード文字と演算子を除く任意の文字列が使用可能です.
=REDUCE(0, SEQUENCE(5), LAMBDA(あ, いう, あ + いう))
--> 15
「array」に$2$次元配列を指定するときには少し注意が必要です.たとえば
{{1, 2}; {3, 4}}
を指定した場合,実はこれではExcelがうまく読み取ってくれません.
{1, 2; 3, 4}
あるいはVSTACK関数とHSTACK関数を利用して
VSTACK(HSTACK(1, 2), HSTACK(3, 4))
とする必要があります.しかし,これだと困るケースがあるのです.つまり,$1$回目の処理で「値」に「{1, 2}」が,$2$回目では「{3, 4}」が代入されていてほしいときです.
これの解決策については,REDUCE関数に慣れた頃に
REDUCE関数のネストと変数宣言
で述べます.
次の式は$6$の正の約数を横に並べる数式です.
=REDUCE(1, SEQUENCE(5, , 2), LAMBDA(a, b, IF(MOD(6, b), a, HSTACK(a, b))))
--> {1, 2, 3, 6}
一気に複雑になりましたね.処理を順に追ってみましょう.
$1$回目の処理ではまず,a に 1 が代入され,b に 2 が代入されて計算が行われます.MOD(6, 2) の戻り値は 0 ですから,HSTACK(1, 2) が計算され,その戻り値である {1, 2} が a に代入されます.
$2$回目の処理では,b に 3 が代入され,MOD(6, 3) の戻り値は 0 なので HSTACK({1, 2}, 3) の戻り値である {1, 2, 3} が a に代入されます.
$3$回目の処理では,b に 4 が代入され,MOD(6, 4) の戻り値は 2 ですから,a の値である {1, 2, 3} が a に代入されます.
これを続けることで処理終了時に a には {1, 2, 3, 6} が代入された状態になり,戻り値は {1, 2, 3, 6} となるのです.
ところで上の例では,第$2$引数の SEQUENCE(5, , 2) は n = 6 とすれば SEQUENCE(n - 1, , 2) です.仮に n = 1 が指定された場合,SEQUENCE(0, , 2) はエラーとなり少し困りますね.
実はREDUCE関数の第$1$引数は省略可能で,省略した場合には第$2$引数の$1$つ目は第$1$引数扱いになり,$1$回目の処理はスキップされます.正の約数には必ず$1$が含まれていますから,次のようにすっきりと書くことができます.
=REDUCE(, SEQUENCE(6), LAMBDA(a, b, IF(MOD(6, b), a, HSTACK(a, b))))
--> {1, 2, 3, 6}
=REDUCE(0, SEQUENCE(6), LAMBDA(a, b, IF(MOD(6, b), a, a + b)))
--> 12
考え方は例2のものと同じです.
REDUCE関数がどういう動きをするのか,だいたいわかったと思います.
ここではREDUCE関数がどのように応用できるかを見ていきます.漸化式の計算が中心になりますが,これはREDUCE関数に慣れるのにちょうど良いのです.なぜかというと,漸化式とはある特定の操作による状態の遷移を表した式に他ならないからです.
REDUCE関数の登場以前では,漸化式の計算はセルの参照を利用した数式のオートフィルによって行うのが一般的でした.ところがREDUCE関数の登場によって数式$1$つでの計算が可能になったのです.
簡単な例で見ていきましょう.
漸化式
$$
a_{n + 1} = 3a_n + 1, \ \ a_1 = 1
$$
によって定まる数列をREDUCE関数で計算してみましょう.$\color{red}{\bigodot}$
=REDUCE(1, SEQUENCE(5), LAMBDA(a, b, 3 * a + 1))
--> 364
これは数列の第$6$項目を計算しています.今までは「値」が計算に直接関わっていましたが,今回はカウンタとして使われていますね.
もし数列を文字通り列として取得したいなら,少し工夫が必要です.
=REDUCE(1, SEQUENCE(5), LAMBDA(a, b, HSTACK(a, 3 * TAKE(a, , -1) + 1)))
--> {1, 4, 13, 40, 121, 364}
TAKE関数で次の項の計算に必要な部分を取り出しています.
もう少し複雑な例を考えてみます.
漸化式
$$
a_{n + 1} = a_n + n^2, \ \ a_1 = 1
$$
によって定まる数列をREDUCE関数で計算してみましょう.
=REDUCE(1, SEQUENCE(5), LAMBDA(a, b, a + b^2))
--> 56
これは数列の第$6$項目を計算しています.今回はカウンタである「値」が計算に関わっていますね.
第$6$項目までを横に並べたいなら,例4と同じようにすればよいでしょう.
=REDUCE(1, SEQUENCE(5), LAMBDA(a, b, HSTACK(a, TAKE(a, , -1) + b^2)))
--> {1, 2, 6, 15, 31, 56}
単純な漸化式なら,すべてこの考え方で計算できます.
REDUCE関数を使った素数判定にはいくつか方法がありますが,今回は「$2$から$n - 1$までの自然数で割り切れるか試す」という古典的な方法を紹介します.
数式の仕様を確認しておきましょう.
Excelにはbreak文はありませんが,代わりにcontinue文のような処理を入れることができます.察しの良い方は例2で気づいていることでしょう.「アキュムレータ」に現在の「値」の状態を保持させておくことで,状態の判定によってその後の処理内容を切り替えられるのです.
より具体的にはこのようにします.$\color{red}{\bigodot}$
=REDUCE("素数", SEQUENCE(n - 2, , 2),
LAMBDA(a,b,
IF(a = "合成数",
a,
IF(MOD(n, b), a, "合成数")
)
)
)
これで,たとえば$n = 101$を渡したときに
=LAMBDA(n, REDUCE("素数", SEQUENCE(n - 2, , 2),
LAMBDA(a,b,
IF(a = "合成数",
a,
IF(MOD(n, b), a, "合成数")
)
)
))(101)
--> "素数"
となります.
このような考え方は処理が重くなるほどに威力を発揮します.
ここまでの使用例で,アキュムレータは「現時点での計算結果」あるいは「現時点での状態」を格納するだけのただの変数でした.もう少し流動的な扱いをすることはできないでしょうか.
実のところ,「アキュムレータは処理のためのテーブルであり,戻り値にはテーブルの一部を使うものだ」と考えた方が実装の際に見通しが良かったりします.次の例を見てみましょう.
$2$以上の自然数$p, n$が与えられたときに,$n$が$p$で何回割り切れるのかをREDUCE関数で計算してみましょう.$\color{red}{\bigodot}$
=CHOOSEROWS(REDUCE(VSTACK(0, n), SEQUENCE(LOG(n, p) + 1),
LAMBDA(a, b,
IF(MOD(CHOOSEROWS(a, 2), p),
a,
a / VSTACK(1, p) + VSTACK(1, 0)
)
)
), 1)
少し複雑になってしまいましたね.順に説明します.
まず「array」の部分では$1$から$\log_pn + 1$までの自然数を指定しています.$1$を足しているのは,$\log_pn$が$1$未満になったときのエラー回避のためです.
そして肝心のアキュムレータの部分ですが,これは {割った回数; 残っている数} を保持するようにしています.はじめは {0; n} ですが,$2$行目が$p$で割り切れると判定されると,$2$行目を$p$で割り,$1$行目に$1$を加えます.
このようにアキュムレータには,戻り値だけでなく計算に使う複数の情報を保持させておく使い方もあるのです.
ここでは具体的には書きませんが,REDUCE関数の中で配列を並べ替えるといった操作をしたときに,ある行がどこから移動してきたのかを追跡したいということがあります.こういうときに各行にラベルのような識別子を付しておいて,並べ替え後に識別子を参照するという方法があり,それはこの考え方によるものです.
さて,ここまでの知識を使えば, 漸化式の計算 の節で扱ったものよりも複雑な漸化式を計算することができるようになります.
漸化式
$$
S_{n + 1} = \sum_{k = 1}^n (n - k + 1)S_k, \ \ S_1 = 1
$$
によって定まる数列をREDUCE関数で計算してみましょう.考え方は例4と同じです.
以下は最初の$6$項を計算しています.
=REDUCE(1, SEQUENCE(5), LAMBDA(a, b, HSTACK(a, SUM(SEQUENCE(, b, b, -1) * a))))
--> {1, 1, 3, 8, 21, 55}
漸化式
$$
a_n = a_{n - 1} + 2a_{n - 2}, \ \ a_0 = 1, a_1 = 1
$$
によって定まる数列をREDUCE関数で計算してみましょう.新しい項の計算には直前の$2$項が必要なので,この$2$つだけをアキュムレータに保持しておけば良さそうです.
以下は第$n$項目を計算する式です.繰り返し回数に注意しましょう.
=LAMBDA(n, IF(n <= 1, 1,
CHOOSEROWS(REDUCE(VSTACK(1, 1), SEQUENCE(n - 1),
LAMBDA(a, b, VSTACK(CHOOSEROWS(a, 2), SUM(VSTACK(2, 1) * a))
)
), 2)
))
あるいは行列演算を利用してこのようにしても良いでしょう.
=LAMBDA(n, IF(n <= 1, 1,
CHOOSEROWS(REDUCE(VSTACK(1, 1), SEQUENCE(n - 1),
LAMBDA(a, b, MMULT(VSTACK(HSTACK(0, 1), HSTACK(2, 1)), a)
)
), 2)
))
漸化式
$$
C_{n, r} = C_{n - 1, r - 1} + C_{n - 1, r}, \ \ C_{n, 0} = C_{n, n} = 1 \ \ (\forall n)
$$
によって定まる数列をREDUCE関数で計算してみましょう.これはREDUCE関数を使った漸化式の計算に慣れていないと難しいです.
まずは計算の方針を立てましょう.漸化式から,$C_{n, r}$を求めるには$C_{n - 1, r - 1}, C_{n - 1, r}$の値を知っておかなければなりません.では$C_{n - 1, r - 1}, C_{n - 1, r}$を求めるには? と考えていくと,アキュムレータに保持するテーブルは次のような構造にすれば良いとわかるでしょう.
| $-$ | $0$ | $1$ | $2$ | $\cdots$ | $r - 1$ | $r$ |
|---|---|---|---|---|---|---|
| $1$ | $1$ | $1$ | $1$ | $\cdots$ | $1$ | $1$ |
| $2$ | $1$ | $C_{2, 1}$ | $C_{3, 2}$ | $\cdots$ | $C_{r, r - 1}$ | $C_{r + 1, r}$ |
| $3$ | $1$ | $C_{3, 1}$ | $C_{4, 2}$ | $\cdots$ | $C_{r + 1, r - 1}$ | $C_{r + 2, r}$ |
| $\vdots$ | $\vdots$ | $\vdots$ | $\vdots$ | $\ddots$ | $\vdots$ | $\vdots$ |
| $n - r$ | $1$ | $C_{n - r, 1}$ | $C_{n - r + 1, 2}$ | $\cdots$ | $C_{n - 2, r - 1}$ | $C_{n - 1, r}$ |
| $n - r + 1$ | $1$ | $C_{n - r + 1, 1}$ | $C_{n - r + 2, 2}$ | $\cdots$ | $C_{n - 1, r - 1}$ | $\textcolor{blue}{C_{n, r}}$ |
こんなふうに書いていますが,実際には横$1$列に引き伸ばした配列で考えます.最右端に追加する項はというと,上の表と漸化式から,$1$つ左の要素と$r+1$つ左の要素から計算されます.初期値は EXPAND(1, , r + 1, 1) として,繰り返し回数は$(r + 1)(n - r)$としましょう.よって,はじめに$n = r$か否かの場合分けをします.配列ができあがったら,最後に最右端を取り出します.
=LAMBDA(n, r, IF(n = r, 1,
CHOOSECOLS(
REDUCE(EXPAND(1, , r + 1, 1), SEQUENCE((r + 1) * (n - r), , 0),
LAMBDA(a, b, IF(MOD(b, r + 1) = 0,
HSTACK(a, 1),
HSTACK(a, SUM(CHOOSECOLS(a, -1, -r-1)))
))
),
-1)
))
漸化式
$$
S^2_{n, r} = S^2_{n - 1, r - 1} + rS^2_{n - 1, r}, \ \ S^2_{n, 0} = 0, \ \ S^2_{n, n} = 1, \ \ S^2_{0, 0} = 1 \ \ (\forall n)
$$
によって定まる数列をREDUCE関数で計算してみましょう.考え方は例9のものと変わりません.
=LAMBDA(n,r, IF(n = r, 1,
CHOOSECOLS(REDUCE(EXPAND(1, , r + 1, 1), SEQUENCE((r + 1) * (n - r), , 0),
LAMBDA(a,b, IF(MOD(b, r + 1) = 0,
HSTACK(a, 0),
HSTACK(
a,
SUM(HSTACK(1, MOD(b, r + 1)) * CHOOSECOLS(a, -1, -r-1))
)
))
), -1)
))
さて,ここまで来るとREDUCE関数のことが大好きになったことでしょう.REDUCE関数を虐め倒したくなってきます.そうです.ネストです.
ネストをするとどんな良いことがあるのか? それは,計算できる漸化式が増える! 保持する配列が小さくなる! ということに尽きます.
ではどういうときにネストするのかというと,「新しい配列を別の配列を参考に作っていく必要がある.でも別の配列も更新しなきゃいけない」というようなときです.また,「どうしても『値』に配列を代入したい」というときには自然とネストが必要であることが多いです.
例9の計算では,新しい値が最右端に出てくるのに対し,配列の左側は早いうちに計算とは関係の無い余分なものとなってしまいます.実際,計算に本当に必要なのは例9の表のうち,次の$2$行だけです.
| $-$ | $0$ | $1$ | $2$ | $\cdots$ | $r - 1$ | $r$ |
|---|---|---|---|---|---|---|
| $i - 1$ | $1$ | $C_{i - 1, 1}$ | $C_{i, 2}$ | $\cdots$ | $C_{i + r - 3, r - 1}$ | $C_{i + r- 2, r}$ |
| $i$ | $1$ | $C_{i, 1}$ | $C_{i + 1, 2}$ | $\cdots$ | $C_{i + r - 2, r - 1}$ | $\textcolor{blue}{C_{i + r - 1, r}}$ |
このアイデアで実装を考えていく場合,$1$行目の配列を参考に$2$行目の配列を作り,それを新しい$1$行目の配列として更新していくことになります.しかし,このような配列の要素の追加に対してはExcelはサポートしてくれていません(一応やってやれないことはないですがここでは割愛します).そこで,$1$行目のみをアキュムレータに保持しておき,REDUCE関数の再帰処理によって作った$2$行目をアキュムレータに代入するのです.
具体的にはこうします.
=LAMBDA(n, r, IF(n = r, 1, IF(r = 0, 1,
CHOOSECOLS(REDUCE(EXPAND(1, , r + 1, 1), SEQUENCE(n - r),
LAMBDA(a, b,
REDUCE(1, SEQUENCE(r),
LAMBDA(c, d,
HSTACK(c, CHOOSECOLS(c, d) + CHOOSECOLS(a, d + 1))
)
)
)
), -1)
)))
ここでもやはり,繰り返し回数に注意します.また,戻り値がどの変数に代入されるのかを意識して組むと良いでしょう.
次の例はかなり特殊な状況を考えます.
$n \times 3$の配列が与えられ,「与えられた配列の各行に対して,その$3$つの数値のうち,偶数が奇数より多いなら$4$列目として$0$を加え,そうでないなら$4$列目として$1$を加える」という操作をREDUCE関数で実現してみましょう.
これはまさに,「どうしても『値』に配列を代入したい」というときです.ところが実際には「値」に配列を代入させることはできません.そこで,「値」に配列の行数を代入させることで,間接的に配列の各行を取得します.
=LAMBDA(array, DROP(REDUCE(0, SEQUENCE(ROWS(array)),
LAMBDA(a, b,
VSTACK(a, HSTACK(CHOOSEROWS(array, b),
CHOOSEROWS(REDUCE(VSTACK(0, 0), CHOOSEROWS(array, b),
LAMBDA(c, d, IF(CHOOSEROWS(c, 2) = 2,
1,
IF(CHOOSEROWS(c, 1) = 2, 0, IF(MOD(d, 2),
c + VSTACK(0, 1),
c + VSTACK(1, 0)
))
))
), 1)
))
)
), 1))
REDUCE関数のネストをするようになると,自然とこのようなことを思うでしょう.つまり,「REDUCE関数をLET関数のように使えないだろうか」と.もちろんそれは可能で,LET関数が第$1$引数に第$2$引数を格納するように,REDUCE関数の第$1$引数をLAMBDA関数の第$1$引数に代入すれば良いのです.つまりこういうことです.
=...REDUCE(なにか複雑な計算, scalar, LAMBDA(result, t, 処理))...
しかしこの方法はLET関数では可能だったLAMBDA式の代入ができず,しかも最大でも$2$つずつしか変数の設定をすることができないため式が長大になりがちで可読性も最悪です.使用は本当に必要な箇所だけに留めましょう.
実はもう一つ,変数には少しトリッキーな使い方があります.次の例を見てください.
=REDUCE(0, {1, 2}, LAMBDA(a, b, REDUCE(b, {1, 2, 3, 4}, LAMBDA(a, b, a + b))))
--> 12
=REDUCE(0, {1, 2}, LAMBDA(a, b, a + REDUCE(b, {1, 2, 3, 4}, LAMBDA(a, b, a + b))))
--> 23
変数 a と b が一時的に上書きされていることがわかります.これだけでは可読性は最悪ですが,上のLET関数代わりの使い方と併用することで,ある程度は可読性の回復が望まれます.すなわち,REDUCE関数とLAMBDA関数の第$1$引数を揃えるのです.
ところで,致命的なことにExcelは大文字と小文字の区別に積極的ではありません.このことはちょっと困ったことを引き起こします.次の数式をセルに入力してみましょう.
=REDUCE(0, SEQUENCE(4), LAMBDA(a, Bあc, REDUCE(0, 1, LAMBDA(A, BあC, A + a + Bあc))))
--> 1
あれれ? おかしいですね.本当なら 10 が返ってくるはずです.計算後の数式をもう一度見てみるとこうなっています.
=REDUCE(0, SEQUENCE(4), LAMBDA(a, Bあc, REDUCE(0, 1, LAMBDA(A, BあC, A + A + BあC))))
これでは 1 が返るのも納得です.
このように,困ったことにExcelは一番内側のスコープで指定された変数を優先的に使用して勝手に数式を変更してしまうのです.これはおそらく,Excelの数式入力中の入力候補表示に代表されるユーザー向けの補助機能によるものだと推測されますが,正確な処理実装が必要な場面においては迷惑な仕様です.
この章のはじめで言ったように,漸化式とはある特定の操作による状態の遷移を表した式に他なりません.つまり,今まで漸化式は数列を表すための式として表されていましたが,この数列とはなにも数値の列でなくてもよいのです.
計算例を挙げるにあたり,これまでとは抽象度が格段に上がっていますので説明に窮することが多くなります.正直なところ,この節は読み飛ばしてもらっても構いません.
漸化式
$$
S^1_{n, r} = S^1_{n - 1, r - 1} + (n - 1)S^1_{n - 1, r}, \ \ S^1_{n, 0} = 0, \ \ S^1_{n, n} = 1, \ \ S^1_{0, 0} = 1 \ \ (\forall n)
$$
によって定まる数列をREDUCE関数をネストして計算してみましょう.考え方は例11と同様です.
=LAMBDA(n, r, IF(n = r, 1, IF(r = 0, 0,
CHOOSECOLS(REDUCE(EXPAND(1, , r + 1, 1), SEQUENCE(n - r),
LAMBDA(a, b, REDUCE(0, SEQUENCE(r),
LAMBDA(c, d,
HSTACK(c, CHOOSECOLS(c, -1) + (b + d - 1) * CHOOSECOLS(a, d + 1))
)
))
), -1)
)))
これを例10と同様に表を横に引き伸ばして計算しようとすると,途端に難しくなることがわかるでしょうか.
正の整数$n$が与えられるので,$n$枚ハノイの塔の最短移動手順を示す配列を,REDUCE関数で計算してみましょう.
まず考えることは戻り値をどうするかです.今回は次のようにします($n = 2$のとき).
| $1$ | $2$ | $3$ |
|---|---|---|
| $0$ | $2$ | $0$ |
| $1$ | $1$ | $0$ |
| $1$ | $0$ | $1$ |
| $0$ | $0$ | $2$ |
次に漸化式をどうするか考えます.すなわち,上の例では$n = 2$の表から$n = 3$の表を作るにはどういう操作が必要かを考えます.列の並べ替えで最短手順が本質的に不変であることを利用すれば良さそうですね.
=LAMBDA(n, LET(ini, VSTACK(HSTACK(0, 1, 0), HSTACK(0, 0, 1)),
IF(n = 1, ini,
REDUCE(ini, SEQUENCE(n - 1),
LAMBDA(a, b,
VSTACK(a --(MOD(SEQUENCE(ROWS(a), 3), 3) = 2),
IF(MOD(b, 2),
CHOOSECOLS(a, 3, 1, 2) --(MOD(SEQUENCE(ROWS(a), 3), 3) = 1),
CHOOSECOLS(a, 2, 3, 1) --(MOD(SEQUENCE(ROWS(a), 3), 3) = 0)
)
)
)
)
)
))
数論的関数$f$と正の整数$n$が与えられるので,$\displaystyle g(m) := \sum_{d\vert m, 0 < d} f(d) \ \ (\forall m)$によって定まる関数$g$に対して,$g(1), \cdots, g(n)$を列挙する数式をREDUCE関数で組んでみましょう.
ここで,$\displaystyle \sum_{d\vert m, 0 < d}$は$m$の正の約数をわたる和を表します.
あらかじめ$f(1), \cdots, f(n)$の値を用意しておくことで,計算量を少しでも減らしておきます.
=LAMBDA(func, n, DROP(
REDUCE(DROP(REDUCE(0, SEQUENCE(n), LAMBDA(a, b, HSTACK(a, func(b)))), , 1),
SEQUENCE(n),
LAMBDA(a, b,
HSTACK(a, SUM(CHOOSECOLS(a,
REDUCE(, SEQUENCE(b), LAMBDA(c, d, IF(MOD(b, d), c, HSTACK(c, d))))
)))
)
), , n)
)
あるいは,$f(1), \cdots, f(n)$の値を処理の外部に保存する方法もあります.
=LAMBDA(func, n, LET(
list, DROP(REDUCE(0, SEQUENCE(n), LAMBDA(a, b, HSTACK(a, func(b)))), , 1),
DROP(REDUCE(0, SEQUENCE(n),
LAMBDA(a, b, HSTACK(a,
SUM(CHOOSECOLS(list, REDUCE(, SEQUENCE(b), LAMBDA(c, d, IF(MOD(b, d), c, HSTACK(c, d))))))
))
), , 1)
))
正の整数$n, r$が与えられるので,$n$個の中から$r$個選ぶすべての組み合わせをREDUCE関数で計算してみましょう.
組み合わせがいくつあるか,ではありません.組み合わせそのものにどのようなものがあるのか,を計算するのです.今回は戻り値は次のようにします($n = 4, r = 2$のとき).
| $1$ | $2$ | $3$ | $4$ |
|---|---|---|---|
| $1$ | $1$ | $0$ | $0$ |
| $1$ | $0$ | $1$ | $0$ |
| $1$ | $0$ | $0$ | $1$ |
| $0$ | $1$ | $1$ | $0$ |
| $0$ | $1$ | $0$ | $1$ |
| $0$ | $0$ | $1$ | $1$ |
肝心の実装方法ですが,いくつかあるものの,どれも難しいので$1$つ紹介するだけにとどめておきます.
以下はその式です.計算方法自体はパスカルの三角形を利用するものになりますが,Excelは多次元配列や辞書型を扱えませんので,例9のような表のインデックスをラベルとして付しておいて後で取り出せるようにしています.
=LAMBDA(n, r, IF(n <= r, EXPAND(1, , n, 1),
LET(label, LAMBDA(array, number_1, number_2, HSTACK(TAKE(array, , n), HSTACK(number_1, number_2) * EXPAND(1, ROWS(array), , 1))),
add, LAMBDA(array, number, array --(MOD(SEQUENCE(ROWS(array), n + 2), n + 2) = n - number)),
ini, REDUCE(EXPAND(0, , n + 2, 0), SEQUENCE(r), LAMBDA(a, b, VSTACK(a, HSTACK(EXPAND(EXPAND(0, , n - b, 0), , n, 1), 0, b)))),
REDUCE(
REDUCE(ini, SEQUENCE(n - r), LAMBDA(a, b,
REDUCE(
REDUCE(a, SEQUENCE(r), LAMBDA(a, c,
VSTACK(a, label(VSTACK(
add(FILTER(a, (CHOOSECOLS(a, -2) = IF(c = 1, 0, b)) * (CHOOSECOLS(a, -1) = c - 1)), b + c - 1),
FILTER(a, (CHOOSECOLS(a, -2) = b - 1) * (CHOOSECOLS(a, -1) = c))
), b, c)
)
)),
1, LAMBDA(array, c, FILTER(array, (CHOOSECOLS(array, -2) >= b - 1) + (CHOOSECOLS(array, -2) = 0)))
)
)),
1, LAMBDA(array, b,
TAKE(FILTER(array, (CHOOSECOLS(array, -2) = n - r) * (CHOOSECOLS(array, -1) = r)), , n)
))
)
))
実はこの数式に少し手を加えることによって,labelやFILTER関数を使わずにより速い処理をすることができるのですが,さらに複雑になってしまうのでここでは触れず,勇気ある読者への課題とします.
REDUCE関数を使う練習問題を考えてみました.
模範解答を載せるつもりはありません.正誤判定は各自で行ってください.
また,徐々に難易度が上がるようにしたつもりですが,ひょっとすると人によっては違うかもしれないということに注意してください.
正の整数$m, n$が与えられるので,$m, n$の最大公約数と最小公倍数をユークリッドの互除法を使って計算する数式を組んでください.
正の整数$n$が与えられるので,$n$以下の正の整数であって,$n$との最大公約数が$1$であるものの個数を計算する数式を組んでください.
正の整数$n$が与えられるので,$n$以下の素数の個数を計算する数式を組んでください.
正の整数$n$が与えられるので,エラトステネスの篩を使って$n$以下の素数を列挙する数式を組んでください.ただし,FILTER関数を使った場合は,使わない方法を$2$通り考えてみましょう.
$2$以上の整数$n$が与えられるので,$n$を素因数分解する数式を組んでください.
写像$D\colon \mathbb{Z}_{\geq 0} \to \mathbb{Z}_{\geq 0}$は以下をみたすとします.
$$
\begin{eqnarray}
\left\{
\begin{array}{l}
D(0) = D(1) = 0, \\
D(p) = 1 \ \ (\text{$p$は素数}), \\
D(ab) = aD(b) + bD(a)
\end{array}
\right.
\end{eqnarray}
$$
正の整数$n$が与えられるので,$D(n)$を計算する数式を$2$通りの方法で組んでください.
各成分が実数の$1 \times n$配列$A$が与えられるので,$A$から重複をゆるしてとった$2$つの実数の積$n^2$個を列挙し,その総和を計算する数式を組んでください.ただし,それぞれに対してREDUCE関数を使う方法とそうでない方法の$2$通りを考えてください.
整数環$\mathbb{Z}$は単項イデアル整域であることが知られています.正の整数$n$が与えられるので,$n$によって生成される$\mathbb{Z}$のイデアル$I := \langle n\rangle$に対して$\langle m\rangle = \sqrt I$をみたす正の整数$m$を計算する数式を組んでください.
余裕のある人はもう$1$通りの方法を考えてみましょう.
Excel数式によって実装された数論的関数$f$と正の整数$n$が与えられるので,$\displaystyle f(m) = \sum_{d \vert m, 0 < d} g(d) \ \ (\forall m)$をみたすような関数$g$に対して$g(n)$を計算する数式を組んでください.
整数の分割を考えます.$4$の分割は
\begin{align}
4 &= 1 + 1 + 1 + 1 \\
&= 1 + 1 + 2 \\
&= 2 + 2 \\
&= 1 + 3 \\
&= 4
\end{align}
の$5$通りあります.
正の整数$n$が与えられるので,$n$の分割の仕方を列挙する数式を組んでください.また,FILTER関数を使った場合は使わない方法を考えてください.
余裕のある人はもう$1$通りの方法を考えてみましょう.
素数$p$と正の整数$n$が与えられるので,有限体$\mathbb{F}_p$上の可約多項式であって,次数が$n$以下であるものの個数を計算する数式を$2$通りの方法で組んでください.
正の整数$n, k$が与えられるので,$n$個から重複をゆるして$k$個選ぶ組み合わせを列挙する数式を組んでください.
正の整数$n$と非負整数$k$が与えられるので,$n$の正の約数すべてを根にもつ整数係数最小多項式$f(x)$の$k$次の項の係数を計算する数式を$3$通りの方法で組んでください.
各成分が非負整数の$1 \times n$配列$A$が与えられるので,これを$i$番目の山に石が$A_{1, i}$個あるとみて,正規形$n$山ニムのグランディ数を計算する数式を組んでください.ただし,計算にはよく知られたグランディ数の周期性や和公式は使わず,定義通りに計算してください.
$1$回の着手で取れる石の最大数を可変にできるとなお良いですね.
例2,4,5,6,8は実はVSTACK関数やHSTACK関数を使わずに数式を組むことができます.どうすれば良いか考えてください.
ただし,使ってよいLAMBDAヘルパー関数はREDUCE関数のみで,$1$つの数式につき$1$つまでです.