応用情報技術者 2014年 春期 午後 問03
循環小数の循環節を検出するアルゴリズムに関する次の記述を読んで、設問1〜4に答えよ。
与えられた自然数nについて、1/nの小数表現を考える。1/nは、割り切れない場合、必ず循環小数となる。本問において、循環小数は、図1に示すように、繰り返し現れる数の並びである循環節の先頭から末尾までを括弧でくくる表記法で表す。
1をnで割る割り算で現れる余りは、1からn−1までの値に限られる。循環小数となる場合は、割り算を小数第1位から行っていくと、どこかでそれまでに現れた余りと同じ値の余りが現れる。以降の割り算で得られる小数は、同じ数の並びが繰り返されることとなり、その数の並びが循環節となる。循環節の桁数は最大n−1である。

1/56の計算で商を1桁ずつ求めていくと、余りの遷移は図2のようになる。ここでは、前のマスの中の値を10倍して56で割った余りが次のマスの中の値となる。余りとして48が再度現れることから、1/56では小数第4位から第9位までが循環節となることが分かる。

〔循環節を検出するアルゴリズム〕
単純に余りを配列に記録して、既出の余りであるかどうか比較することによって循環節を検出する方法では、割り算の各桁で現れる余りの種類の最大であるn−1種類の余りを記録する必要がある。その配列の大きさはnに比例することになり、処理できるnの値は使用可能な記憶域の大きさによって制約される。そこで記憶域の制約を受けないで、“ウサギとカメ” に例えられるフロイドの循環検出法のアルゴリズムを用いる。この検出法は、循環するデータの先頭と末尾の位置を効率良く検出できることが証明されている。
図2の余りを格納したマスから次のマスへ割り算を1桁進めることを、“ウサギとカメ” の歩みに例えて1歩進むという。
このアルゴリズムでは、まず、図3に示すように、カメは〈1〉、〈2〉、…と一度に1歩ずつ、ウサギは《1》、《2》、…と一度に2歩ずつ進む。両者は同時に出発し、進んだマス(〈1〉と《1》、〈2〉と《2》、…)の余りを比較しながら、余りが一致するところ(〈6〉と《6》)まで進む。
このように、循環小数となる全てのnにおいて、ウサギは循環部分の何巡目かで周回遅れのカメに必ず追い付き、両者の余りは一致する。

両者の余りが一致したら、図4に示すように、カメは引き続き、ウサギは最初に戻って今度は両者とも1歩ずつ進む。最初に両者の余りが一致したところ(〈9〉と《9》)の次の割り算の商が、循環節の先頭である。

循環節の先頭を検出した後、図5に示すように、ウサギだけが再びカメと余りが一致するところ(〈9〉と《15》)まで1歩ずつ進むことによって、循環節の末尾を検出する。

このアルゴリズムを使って循環節の先頭と末尾の位置を求める関数junkanを図6に示す。図6で使用する変数と関数は、表1のとおりである。

与えられたnに基づき、O記法で表した場合、関数junkanのプログラムが必要とする記憶域の大きさは オ となり、計算量は カ となる。
設問1:
問題文を見る模範解答
ア:mがpと等しい
イ:p ← 1
ウ:mがpと等しくない
エ:t ← s
解説
解答の論理構成
-
第1ループの終了条件(ア)
- 問題文は「両者は同時に出発し、進んだマス…の余りを比較しながら、余りが一致するところ…まで進む。」と述べています。
- したがってウサギ(p)とカメ(m)の余りが一致した時点でbreakすべきです。
- よって ア は「mがpと等しい」。
-
pを元に戻す操作(イ)
- 同じ段落で「ウサギは最初に戻って」とあるため、ウサギの余りpを開始時の余り1にリセットします。
- よって イ は「p ← 1」。
-
循環節先頭を探すループ条件(ウ)
- 復帰後は「今度は両者とも1歩ずつ進む。最初に両者の余りが一致したところ…が、循環節の先頭」と説明されています。
- 一致するまで繰り返すのでループ条件は「mがpと等しくない」。
- よって ウ は「mがpと等しくない」。
-
循環節末尾検出の初期化(エ)
- 末尾は「ウサギだけが再びカメと余りが一致するところ…まで1歩ずつ進む」ことで求めます。
- このときt(末尾位置)は循環節の先頭位置sから数え始める必要があります。
- よって エ は「t ← s」。
誤りやすいポイント
- 第1ループと第2ループの比較対象を混同し、ア と ウ を逆に記入してしまう。
- 「ウサギは最初に戻る」を「余りを 0 にする」と誤解し、イ に「p ← 0」としてしまう。
- 末尾検出でtを0から数え始め、エ を書き漏らして循環節長がずれる。
FAQ
Q: ループが終了しない場合はありますか?
A: ありません。問題文で「ウサギは循環部分の何巡目かで周回遅れのカメに必ず追い付き」と保証されています。
A: ありません。問題文で「ウサギは循環部分の何巡目かで周回遅れのカメに必ず追い付き」と保証されています。
Q: pを最初に戻すのはなぜ1なのですか?
A: m・p は「1 を n で割る」計算の余りを保持します。開始時の余りは 1 なので、「最初に戻る」は p ← 1 になります。
A: m・p は「1 を n で割る」計算の余りを保持します。開始時の余りは 1 なので、「最初に戻る」は p ← 1 になります。
Q: t ← s を省略するとどうなりますか?
A: tが0のまま末尾探索を始めるため、循環節の長さ(末尾位置)が誤って小さい値になります。
A: tが0のまま末尾探索を始めるため、循環節の長さ(末尾位置)が誤って小さい値になります。
関連キーワード: フロイドの循環検出法, 循環小数, 余り遷移, 2ポインタ手法, 計算量解析
設問2:
問題文を見るn=88のとき、図6中のαとβはそれぞれ何回実行されるか答えよ。
模範解答
α:4
β:3
解説
解答の導き方
まず,プログラム中の該当箇所を確認します。問題文のプログラムには「m ← amari(m * 10, n) ← α // カメが1歩進む」「p ← amari(amari(p * 10, n) * 10, n) // ウサギが2歩進む」とあり,α は第1の while 各反復でカメ(m)が 1 歩進むたびに実行され,β は先頭検出の while 内で「m ← amari(m * 10, n) ← β」が実行されるたびに実行されます。これを根拠に回数を数えます。
1/88の余り遷移を手で求めます。余りの更新をf(r) = amari(r * 10, 88) と定義し、 とすると:
- amari()
- amari()
- amari()
- amari()
- amari()
となるので、余り列は の周期(長さ )で循環し、最初に循環に入る位置は です(余り列のインデックスで表した値)。(小数の桁位置 は余りインデックスに対して になる点に注意します。問題文中の定義と一致します。)
フロイド法の性質から、第1フェーズ(ウサギは2歩、カメは1歩)のループで両者が初めて出会う反復回数 は「最小の の倍数で となる値」です。理由は、反復 の時点でカメは 、ウサギは にいるため、両者一致は 、すなわち差 が周期 の倍数である必要があるためです。ここで なので、最小の の倍数で は です。したがって、第1ループ中でカメが進む行(α)は4回実行されます(α=4)。
確認のため第1ループを逐次シミュレーションすると:
- 反復1: m=f(1)=10 、 p=f(f(1))=f(10)=12 → 等しくない(α 回数1)
- 反復2: m=f(10)=12 、 p=f(f(12))=f(32)=56 → 等しくない(α 回数2)
- 反復3: m=f(12)=32 、 p=f(f(56))=56 → 等しくない(α 回数3)
- 反復4: m=f(32)=56 、 p=f(f(56))=56 → 等しいのでbreak(α 回数4)
次に β(循環節の先頭検出での m 更新回数)を数えます。問題文に「カメは引き続き、ウサギは最初に戻って今度は両者とも1歩ずつ進む」とあるので,ここで行われる [イ] は p ← 1 です(ウサギを出発点に戻す)。実行時点では break 後に m=p=56 でしたから,p を 1 に戻してから両者を 1 歩ずつ進めると,ウサギが出発点から循環先頭に達するまでの反復回数が になります。逐次シミュレーションすると:
初期(先頭検出開始時): m=56, p=1, s=1
- 繰り返し1: s→2, m=f(56)=32(β 回数1), p=f(1)=10 → 等しくない
- 繰り返し2: s→3, m=f(32)=56(β 回数2), p=f(10)=12 → 等しくない
- 繰り返し3: s→4, m=f(56)=32(β 回数3), p=f(12)=32 → 等しいので終了
したがって β は3回実行されます(β=3)。第2ループでの最後のpの1回の追加更新や第3フェーズ(末尾検出)の処理は β の回数に影響しません。
まとめ:
α:4
β:3
β:3
誤りやすいポイント
- [イ] を m ← 1 と誤解する点。本文は「ウサギは最初に戻る」と明記しており,正しくは p ← 1 です。ここを取り違えると先頭検出の初期状態が逆になり誤ったカウントになります。
- 余りのインデックスと小数桁位置のズレ(余りのインデックス と小数桁位置 は )を見落とす点。余りが最初に繰り返した位置と、循環節の先頭小数桁は1つずれます。
- ループ内で変数を更新してから比較しているか(更新後にifを行うか)を見落とし、初期状態で即一致すると誤判断する点。プログラムは各反復でまずm,pを更新してから比較しています。
- β を周期長 と混同する点。β は先頭検出フェーズでのカメの更新回数で、余り列の最初の循環位置 に等しくなります(今回は )。
- 終了条件として余りが0になる場合(有限小数)は第2フェーズを実行しない点(コード中にif(pが0と等しくない) がある)を忘れる点。
FAQ
Q: α を余り列を全部列挙せずに求められますか?
A: はい。フロイド法の理論より、α は「最小の周期長 の倍数で 」となる反復回数です。したがって、余り列の最初の循環位置 と周期長 が分かればその式で直接求められます。
A: はい。フロイド法の理論より、α は「最小の周期長 の倍数で 」となる反復回数です。したがって、余り列の最初の循環位置 と周期長 が分かればその式で直接求められます。
Q: β は何を表しますか?
A: β は先頭検出のループでmを更新した回数で、ウサギを出発点に戻して両者を1歩ずつ進めるときにウサギが循環節の先頭に達するまでの回数、すなわち余り列の最初の循環位置 に等しいです。
A: β は先頭検出のループでmを更新した回数で、ウサギを出発点に戻して両者を1歩ずつ進めるときにウサギが循環節の先頭に達するまでの回数、すなわち余り列の最初の循環位置 に等しいです。
Q: 分母に2と5だけが素因数ならどうなりますか?
A: 余りが0になり有限小数になります。プログラムではif(pが0と等しくない) により第2フェーズ(先頭検出)をスキップしますので、β は実行されません。
A: 余りが0になり有限小数になります。プログラムではif(pが0と等しくない) により第2フェーズ(先頭検出)をスキップしますので、β は実行されません。
関連キーワード: フロイドの循環検出法、循環小数、剰余列、周期()
設問3:
問題文を見る1/nが割り切れるとき、関数junkanの戻り値はどのようになるか。15字以内で述べよ。
模範解答
s, tともに0である。
解説
解答の導き方
まず図6の関数junkanの冒頭で「m ← 1」「p ← 1」「s ← 0」「t ← 0」と初期化していることを確認します。while(true)のループではカメが「m ← amari(m * 10, n)」で1歩進み、ウサギが「p ← amari(amari(p * 10, n) * 10, n)」で2歩進み、各回に両者の余りが一致したらループを抜けます。問題文にあるように割り算の余りは有限個で、1/nが割り切れる場合は途中で余りが0になり、その後は0が連続します。したがって初期ループのいずれかの回でウサギのpが0になり、それ以降pは0のまま維持されます。カメのmも有限回のうちに0に到達して最終的に m = p = 0 となり while を抜けます。
while を抜けた直後にある条件「if(pが0と等しくない)」は p が 0 でない場合に限り循環節の先頭・末尾を求める処理を実行することを意味します。割り切れる場合はこの if の中身が実行されず、s と t は初期値のまま返されます。したがって関数junkanの戻り値は「s, tともに0である。」となります。
誤りやすいポイント
- pが0になった瞬間にmも同一回で0になると誤解する。例:n=4では初回でpが0になりmは2、次回にmが0になってその時点で一致してbreakする。pが先に0になることがある点を押さえること。
- s, t が初期化で0になっている点を見落とすと、戻り値が0である理由が説明できない。図6冒頭の「s ← 0」「t ← 0」を必ず確認する。
- whileを抜けた直後の判定「if(pが0と等しくない)」を読み違え、p==0でも先頭検出処理が行われると誤解しやすい(実際はスキップされる)。
FAQ
Q: pが0になったらすぐにループを抜けますか?
A: いいえ。whileでは毎回「m」と「p」を更新した後に両者が一致するかを判定します。pが0になってもmが0でなければ一致せず、mが0になる回に一致してループを抜けます(例:n=4)。
A: いいえ。whileでは毎回「m」と「p」を更新した後に両者が一致するかを判定します。pが0になってもmが0でなければ一致せず、mが0になる回に一致してループを抜けます(例:n=4)。
Q: 1/nが割り切れない(循環小数)の場合は戻り値はどうなりますか?
A: 割り切れない場合は初期ループでmとpが循環部分のどこかで一致し、かつpは0でないため「if(pが0と等しくない)」の中の処理が実行され、s,tに循環節の先頭・末尾が設定されて返ります。
A: 割り切れない場合は初期ループでmとpが循環部分のどこかで一致し、かつpは0でないため「if(pが0と等しくない)」の中の処理が実行され、s,tに循環節の先頭・末尾が設定されて返ります。
Q: この実装が必要とする記憶域はどのくらいですか?
A: 図6の実装は変数m, p, s, tと一時変数のみを使うため、必要な記憶域は定数で表される(O(1))です。
A: 図6の実装は変数m, p, s, tと一時変数のみを使うため、必要な記憶域は定数で表される(O(1))です。
関連キーワード: フロイドの循環検出法、循環小数、余りの遷移、時間計算量、空間計算量
設問4:
問題文を見る本文中のオ、カに入れる適切な字句を答えよ。
模範解答
オ:O(1)
カ:O(n)
解説
解答の論理構成
-
記憶域量の考察
- プログラム図6には変数として「n」「m」「p」「s」「t」の5つしか登場しません。
- 【問題文】では「与えられたnに基づき、0記法で表した場合、関数junkanのプログラムが必要とする記憶域の大きさは オ」とあります。
- 配列やリストなどサイズがnに比例する構造を一切使わず、固定個数のスカラー値だけで処理しているため、必要なメモリは入力サイズnに依存せず一定です。
- よって オ には計算機科学で「定数時間(空間)」を表す O(1) が入ります。
-
計算量の考察
- 周期検出部(ウサギとカメで余りが一致するまで)
while ループは余りの総種類数である高々 n−1 回で必ず終了します(同じ余りが出現すると循環が確定するため)。 - 循環節の先頭検出部、末尾検出部も、それぞれ循環節長+非循環部長を1回ずつなぞるだけなので、総和は高々n−1歩です。
- したがって全体で実行する余り計算は入力値に比例し、「計算量は カ となる」という記述には O(n) が当てはまります。
- 周期検出部(ウサギとカメで余りが一致するまで)
-
まとめ
- 記憶域:固定個数の変数のみ → O(1)
- 計算量:余りの種類数(最大n−1)だけ繰返し → O(n)
誤りやすいポイント
- 「amari(a,b) を2回呼ぶから2倍でO(2n)」と細かく数えてしまう
➔ 演算回数に定数倍を付けてもビッグオー表記では同じO(n) です。 - 余りを毎回記録していると勘違いし、記憶域をO(n) としてしまう
➔ 本アルゴリズムはフロイド法なので記録は不要です。 - 循環節長だけを計算量として採り「最大でもn−1だからO(n−1)」と書く
➔ 引き算も定数扱い、正式にはO(n) とします。
FAQ
Q: 余りを比較するだけで本当にメモリは増えませんか?
A: はい。2つのポインタ(m,p)を更新して比較するだけなので、循環検出に過去の軌跡を保存する必要がありません。
A: はい。2つのポインタ(m,p)を更新して比較するだけなので、循環検出に過去の軌跡を保存する必要がありません。
Q: nが素数でも複合数でも計算量は変わらないのですか?
A: 変わりません。余りの種類数は常に最大でn−1なので、最悪計算量はどちらもO(n) です。
A: 変わりません。余りの種類数は常に最大でn−1なので、最悪計算量はどちらもO(n) です。
Q: もし配列表現で余りを保存したらどうなりますか?
A: その場合は余りを最大n−1個格納するため記憶域がO(n)、計算量も配列検索方法によっては悪化します。フロイド法はこの欠点を解消します。
A: その場合は余りを最大n−1個格納するため記憶域がO(n)、計算量も配列検索方法によっては悪化します。フロイド法はこの欠点を解消します。
関連キーワード: ビッグオー表記, フロイド循環検出法, 定数空間, 余り計算, ループ解析




