第 7 章の For は回数が決まっているときの道具でした。
いつ終わるか分からないときは Do 〜 Loop を使います。
条件を前に置くか後ろに置くか、続ける条件か終わる条件かで 4 通りあります。
Do While i <= 5
…
Loop
Do Until i > 5
…
Loop
Do
…
Loop While i <= 5
Do
…
Loop Until r = 0
前判定と後判定の違いは、「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
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)、
空のセルでも右側の比較が行われます。
文字列と数値の比較になって、思わぬ結果になることがあります。
「先に守ってから、中で判断する」のが安全な書き方です。
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 まで調べれば足ります (n = a×b なら、a と b の少なくとも一方は √n 以下)。 30 なら 5 まで、10000 なら 100 まで。計算量が桁違いに減ります。
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 型でも足りなくなります)。
型の選び方に注意。
kyou と goukei を Integer にすると、
32,767 を超えた時点でエラーになります(15 日目あたり)。
「倍々に増える」と分かっているものは、最初から Long にしておきます。
上のプログラムで As Long を As Integer に変えて試すと、
どこで止まるかが分かります。
ユークリッドの互除法を用いて、2 つの正の整数の最大公約数(GCD)を求めよ。 さらに、最小公倍数(LCM)も求めよ。
元資料 11〜13 枚目です。考え方は 「大きい方から小さい方を引いても、公約数は変わらない」という性質を使います。 引き算を繰り返して、差が 0 になったときの値が最大公約数です。
| ① | 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 です。
上のプログラムでは K0・Y0 に最初の値を保存しています。
84 と 36 なら GCD = 12、LCM = 84×36÷12 = 252 です。
元資料 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 でたどると、こうなります。
| K | Y | r = Y−K | どの枝へ | |
|---|---|---|---|---|
| ③ | 4 | 6 | 2 | ⑤(K>r)→ Y=4, K=2 |
| ③ | 2 | 4 | 2 | ⑥(K≦r)→ K=2(変わらない) |
| ③ | 2 | 4 | 2 | ⑥ → K=2 2 行前と同じ状態 |
| ③ | 2 | 4 | 2 | ⑥ → 以下、永久に同じ |
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
第 6 章でアルゴリズムの条件として有限性(いつか必ず終わる)を挙げました。 上の例が、まさにそれです。手順が「正しそうに見える」ことと「終わる」ことは別です。
終わることを確かめる定石は 「毎回かならず小さくなる量があるか」を探すことです。
この「減っていく量」を見つける考え方は、 第 14 章の再帰で「終了条件に近づいているか」を確かめるときにも、そのまま使います。
引き算を繰り返すかわりに、余りを 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
任意の正の整数を素因数分解するプログラムを作成せよ。
考え方は単純です。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) と書くよりも速く、誤差の心配もありません。
Do 〜 Loop は 4 通り。前判定は 0 回もありうる、後判定は必ず 1 回通るDo で書くLong にする。100 万円超えは 20 日目・1,048,575 円Mod が 0 かで判定。√n まで調べれば足りるもとにしたのは「情報学入門(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 では書けない例として的確です。