情報学入門(データ演習) / 第 8 章

繰り返し(2)

Do 〜 Loop ── いつ終わるか分からない繰り返し

この章のねらい

1. Do 〜 Loop の 4 つの形

第 7 章の For は回数が決まっているときの道具でした。 いつ終わるか分からないときは Do 〜 Loop を使います。 条件を前に置くか後ろに置くか続ける条件か終わる条件かで 4 通りあります。

① Do While 〜 Loop(前判定・続ける条件)

Do While i <= 5
    …
Loop
条件が真の間くり返す。最初から偽なら 1 回も実行されない

② Do Until 〜 Loop(前判定・終わる条件)

Do Until i > 5
    …
Loop
条件が真になるまでくり返す。①と同じ動きを裏返しに書いたもの

③ Do 〜 Loop While(後判定・続ける条件)

Do
    …
Loop While i <= 5
必ず 1 回は実行される。実行してから判断する

④ Do 〜 Loop Until(後判定・終わる条件)

Do
    …
Loop Until r = 0
必ず 1 回は実行される。互除法はこの形が自然

前判定と後判定の違いは、「0 回でも許すか」です。 ①② は条件を先に見るので、最初から条件を満たしていなければ1 回も実行しません。 ③④ は先に実行するので必ず 1 回は通ります。 「まず 1 回計算してみて、その結果で続けるか決める」場面(互除法・収束計算)では ③④ が自然です。

Sub Macro1()
    Dim i As Integer
    Dim c As Integer

    ' ① 前判定:i = 10 なので、条件 i <= 5 は最初から偽
    i = 10: c = 0
    Do While i <= 5
        c = c + 1
        i = i + 1
    Loop
    Cells(1, 1) = "① Do While(前判定)": Cells(1, 3) = c & " 回実行"

    ' ③ 後判定:同じ i = 10 でも……
    i = 10: c = 0
    Do
        c = c + 1
        i = i + 1
    Loop While i <= 5
    Cells(2, 1) = "③ Loop While(後判定)": Cells(2, 3) = c & " 回実行"

    Cells(4, 1) = "同じ条件・同じ初期値でも、実行回数が違う"

    ' ② と ④ は、条件を裏返して書いたもの
    i = 1: c = 0
    Do Until i > 5
        c = c + 1
        i = i + 1
    Loop
    Cells(6, 1) = "② Do Until": Cells(6, 3) = c & " 回実行"

    i = 1: c = 0
    Do
        c = c + 1
        i = i + 1
    Loop Until i > 5
    Cells(7, 1) = "④ Loop Until": Cells(7, 3) = c & " 回実行"
End Sub

2. 10 人の点数の平均(元資料 3・4 枚目)

Excel なら =AVERAGE(A2:A11) で済みますが、VBA で書いてみます。 「読んで、足して、割る」という流れの練習です。

Sub Macro2()
    Dim i As Integer
    Dim s As Long          ' 合計は大きくなりうるので Long
    Dim n As Integer
    Dim heikin As Double

    s = 0
    n = 0

    For i = 2 To 11
        If Cells(i, 1).Value <> "" Then     ' 空を除く(第 2 章 §4)
            s = s + Cells(i, 1).Value
            n = n + 1
        End If
    Next i

    Cells(13, 1) = "合計":   Cells(13, 2) = s
    Cells(14, 1) = "人数":   Cells(14, 2) = n
    If n > 0 Then
        heikin = s / n
        Cells(12, 1) = "平均"
        Cells(12, 2) = Format(heikin, "#.##")
    End If

    ' 平均より上の人に色をつける
    For i = 2 To 11
        If Cells(i, 1).Value <> "" Then
            If Cells(i, 1).Value > heikin Then
                Cells(i, 2) = "↑"
                Cells(i, 2).Interior.Color = RGB(198, 239, 206)
            End If
        End If
    Next i
End Sub

If を入れ子にしているのは、わざとです。 If Cells(i,1) <> "" And Cells(i,1) > heikin Then と 1 行で書きたくなりますが、 VBA は And両側を必ず評価するので(第 5 章 §4.4)、 空のセルでも右側の比較が行われます。 文字列と数値の比較になって、思わぬ結果になることがあります。 「先に守ってから、中で判断する」のが安全な書き方です。

3. 多重ループ ── 2〜30 までの約数(元資料 5〜7 枚目)

n約数とは、n を割り切る数です。 「割り切れるか」は Mod が 0 かどうかで判定できます(第 3 章)。

Sub Macro3()
    Dim n As Integer
    Dim d As Integer
    Dim r As Integer          ' 書き込む列
    Dim kosuu As Integer      ' 約数の個数

    For n = 2 To 30
        Cells(n - 1, 1) = n
        r = 2
        kosuu = 0
        For d = 1 To n
            If n Mod d = 0 Then       ' 割り切れたら約数
                Cells(n - 1, r) = d
                r = r + 1
                kosuu = kosuu + 1
            End If
        Next d
        ' 約数が 1 と自分の 2 個だけなら素数
        If kosuu = 2 Then
            Cells(n - 1, 1).Interior.Color = RGB(255, 235, 156)
        End If
    Next n
End Sub

色がついた 10 個(2, 3, 5, 7, 11, 13, 17, 19, 23, 29)が 30 以下の素数です。約数が 1 と自分自身の 2 個だけというのが素数の定義そのものなので、 数えるだけで判定できます。

ただし、この方法は無駄が多いです。 n の約数を探すのに 1 から n まで全部試していますが、 n まで調べれば足りますna×b なら、ab の少なくとも一方は √n 以下)。 30 なら 5 まで、10000 なら 100 まで。計算量が桁違いに減ります。

4. 回数が決まらない繰り返し(元資料 8〜10 枚目)

1 日目に 1 円、2 日目に 2 円というように毎日倍額貯金すると、 合計が 100 万円を超えるのは何日目で、同時に合計金額はいくらになるか求めなさい。

何日かかるかは、計算する前には分かりません。 だから For では書けません ── これが Do 〜 Loop の出番です。

Sub Macro4()
    Dim hi As Integer          ' 日数
    Dim kyou As Long           ' その日に入れる金額
    Dim goukei As Long         ' 合計
    Dim mokuhyou As Long

    mokuhyou = 1000000         ' 100 万円

    Cells(1, 1) = "日":  Cells(1, 2) = "その日":  Cells(1, 3) = "合計"

    hi = 0
    kyou = 1
    goukei = 0

    Do While goukei <= mokuhyou
        hi = hi + 1
        goukei = goukei + kyou
        If hi <= 21 Then
            Cells(hi + 1, 1) = hi
            Cells(hi + 1, 2) = kyou
            Cells(hi + 1, 3) = goukei
        End If
        kyou = kyou * 2        ' 次の日は倍
    Loop

    Cells(23, 1) = "超えたのは"
    Cells(23, 2) = hi & " 日目"
    Cells(24, 1) = "そのときの合計"
    Cells(24, 2) = goukei
    Cells(23, 2).Interior.Color = RGB(255, 235, 156)
End Sub

答えは 20 日目、1,048,575 円。 n 日目に入れるのは 2n−1 円で、合計は 2n − 1 円になります。 19 日目で 524,287 円、20 日目で 1,048,575 円 ── 1 日で倍増えるので、 超えるときは一気に超えます。

目標を 1 億円にしても、たった 27 日です。 上のプログラムで mokuhyou を変えて試してみてください。 これが指数関数的な増加で、 30 日続けたら 10 億円を超えます(そして 64 日目には Long 型でも足りなくなります)。

型の選び方に注意。 kyougoukeiInteger にすると、 32,767 を超えた時点でエラーになります(15 日目あたり)。 「倍々に増える」と分かっているものは、最初から Long にしておきます。 上のプログラムで As LongAs Integer に変えて試すと、 どこで止まるかが分かります。

5. ユークリッドの互除法 ── 最大公約数と最小公倍数

ユークリッドの互除法を用いて、2 つの正の整数の最大公約数(GCD)を求めよ。 さらに、最小公倍数(LCM)も求めよ。

元資料 11〜13 枚目です。考え方は 「大きい方から小さい方を引いても、公約数は変わらない」という性質を使います。 引き算を繰り返して、差が 0 になったときの値が最大公約数です。

5.1 元資料の手順(1 つ目)── これは正しい

2 つの正の整数を変数 K と変数 Y へ各々代入する
K の値と Y の値を比べ、K > Y の場合 K と Y を交換する
変数 r = Y − K を代入する
もし r の値が 0 の場合、⑥へ
変数 r の値を変数 Y へ代入し、②へ
変数 K(=Y)の値が最大公約数である
Sub Macro5()
    Dim K As Long, Y As Long, r As Long, t As Long
    Dim K0 As Long, Y0 As Long     ' もとの値を残しておく
    Dim cnt As Integer

    K = Cells(1, 1).Value
    Y = Cells(1, 2).Value
    K0 = K: Y0 = Y                 ' ← 最小公倍数のために必要

    Do
        If K > Y Then              ' ② 交換
            t = K: K = Y: Y = t
        End If
        r = Y - K                  ' ③
        cnt = cnt + 1
        If r = 0 Then Exit Do      ' ④
        Y = r                      ' ⑤
    Loop

    Cells(2, 1) = "最大公約数 GCD"
    Cells(2, 2) = K
    Cells(3, 1) = "最小公倍数 LCM"
    Cells(3, 2) = K0 * Y0 / K
    Cells(4, 1) = "引き算をした回数"
    Cells(4, 2) = cnt
End Sub

最小公倍数の式に注意。 元資料は「最小公倍数を求めるには LCM = K * Y / GCD の公式を用います」と書いていますが、 ⑥ の時点では K も Y も最大公約数になっています (だから「変数 K(=Y)の値が」と書かれているわけです)。 そのまま当てはめると GCD × GCD ÷ GCD = GCD になってしまいます。
正しくはもとの 2 つの値を使って LCM = K0 * Y0 / GCD です。 上のプログラムでは K0Y0 に最初の値を保存しています。 84 と 36 なら GCD = 12、LCM = 84×36÷12 = 252 です。

5.2 元資料の手順(2 つ目)── これは終わりません

元資料 13 枚目には、⑤を細かく分けたもう 1 つの手順が載っています。

2 つの正の整数を変数 K と変数 Y へ各々代入する
K > Y の場合 K と Y を交換する
変数 r = Y − K を代入する
もし r の値が 0 の場合、⑦へ
もし K > r の場合、K の値を Y へr の値を K へ代入し、③へ
もし K ≦ r の場合、r の値を K へ代入し、③へ  ← ここで Y が変わらない
変数 K(=Y)の値が最大公約数である

この手順は、どんな 2 つの数を入れても終わりません。 ⑥ で K に r を入れるだけで Y を変えていないため、 同じ状態を行き来してしまいます。 たとえば K=4, Y=6 でたどると、こうなります。

KYr = Y−Kどの枝へ
462⑤(K>r)→ Y=4, K=2
242⑥(K≦r)→ K=2(変わらない)
242⑥ → K=2 2 行前と同じ状態
242⑥ → 以下、永久に同じ

1〜40 のすべての組(1600 通り)で確かめましたが、 1 つも終わりませんでした。 r が 0 になる枝に到達できないからです。

Sub Macro6()
    Dim K As Long, Y As Long, r As Long, t As Long

    K = Cells(1, 1).Value
    Y = Cells(1, 2).Value

    If K > Y Then              ' ②(1 回だけ)
        t = K: K = Y: Y = t
    End If

Step3:
    r = Y - K                  ' ③
    If r = 0 Then GoTo Owari   ' ④
    If K > r Then              ' ⑤
        Y = K
        K = r
    Else                       ' ⑥ ← ここが問題
        K = r
    End If
    GoTo Step3

Owari:
    Cells(2, 1) = "GCD"
    Cells(2, 2) = K
End Sub

直し方。⑥ でY のほうを更新すれば正しく動きます。

もし K ≦ r の場合、r の値を Y へ代入し、③へ

こうすると K ≦ Y が保たれ、Y が必ず小さくなるので、いつか r が 0 になります。 この直した版を 1〜40 の全 1600 組で確かめ、すべて正しい最大公約数を返しました。 下で試せます。

Sub Macro7()
    Dim K As Long, Y As Long, r As Long, t As Long
    Dim cnt As Integer

    K = Cells(1, 1).Value
    Y = Cells(1, 2).Value

    If K > Y Then
        t = K: K = Y: Y = t
    End If

Step3:
    r = Y - K
    cnt = cnt + 1
    If r = 0 Then GoTo Owari
    If K > r Then
        Y = K
        K = r
    Else
        Y = r                  ' ← ここを直した
    End If
    GoTo Step3

Owari:
    Cells(2, 1) = "GCD"
    Cells(2, 2) = K
    Cells(3, 1) = "引き算の回数"
    Cells(3, 2) = cnt
End Sub

5.3 なぜ「終わること」を確かめなければならないか

第 6 章でアルゴリズムの条件として有限性(いつか必ず終わる)を挙げました。 上の例が、まさにそれです。手順が「正しそうに見える」ことと「終わる」ことは別です。

終わることを確かめる定石は 「毎回かならず小さくなる量があるか」を探すことです。

この「減っていく量」を見つける考え方は、 第 14 章の再帰で「終了条件に近づいているか」を確かめるときにも、そのまま使います。

5.4 割り算を使う版(こちらが普通)

引き算を繰り返すかわりに、余りを 1 回で求めれば速くなります。 元資料は引き算版だけですが、ユークリッドの互除法として広く知られているのは Mod を使う形です。 84 と 36 の場合、引き算版は 5 回まわりますが、余り版は 2 回で終わります。 数が離れていると差は劇的です ── 1000 と 3 なら、引き算版は 336 回、余り版は 2 回 (下のモデルで実際に数えられます)。

Sub Macro8()
    Dim K As Long, Y As Long, r As Long, t As Long
    Dim c1 As Long, c2 As Long
    Dim A As Long, B As Long

    A = Cells(1, 1).Value
    B = Cells(1, 2).Value

    ' 引き算版
    K = A: Y = B: c1 = 0
    Do
        If K > Y Then
            t = K: K = Y: Y = t
        End If
        r = Y - K
        c1 = c1 + 1
        If r = 0 Then Exit Do
        Y = r
    Loop
    Cells(2, 1) = "引き算版 GCD": Cells(2, 2) = K
    Cells(2, 3) = c1 & " 回"

    ' 余り版(Mod を使う)
    K = A: Y = B: c2 = 0
    Do While Y <> 0
        r = K Mod Y
        K = Y
        Y = r
        c2 = c2 + 1
    Loop
    Cells(3, 1) = "余り版 GCD":   Cells(3, 2) = K
    Cells(3, 3) = c2 & " 回"

    Cells(5, 1) = "最小公倍数"
    Cells(5, 2) = A * B / K
    Cells(7, 1) = "A1・B1 を 84 と 36 に変えて比べてみてください"
End Sub

6. 素因数分解(元資料 14 枚目)

任意の正の整数を素因数分解するプログラムを作成せよ。

考え方は単純です。2 から順に割ってみて、割り切れる限り割り続ける。 割り切れなくなったら次の数へ進みます。 「割り切れる限り」は回数が決まらないので、ここでも Do 〜 Loop が要ります。

Sub Macro9()
    Dim n As Long
    Dim d As Long
    Dim r As Integer          ' 書き込む列
    Dim shiki As String

    n = Cells(1, 1).Value
    Cells(1, 2) = "="
    r = 3
    shiki = ""

    d = 2
    Do While d * d <= n              ' √n まで調べれば足りる
        Do While n Mod d = 0         ' 割り切れる限り割り続ける
            Cells(1, r) = d
            r = r + 1
            shiki = shiki & d & " × "
            n = n / d
        Loop
        d = d + 1
    Loop

    ' 残った n が 1 でなければ、それ自身が素数
    If n > 1 Then
        Cells(1, r) = n
        shiki = shiki & n & " × "
    End If

    Cells(3, 1) = "式"
    Cells(3, 2) = Left(shiki, Len(shiki) - 3)   ' 末尾の "×" を削る
End Sub

360 = 2 × 2 × 2 × 3 × 3 × 5 と出ます。 Do While入れ子になっているところが要点です ── 外側は「割る数 d を進める」、内側は「同じ d で割り切れる限り割る」。 360 は 2 で 3 回割れるので、内側が 3 回まわります。

d * d <= n という条件は、 §3 で触れた「√n まで調べれば足りる」を、 平方根を計算せずに書いたものです(両辺を 2 乗した形)。 d <= Sqr(n) と書くよりも速く、誤差の心配もありません。

7. まとめ

  1. Do 〜 Loop は 4 通り。前判定は 0 回もありうる、後判定は必ず 1 回通る
  2. 回数が決まらない繰り返し(倍々貯金・互除法・収束計算)は Do で書く
  3. 倍々に増えるものは Long にする。100 万円超えは 20 日目・1,048,575 円
  4. 約数は Mod が 0 かで判定。n まで調べれば足りる
  5. 互除法は大きい方から小さい方を引いても公約数は変わらないことを使う
  6. 最小公倍数には「もとの 2 つの値」が必要。互除法は K と Y を壊してしまう
  7. 終わることを確かめるには「毎回かならず小さくなる量」を探す

章末問題

付録:元資料からの修正一覧

もとにしたのは「情報学入門(VBA)145-2019」全 25 枚です。

元資料この教材種別
13 枚目の互除法の手順(2 つ目)の⑥「K ≦ r の場合、r の値を K へ代入し、③へ」 この手順はどんな入力でも終わりません。 Y が更新されないため同じ状態を行き来します。 1〜40 の全 1600 組で確認し、1 つも終わりませんでした。 ⑥ を「r の値を Y へ」に直すと、同じ 1600 組すべてで正しい答えを返します。 12 枚目の 1 つ目の手順は正しいので、そちらを使ってください誤り
「最小公倍数を求めるには LCM = K * Y / GCD の公式を用います」 ⑥ の時点で K と Y はどちらも最大公約数になっているので、 そのまま当てはめると答えが GCD になる。もとの 2 値を別の変数に保存する必要がある不足
互除法が引き算版のみ Mod を使う版を追加。1000 と 3 なら 336 回 対 2 回と桁違いに速いことを、実際に数えて示した不足
Do 〜 Loop の 4 つの形(前判定/後判定 × 続ける/終わる)が整理されていない 4 つを並べ、前判定は 0 回もありうるという違いを実行して確かめられるようにした不足
約数を求めるループが 1 から n まで全部試している n まででよいことを説明し、素因数分解では d * d <= n として実装した不足
倍々貯金の型に触れていない Integer では 15 日目で範囲外エラーになる。Long を使う不足
「アルゴリズムが終わること」の確かめ方が示されていない §5.3 として追加。毎回かならず小さくなる量を探すという定石(第 14 章の再帰にもつながる)不足
10 人の平均・多重ループ(約数)が資料 142 と 145 の両方にあるこの章に集約重複

元資料で正しかったこと。 12 枚目の互除法の手順(1 つ目)は正しく動きます ── 84 と 36 で 5 回の引き算を経て 12 を返すことを確認しました。 互除法の考え方(大きい方から小さい方を引く)、 LCM = 2 数の積 ÷ GCD という関係そのもの、 倍々貯金・約数・素因数分解という課題の立て方はいずれも適切です。 とくに「不定回数の繰り返し」という節を立てて倍々貯金を置いた構成は、 For では書けない例として的確です。