12.6 演習¶
12.6.1 確認問題¶
1. 分割統治に適した処理¶
ある生徒は、「まず 2 つに分け、それぞれを解いてから結果をまとめる」という方法で、次の処理を行おうとしています。 それぞれについて、「分割統治に適している」「分割統治はできるが、作業の総量は減らない」「2 つを独立して解けない」のいずれかを判断し、理由を説明してください。
- ソートされていない配列をソートする。
- 配列の最大値を求める。
- 一連の
push(x)、pop()というスタック操作を順に実行し、各pop()で得た要素を出力する。
解答
- 適しています。半分に分け、2 つを独立してソートし、\(O(n)\) でマージします。これはマージソートそのものです。
- 分割統治はできますが、作業の総量は減りません。左右の 2 つを合わせると、結局は \(n\) 個の要素をすべて確認する必要があり、 直接走査する場合と同じく \(O(n)\) の時間がかかります。
- 2 つを独立して解くことはできません。後半を開始するときのスタックの内容が前半の実行結果に依存するため、 互いの結果を知らない状態では独立して処理できません。
2. 高速べき乗で計算を減らす仕組み¶
次の再帰関数は、分割統治を使って \(x^n\) を計算します。
def fast_pow(x, n):
if n == 0:
return 1
half = fast_pow(x, n // 2)
if n % 2 == 0:
return half * half
return half * half * x
この関数で fast_pow(3, 5) を計算します。
- 再帰呼び出しでは、引数
nはどのような値に順に変わりますか? - 最も深い呼び出しから戻るとき、各呼び出しはどの値を順に返しますか?
fast_pow(x, n // 2)を 2 回書くのではなく、先にhalfへ保存するのはなぜですか?
解答
-
引数は
5 → 2 → 1 → 0の順に変わります。毎回指数を半分にし、終了条件に達するまで続けます。 -
n = 0のとき 1 を返します。n = 1のとき \(1×1×3=3\)、n = 2のとき \(3×3=9\)、n = 5のとき \(9×9×3=243\) を返します。 -
乗算の両側に
fast_pow(x, n // 2)を 1 回ずつ書くと、2 つの再帰呼び出しがまったく同じ部分問題を計算します。 結果を先にhalfへ保存すれば、各層で再帰するのは 1 回だけとなり、再帰の深さは約 \(\log n\) です。 2 回呼び出すと、大量の重複計算が発生します。
3. 走査列から左右の部分木を分ける¶
重複するノードを持たない二分木の先行順走査と中間順走査は、それぞれ次のとおりです。
- 先行順走査:
[A, B, D, E, C] - 中間順走査:
[D, B, E, A, C]
根ノードの層だけを分けてください。それ以降の再帰を行ったり、木全体を描いたりする必要はありません。
- 根ノードは何ですか?
- 左右の部分木は、中間順走査のどの部分にそれぞれ対応しますか?
- 左右の部分木は、先行順走査のどの部分にそれぞれ対応しますか?根ノードの直接の子ノードは何ですか?
解答
-
先行順走査の最初のノードが根ノードなので、根ノードは
Aです。 -
Aによって中間順走査は 2 つに分かれます。左部分木は[D, B, E]、右部分木は[C]です。 -
左部分木には 3 つのノードがあるため、根ノード
Aの後ろにある 3 つの先行順走査の要素が左部分木に属し、[B, D, E]となります。残りの[C]は右部分木に属します。 したがって、根ノードの左子ノードはB、右子ノードはCです。
12.6.2 プログラミング演習¶
1. 高速べき乗¶
実数 x と整数 n が与えられます。言語に組み込まれたべき乗関数を呼び出さず、\(x^n\) を計算してください。
再帰による分割統治を使い、毎回指数を半分にして、計算済みの部分問題の結果を再利用してください。
この問題では x = 0 の場合も含めて \(x^0=1\) とします。n < 0 のときは x != 0 が保証され、答えを \((1/x)^{-n}\) に変換できます。
解法のヒント
- n が 0 のとき、答えは 1 です
- x の n // 2 乗を計算したら half に保存し、2 回目の再帰呼び出しを行わないようにします
- n < 0 のときは、まず x を 1 / x に変え、次に n を -n に変えます。C++ または Java を使う場合は、最小の 32 ビット整数の符号を反転したときのオーバーフローを避けるため、先に n を 64 ビット整数へ変換できます