応用情報技術者 2024年 秋期 午後 問03
素数を列挙するアルゴリズムに関する次の記述を読んで、設問に答えよ。
素数とは、2以上の自然数のうち、正の約数が1と自身だけである数のことである。
2以上の自然数Nに対して、N以下の素数を列挙する関数prime1のプログラムを図1に示す。なお、本問では、配列の要素番号は1から始まり、要素数が0の配列を{}で表す。

この関数prime1の時間計算量は、Nを用いて表すとO(ア)である。
〔アルゴリズムの改良1〕
素数の定義によって、2以上の自然数sについて、s自身を除くsの正の倍数uは、1とu以外にsも約数に含むので素数ではない。この性質を利用して関数prime1を改良し、次の手順で素数を列挙する関数prime2を考える。
(1) 2以上N以下の自然数について、全て“素数である”とマークする。
(2) 2以上N以下の自然数dについて、次の(a)、(b)を行う。
(a) dが“素数ではない”とマークされている場合、何もしない。
(b) dが“素数である”とマークされている場合、次の処理を行う。
① dが素数であることを確定させる。
② d以上の自然数xについて、dをx倍した数を“素数ではない”とマークする。
関数prime2のプログラムを図2に示す。
関数prime2は関数prime1と比較してメイン処理部の時間計算量を小さくすることができ、引数Nの値が同一の場合において、関数prime2の(L2)の行の実行回数は、関数prime1の(L1)の行の実行回数以下となる。
〔アルゴリズムの改良2〕
4以上の偶数は全て2の倍数であるので素数ではない。したがって、2以外の素数を列挙するためには奇数だけを考慮すればよい。この性質を利用して、関数prime2に次の変更を加えた関数prime3を考える。
(1) 関数の戻り値として素数の一覧が格納されるprimesにあらかじめ2を格納しておく。
(2) いずれのループも奇数についてだけ実行されるようにする。
(3) 3以上の自然数2k+1が素数か否かをisPrime[k]で表すようにする。
関数prime3のプログラムを図3に示す。
関数prime3は関数prime2と比較してメイン処理部の二重ループの実行回数を減らすことができる。引数Nの値が同一の場合において、関数prime3の(L3)の行の実行回数は、関数prime2の(L2)の行の実行回数の半分以下となる。加えて、計算に必要な配列isPrimeの要素数も半分以下に減らすことができる。
設問1:
本文中のアに入れる適切な字句を答えよ。
模範解答
ア:
解説
解答の論理構成
- 問題文には
「この関数prime1の時間計算量は、Nを用いて表すとO(ア)である。」
とあり、ア を求める設問です。 - 関数 prime1 の主要部は
① 変数 d を 2 から N まで 1 ずつ増やす外側ループ
② 変数 t を 2 から d-1 まで 1 ずつ増やす内側ループ
の二重構造になっています。 - 外側ループが1回回るたびに、内側ループは最大で d-2 回実行されます。
したがって総実行回数は
となります。 - 上式の最高次数項は であり、定数倍と低次数項を無視すると
O() に漸近します。 - よって ア には が入ります。
誤りやすいポイント
- 内側ループの上限を常に N と誤解し O() としてしまう。実際には上限が d に比例するため二乗オーダーにとどまります。
- 内側ループが d 未満で止まることから、 の等差数列和になる点を見落とし、O() などと早合点する。
- 定数項や 1/2 の係数をそのままビッグオー表記に持ち込む(例:O() と書く)ミス。ビッグオーでは定数倍を省略します。
FAQ
Q: なぜ和が になるのですか?
A: d が 2 のとき内側ループは 0 回、d が 3 のとき 1 回…と増えていき、一般に d のときは d-2 回まわるためです。
A: d が 2 のとき内側ループは 0 回、d が 3 のとき 1 回…と増えていき、一般に d のときは d-2 回まわるためです。
Q: 等差数列の和を暗算で覚えていません。どう確認すれば良いですか?
A: 公式 を利用します。本問では を代入すれば になります。
A: 公式 を利用します。本問では を代入すれば になります。
Q: ビッグオーで O() と O() の違いは評価対象になりますか?
A: なりません。ビッグオーは漸近的な上界を示すので定数倍の差異は無視されます。
A: なりません。ビッグオーは漸近的な上界を示すので定数倍の差異は無視されます。
関連キーワード: 素数判定、二重ループ、計算量解析、ビッグオー表示
設問2:
図2中のイ、ウに入れる適切な字句を答えよ。
模範解答
イ:isPrimedがtrueと等しい
ウ:t+d
解説
解答の論理構成
- 【問題文】では関数prime2の手順を
「② d以上の自然数xについて、dをx倍した数を“素数ではない”とマークする。」
と述べています。
つまり、既に合成数と分かった数は isPrime が false になっており、まだ true のまま残っているものだけが候補です。 - したがって (L2) より前に置かれる条件 イ では
「d が “素数である”とマークされている場合」を判定しなければなりません。
配列 isPrime の仕様は【問題文】にある
「isPrimekがtrueであればkが素数である」
なので、判定式は
isPrimedがtrueと等しい - 内側の while ループは
t ← d × d からスタートし、d の倍数を順に潰していく典型的な「エラトステネスのふるい」です。
1 回のループで t を次の倍数に進めるには d を足せばよいので
t ← t + d
が ウ に入る語句となります。 - 以上より
イ:isPrimedがtrueと等しい
ウ:t+d
誤りやすいポイント
- isPrimed を参照せず、誤って primes の末尾要素との比較を書いてしまう。
- 内側ループを t ← t * d としてしまい、倍数ではなく指数列を生成して無限ループに陥る。
- t ← d + d と書いて初回だけ正しくても 2 回目以降に t を保持し損ねる実装ミス。
FAQ
Q: なぜ t を d × d から始めるのですか?
A: d 未満の倍数は既に小さい素数の段階で潰されているため、冗長なチェックを省けるからです。
A: d 未満の倍数は既に小さい素数の段階で潰されているため、冗長なチェックを省けるからです。
Q: ループ条件で isPrimed を見るのと、条件部で if 判定するのはどちらが効率的ですか?
A: 条件部で if 判定する方がコードの可読性を保ちつつ同じ効果を得られます。while 条件に組み込むと可読性が下がり、メンテナンス性が悪化します。
A: 条件部で if 判定する方がコードの可読性を保ちつつ同じ効果を得られます。while 条件に組み込むと可読性が下がり、メンテナンス性が悪化します。
Q: t+d ではなく d を足し続ける理由は?
A: d の倍数列は算術級数 なので、公差は常に d です。足し続ければ漏れなく列挙できます。
A: d の倍数列は算術級数 なので、公差は常に d です。足し続ければ漏れなく列挙できます。
関連キーワード: 素数、エラトステネスのふるい、時間計算量、配列操作、ブールフラグ
設問3:
図3中のエ〜カに入れる適切な字句を答えよ。
模範解答
エ:isPrime[(d-1)÷2]がtrueと等しい
オ:(t-1)÷2
カ:t+2×d
解説
解答の論理構成
-
素数表現のルール確認
- 問題文には「3以上の自然数2k+1が素数か否かをisPrimekで表すようにする」とあります。
- したがって「奇数 2k+1 ⇔ 配列添字 k」の対応が決定します。
-
エ の導出
- ループ変数 d は「3 から 2 ずつ増える奇数」です。
- 配列添字への変換は 。
- 「isPrimek が true なら素数と確定」という仕様より
エ は isPrime[(d-1)÷2] が true と等しい になります。
-
オ の導出
- 内側ループでは t ← d × d からスタートし、以降は奇数の倍数だけを調べます。
- 配列添字は同じ変換規則で 。
- よって代入対象は isPrime[(t-1)÷2]、中の添字部のみを書くので オ は (t-1)÷2 です。
-
カ の導出
- 従来のふるいでは としていましたが、偶数を無視するため倍数の刻み幅は 。
- したがって カ は t + 2 × d になります。
誤りやすいポイント
- 添字変換を としてしまい偶数混入でバグになる。
- カ を t + d としてしまい、偶数倍を繰り返しマークして性能劣化。
- エ を isPrimed と書いてしまい、配列の範囲外アクセスまたは正否逆転を招く。
FAQ
Q: どうして からふるい落とすのですか?
A: 未満の数との積はすでにより小さい素数の段階で処理済みだからです。これにより重複チェックを省けます。
A: 未満の数との積はすでにより小さい素数の段階で処理済みだからです。これにより重複チェックを省けます。
Q: 配列サイズが「半分以下」になる理由は?
A: 2 を除くと調べる対象は奇数だけです。範囲が 個になるため、偶数も含めた prime2 の配列と比べてほぼ半分になります。
A: 2 を除くと調べる対象は奇数だけです。範囲が 個になるため、偶数も含めた prime2 の配列と比べてほぼ半分になります。
Q: 時間計算量はどの程度改善されますか?
A: 偶数を除外したことで二重ループの内側が約 1/2 に、さらに から始めることで外側も減り、理論的にはエラトステネスのふるいの に近づきます。
A: 偶数を除外したことで二重ループの内側が約 1/2 に、さらに から始めることで外側も減り、理論的にはエラトステネスのふるいの に近づきます。
関連キーワード: エラトステネスのふるい、配列インデックス変換、奇数列挙、時間計算量、マーキング手法
設問4:
prime1(20), prime2(20), prime3(20)をそれぞれ実行したとき、図1中の(L1)の行、図2中の(L2)の行、図3中の(L3)の行が実行される回数をそれぞれ答えよ。
模範解答
(L1):171
(L2):13
(L3):2
解説
解答の論理構成
-
ループ構造の整理
- 図1・図2・図3はすべて「素数列挙」のために二重ループを用いる。
- 計数対象はそれぞれ
- 図1中の「(L1)」
- 図2中の「(L2)」
- 図3中の「(L3)」
の3行である。
- いずれも 引数 N に “20” を与えたとき の実行回数を求める。
-
prime1(20) の場合
- 外側ループは “d が 2 以上 20 以下” を順に取る。
- 内側ループは “t が d 未満” の間だけ回り、「(L1)」=t ← t + 1 が1回実行されるたびに1カウント。
- 自然数 に対し、 は と動くので回数は 。
- 総回数は = 0+1+2+\dots +18 = \frac{18 \times 19}{2} = 171$$
- よって (L1) は 171 回。
-
prime2(20) の場合
- 改良で “素数である を見つけたときだけ” 内側ループをまわし、開始値は “t ← d × d”。
- さらに「(L2)」の実行は を ずつ増やしながら 以下の倍数を消す” ときだけ。
- では であり、 が 以下になる素数 は “2” と “3” だけ。
- のとき: は “4,6,8,10,12,14,16,18,20” → 9 回
- のとき: は “9,12,15,18” → 4 回
- は なので0回
- 合計 9+4 = 13 回。
-
prime3(20) の場合
- さらに “偶数を完全に除外” し、
- は “3,5,7,… ” と 2 ずつ増加
- 内側ループは “t ← d × d” から 2 × d ずつ増やして奇数倍のみを処理する。
- では を満たす奇素数 は “3” だけ。
- のとき: は “9,15” → 2 回
- は のため0回
- 合計 2 回。
- さらに “偶数を完全に除外” し、
-
結果のまとめ
- (L1):171
- (L2):13
- (L3):2
誤りやすいポイント
- d × d から始める条件を見落とし、 など低い倍数も数えてしまう。
- まで調べればよい事実を使わず、すべての で内側ループを考えてしまう。
- prime3 の増分が “2 × d” であることに気付かず、“d” で加算したまま計算する。
FAQ
Q: なぜ prime2 では “2” と “3” だけを数えればよいのですか?
A: (L2) が実行される条件は N=20$ では “2” の 4 と “3” の 9 だけが該当し、“5” 以上は 25 で上限を超えるため実行されません。
A: (L2) が実行される条件は N=20$ では “2” の 4 と “3” の 9 だけが該当し、“5” 以上は 25 で上限を超えるため実行されません。
Q: prime3 の内側ループが “2 回” しか回らないのは偶然ですか?
A: いいえ。偶数を除外し奇数倍だけを処理するため、 の倍数で 以下の奇数は “9” と “15” の2個しか存在しないからです。
A: いいえ。偶数を除外し奇数倍だけを処理するため、 の倍数で 以下の奇数は “9” と “15” の2個しか存在しないからです。
Q: 3つのアルゴリズムで配列 isPrime の要素数が異なる理由は?
A: 改良を重ねるごとに「調べる候補数」を減らしているためです。prime2 では 2..N をすべて保持しますが、prime3 では奇数だけを対象とし、要素数が約半分になります。
A: 改良を重ねるごとに「調べる候補数」を減らしているためです。prime2 では 2..N をすべて保持しますが、prime3 では奇数だけを対象とし、要素数が約半分になります。
関連キーワード: 素数、エラトステネスのふるい、時間計算量、ループ最適化、配列インデックス


