母関数を使って括弧のパターンを求めてみた
n個の対応する括弧のパターンの数を母関数を使って求めてみました。
これはid:nobsunさんが指摘なさっているようにカタラン数という数になるんですが、そういう前提知識なしで機械的に解きたいような場合に使えます。
いろんな教科書があると思いますが、私は以下の本で学びました。
参考書籍:
- 作者: ロナルド・L.グレアム,オーレンパタシュニク,ドナルド・E.クヌース,Ronald L. Graham,Oren Patashnik,Donald E. Knuth,有沢誠,萩野達也,安村通晃,石畑清
- 出版社/メーカー: 共立出版
- 発売日: 1993/08
- メディア: 単行本
- 購入: 5人 クリック: 224回
- この商品を含むブログ (38件) を見る
T = 1 + () + ()() + (()) + ()()() + ()(()) + (())() + (()()) + ((())) + ()()()() + ...
ここで括弧一組をxと置き換えてみると、
T = 1 + x + x^2 + x^2 + x^3 + x^3 + x^3 + x^3 + x^3 + x^4 + ... = 1 + x + 2 x^2 + 5 x^3 + 14 x^4 + ...
という級数になりますが、このx^nの各係数が求める答えであるという事が分かるかと思います。このように求める数列を係数としてもつ関数を母関数といいます。この母関数は式の再帰的な構造に注目すると簡単に求める事ができます。
では順番にやっていきます。
とりあえず、xに置き換える前の式に戻ります。
T = 1 + () + ()() + (()) + ()()() + ()(()) + (())() + (()()) + ((())) + ()()()() + ...
ここで途中で複数に分割できるパターン("()()"とか"()(())"とか)を(交換出来ない)掛け算だと思って、左端をくくり出すと
T = 1 + () {1 + () + ()() + (()) + ()()() + ...} + (()) {1 + () + ()() + (()) + ()()() + ...} + (()()) {1 + () + ()() + ...} + ...
くくった結果再びTが現れたので整理すると
T = 1 + () T + (()) T + (()()) T + ...
となります。今度はTをくくると
T = 1 + {() + (()) + (()()) + ...} T
となります。ここで残った中括弧の中身はTの各項を括弧で囲んだ物であることがわかります。つまり
() + (()) + (()()) + ... = '(' {1 + () + ()() + (()) + ...} ')'
となってますから、以下のシンプルな式を得ます。
T = 1 + '(' T ')' T
'...'がなくなったら括弧をxに置き換えます。この置き換えでx^nの係数は変わらない事が分かると思います。
T = 1 + x T^2
これは2次方程式ですから解の公式で解けて
T = (1 - sqrt(1- 4 x)) / (2 x)
と母関数が求まります。コイツのx^nの係数が求める答えです。
一応こいつの無限級数展開をWolfram Alphaで聞いてみると
確かに各係数(1,1,2,5,14,42,..)が求める答えになっている事がわかります。この係数の一般項を具体的求めると、それがカタラン数である事が分かるのですがWolfram Alphaはそこまでは教えてくれませんでした。
一般項を求めないで、漸化式を導くという事をやっても良いかと思います。
一つ前の式に戻ります。
T = 1 + x T^2
これから漸化式が作れます。n組の括弧からなるパターンの数をT(n)とおくと
T(n) = T(0) + T(1) x + T(2) x^2 + T(3) x^3 + T(4) x^4 + ...
となってる訳ですから、両辺を展開してx^nの係数だけ比較してみると、n > 0の時は
T(n) = T(0) T(n-1) + T(1) T(n-2) + ... + T(n-1) T(0)
となります。カタラン数の漸化式を機械的に求める事が出来ました。
参考までにですが、右辺の形は畳込みと言って「母関数の積(T^2)の係数 = 母関数の係数の畳込み」という有名な性質です。他にも微分・積分やシフト演算などを用いる母関数についての公式がありますので、それを覚えとくとこういった計算が素早くできます。
さて、「漸化式」があるならば「動的計画法」が定石です。rubyで書いてみるとこんな感じになります。(n=100まで求めてみます)
t = [] t[0] = 1 (1..100).each do |n| x = 0 (0..n-1).each do |k| x += t[k] * t[n-k-1] end t[n] = x puts t[n] end
%nineties% time ruby kakko.rb 1 2 5 14 42 132 429 1430 4862 16796 58786 208012 742900 2674440 ... 中略 ... 896519947090131496687170070074100632420837521538745909320 ruby kakko.rb 0.03s user 0.01s system 77% cpu 0.057 total
文字列集合を得る
ここでも上で求めた式が使えます
T = 1 + '(' T ')' T
Haskellでやってみようと思いますが、右辺に無限リストが2つあるので漸化式に直しとく必要があります。
T(0) = 1 T(n) = '(' T(0) ')' T(n-1) + '(' T(1) ')' T(n-2) + ... + '(' T(n-1) ')' T(0)
これを立式すると次のようになります。
pattern 0 = [""] pattern n = [concat["(",l,")",r] | i <- [0..n-1], l <- pattern i, r <- pattern (n-i-1)]
もしくは無限リストとして自分自身を参照するようにするとこんな感じになります。
pattern = [""] : [[concat["(",l,")",r] | i <- [0..n-1], l <- pattern !! i, r <- pattern !! (n-i-1)] | n <- [1..]]
patternは括弧のつけ方の無限リストになっています。
%ghci GHCi, version 6.12.1: http://www.haskell.org/ghc/ :? for help Loading package ghc-prim ... linking ... done. Loading package integer-gmp ... linking ... done. Loading package base ... linking ... done. Prelude> :l kakko.hs [1 of 1] Compiling Main ( kakko.hs, interpreted ) Ok, modules loaded: Main. *Main> take 7 pattern [[""],["()"],["()()","(())"],["()()()","()(())","(())()","(()())","((()))"],["()()()()","()()(())","()(())()","()(()())","()((()))","(())()()","(())(())","(()())()","((()))()","(()()())","(()(()))","((())())","((()()))","(((())))"],["()()()()()","()()()(())","()()(())()","()()(()())","()()((()))","()(())()()","()(())(())","()(()())()","()((()))()","()(()()())","()(()(()))","()((())())","()((()()))","()(((())))","(())()()()","(())()(())","(())(())()","(())(()())","(())((()))","(()())()()","(()())(())","((()))()()","((()))(())","(()()())()","(()(()))()","((())())()","((()()))()","(((())))()","(()()()())","(()()(()))","(()(())())","(()(()()))","(()((())))","((())()())","((())(()))","((()())())","(((()))())","((()()()))","((()(())))","(((())()))","(((()())))","((((()))))"],["()()()()()()","()()()()(())","()()()(())()","()()()(()())","()()()((()))","()()(())()()","()()(())(())","()()(()())()","()()((()))()","()()(()()())","()()(()(()))","()()((())())","()()((()()))","()()(((())))","()(())()()()","()(())()(())","()(())(())()","()(())(()())","()(())((()))","()(()())()()","()(()())(())","()((()))()()","()((()))(())","()(()()())()","()(()(()))()","()((())())()","()((()()))()","()(((())))()","()(()()()())","()(()()(()))","()(()(())())","()(()(()()))","()(()((())))","()((())()())","()((())(()))","()((()())())","()(((()))())","()((()()()))","()((()(())))","()(((())()))","()(((()())))","()((((()))))","(())()()()()","(())()()(())","(())()(())()","(())()(()())","(())()((()))","(())(())()()","(())(())(())","(())(()())()","(())((()))()","(())(()()())","(())(()(()))","(())((())())","(())((()()))","(())(((())))","(()())()()()","(()())()(())","(()())(())()","(()())(()())","(()())((()))","((()))()()()","((()))()(())","((()))(())()","((()))(()())","((()))((()))","(()()())()()","(()()())(())","(()(()))()()","(()(()))(())","((())())()()","((())())(())","((()()))()()","((()()))(())","(((())))()()","(((())))(())","(()()()())()","(()()(()))()","(()(())())()","(()(()()))()","(()((())))()","((())()())()","((())(()))()","((()())())()","(((()))())()","((()()()))()","((()(())))()","(((())()))()","(((()())))()","((((()))))()","(()()()()())","(()()()(()))","(()()(())())","(()()(()()))","(()()((())))","(()(())()())","(()(())(()))","(()(()())())","(()((()))())","(()(()()()))","(()(()(())))","(()((())()))","(()((()())))","(()(((()))))","((())()()())","((())()(()))","((())(())())","((())(()()))","((())((())))","((()())()())","((()())(()))","(((()))()())","(((()))(()))","((()()())())","((()(()))())","(((())())())","(((()()))())","((((())))())","((()()()()))","((()()(())))","((()(())()))","((()(()())))","((()((()))))","(((())()()))","(((())(())))","(((()())()))","((((()))()))","(((()()())))","(((()(()))))","((((())())))","((((()()))))","(((((())))))"]]
「日本の硬貨を使ってn円払う払い方の総数は?」
母関数の応用として有名な例を一つ紹介。
1 + x + x^2 + x^3 + ...
のx^nの係数が1円玉のみでn円払う払い方の総数(つまり1通り)。
1 + x^5 + x^10 + x^15 + x^20 + ...
のx^nの係数が5円玉のみでn円払う払い方の総数(つまり1通り)。
...
これらの組み合わせを考えれば良いので、母関数は
T = (1 + x + x^2 + x^3 + ..) (1 + x^5 + x^10 + x^15 + x^20 + ...) (1 + x^10 + x^20 + ...) (1 + x^50 + x^100 + ...) (1 + x^100 + x^200 + ...) (1 + x^500 + x^1000 + ...)
となります。それぞれの形式的級数(無限等比級数の和の公式を使って)を閉形式にすると
T = 1/(1-x) 1/(1-x^5) 1/(1-x^10) 1/(1-x^50) 1/(1-x^100) 1/(1-x^500)
となります(ここでは発散とか考えなくてよし)。
これから一般解を求める事ができますが、めんどくさいので省略。
漸化式を作ります。
分母を払うと
(1-x)(1-x^5)(1-x^10)(1-x^50)(1-x^100)(1-x^500)T = 1 .... (1)
となります。
さてTのx^nの係数T(n)の漸化式を求めます。
まず(1-x^500)Tのx^nの係数は
A(n) = T(n)-T(n-500)
です。(但し、X(n)の中身が負になったときはX(n)=0とします。)
よって(1-x^100)(1-x^500)Tのx^nの係数は
B(n) = A(n) - A(n-100)
です。これを繰り返すと
A(n) = T(n)-T(n-500) B(n) = A(n) - A(n-100) C(n) = B(n) - B(n-50) D(n) = C(n) - C(n-10) E(n) = D(n) - D(n-5) F(n) = E(n) - E(n-1)
となります。
これを移項して整理すると
T(n) = T(n-500) + A(n) A(n) = A(n-100) + B(n) B(n) = B(n-50) + C(n) C(n) = C(n-10) + D(n) D(n) = D(n-5) + E(n) E(n) = E(n-1) + F(n)
となって下から上に向かって動的計画法で解ける形になります。
F(n)が(1)式の左辺のx^nの係数なのでF(0) = 1で、n > 0の時はF(n) = 0となる事に注意してください。
rubyで書くと以下の様になります。せっかくのRubyなので「インデックスが負なら0」をArrayの[]を書き換えて実装してみました。
class Array def [](i) i < 0 ? 0 : at(i) end end t = [] a = [] b = [] c = [] d = [] e = [] (0..10000).each do |n| f = (n == 0) ? 1 : 0 e[n] = e[n-1] + f d[n] = d[n-5] + e[n] c[n] = c[n-10] + d[n] b[n] = b[n-50] + c[n] a[n] = a[n-100] + b[n] t[n] = t[n-500] + a[n] puts "#{n}円 : #{t[n]}通り" end
nineties% time ruby kouka.rb 0円 : 1通り 1円 : 1通り 2円 : 1通り 3円 : 1通り 4円 : 1通り 5円 : 2通り 6円 : 2通り 7円 : 2通り 8円 : 2通り 9円 : 2通り 10円 : 4通り 11円 : 4通り ... 9996円 : 7825615000通り 9997円 : 7825615000通り 9998円 : 7825615000通り 9999円 : 7825615000通り 10000円 : 7844606371通り ruby kouka.rb 0.19s user 0.06s system 63% cpu 0.395 total
この例では問題の難易度に比べて大げさすぎる気がしますが、まぁおもしろいんではないかと思います。他にもいろんな応用がありますので、是非調べてみて下さい。。