- 物理機リージョン/AZ
- ハイパーバイザVM exit で捕まえる
- VMゲストOS
- コンテナnamespace / cgroup
- プロセスアプリ / DB今ここ
第11期では、増え方で測る・配列とハッシュ・log・ソートの下限などを見ました。第29期はその続きで、手法ごとに「問題のどんな構造を使っているか」を13本で読みます。CLRS を読むための地図です。初回は、後の回で何度も使う道具「ループ不変式」です。
まず結論
テストが全部通っても、分かるのは「試した入力では動いた」までです。ループが正しいと言うには、各周の始めに成り立つ約束(ループ不変式)を1つ決め、①最初に成り立つ ②1周回っても保たれる ③終わったときにそれが答えを意味する、の3つで言います。
約束は assert で毎周確かめられます。範囲を1つずらすと、どの周で約束が破れるかがそのまま見えます。
挿入ソートのループの開始を 1 から 2 に変えました(本文のコードなら range(1, n + 1) の 1 を 2 に。2個目の要素を差し込む周を飛ばす)。値が 0〜50・長さ 2〜30 の乱数列 1,000 本を入れると、毎周の約束を確かめる assert で落ちるのは何本くらいでしょう。
当てることが目的ではありません。外した予測ほど記憶に残るので、直感で選んでください。
テストが言えるのは、試した入力のことだけ
ループを含む関数にテストを20個書き、全部通ったとします。分かるのはその20個の入力では正しい答えが出たことです。21個目は分かりません。
そこで、入力の数ではなくループの形で正しさを言います。道具は、ループの各周の始めに成り立つと決めた約束(ループ不変式)です。確かめるのは次の3つです。
- 最初に成り立つ(1周目に入る前)
- 1周回っても保たれる(周の始めに成り立てば、次の周の始めにも成り立つ)
- 終わったとき、それが答えを意味する
1と2がそろえば、何周回っても約束は破れません。3があれば、終わった時点の約束がそのまま「正しい答え」になります。1は再帰の基底条件と同じ役回りです。
- ・①と②で「何周でも保たれる」、③で「それが答え」
- ・テストの本数は、この3つのどれも示さない
約束を assert にすると、破れる周が見える
約束は Python の assert でそのまま書けます。各周の始めに置き、最後の確認は終了時(i = n)に行います。
def insertion_sort(a):
orig = list(a)
n = len(a)
for i in range(1, n + 1):
assert a[:i] == sorted(orig[:i]), f"i={i}" # 約束
if i == n:
break # ③ 終了時の確認
x = a[i]
j = i - 1
while j >= 0 and a[j] > x:
a[j + 1] = a[j]
j -= 1
a[j + 1] = x
return a
このままの版と、わざと1つずらした版を走らせた結果です(Python 3.14.7 / Linux aarch64、Docker の python:3.14-slim、2026-10-07)。
assert passed on 1000 random lists (len 0..30, values 0..50)input [5, 3, 8, 1] -> fails at i=1: a=[5, 3, 8, 1] a[:i+1]=[5, 3] sorted=[3, 5]input [1, 2, 3, 0] -> fails at i=3: a=[1, 2, 3, 0] a[:i+1]=[1, 2, 3, 0] sorted=[0, 1, 2, 3]input [1, 2, 3, 4] -> no failure (already sorted prefix)range start 2 -> fails at first check i=2: a[:i]=[5, 3] sorted=[3, 5]range(2, n) start: failed 499 of 1000 random lists
正しい約束は、長さ 0〜30 の乱数列 1,000 本ですべて通りました。 確かめる範囲を a[:i+1] に広げる(まだ差し込んでいない要素まで並んでいると主張する)と、[5, 3, 8, 1] は最初の周 i=1 で落ちます。ところが [1, 2, 3, 0] は i=3 まで通り、並び済みの [1, 2, 3, 4] では一度も落ちません。
ループの開始を 1 から 2 にずらす(上のコードなら range(1, n + 1) の 1 を 2 に)と、[5, 3, 8, 1] は最初の確認 i=2 で落ちます。落ちるのは先頭2個が逆順のときだけで、長さ 2〜30 の別の 1,000 本(上の 1,000 本とは別の乱数列)では 499 本が落ちました(値が 0〜50 の一様乱数なら理論上の割合は 25/51 ≈ 0.49。499 はこの1回の乱数での値です)。壊れたコードが約半分のテストを通るということです。
約束を「先頭 i+1 個が並んでいる」に変えた assert は、[5, 3, 8, 1] では i=1 で落ち、[1, 2, 3, 0] では i=3 まで通りました。ここから読み取れることはどれですか。
約束は、ループの外へ出たとき事後条件になる
3の「終わったときに答えを意味する」は、関数の事後条件(終了時に成り立つと約束する条件)につながります。ループ不変式は、事後条件をループの途中まで引き延ばしたものと読めます。
この考え方には2本の古典があります。Floyd 1967 は冒頭で、流れ図の各接続に命題を結びつけ、その接続を通るたびに成り立つと主張する、という枠組みを示しています。
an association of a proposition with each connection in the flow of control through a program, where the proposition is asserted to hold whenever that connection is taken.
Hoare 1969 はこれをプログラムの文面に置き直し、繰り返しを Rule of Iteration として扱います。
Suppose P to be an assertion which is always true on completion of S, provided that it is also true on initiation. Then obviously P will still be true after any number of iterations of the statement S (even no iterations).
誰が最初かは言い切れません。 どちらも先行する考えに由来すると書いています。Hoare は「The treatment given below is essentially due to Floyd [8] but is applied to texts rather than flowcharts.」と書き、Floyd は自分の証明の型について「not original; they are based on ideas of Perlis and Gorn」と書いています。Hoare 1969 の6ページを目視した範囲では invariant という語は見当たりません。
bisect は事後条件を docstring に書いている
第11期『半分にすると、なぜ20回で済むのか』では、二分探索を「値ではなく境目を探すもの」として見ました。その境目を、CPython は事後条件として書いています。Lib/bisect.py(3.14 ブランチ)の bisect_left の docstring です。
The return value i is such that all e in a[:i] have e < x, and all e in a[i:] have e >= x. So if x already appears in the list, a.insert(i, x) will insert just before the leftmost x already there.
HTML の公式文書は同じことを別の書き方で載せています。
all(elem < x for elem in a[lo : ip]) is true for the left slice and all(elem >= x for elem in a[ip : hi]) is true for the right slice.
同じファイルの Python 版のループです(実行時は、ファイル末尾の from _bisect import * で C 実装に置き換わります)。
while lo < hi:mid = (lo + hi) // 2if a[mid] < x:lo = mid + 1else:hi = midreturn lo
ここでの約束は、本教材の読みとして「a[:lo] はすべて x 未満、a[hi:] はすべて x 以上」です。最初は両側が空なので成り立ちます。どちらの枝も、確かめた側へだけ lo か hi を動かすので保たれます。終了時は lo == hi で、docstring の事後条件そのものになります。
CPython の bisect_left のループ(lo と hi を縮める)が毎周守っていて、終了時に docstring の事後条件そのものになる約束はどれですか。
挿入ソート
約束は「先頭 i 個は、元の先頭 i 個を並べたもの」。i=1 では自明で、1周で1個ずつ差し込んで保ち、i=n で全体が並ぶ。assert で毎周確かめられ、range を1つずらすと落ちる周が見える。
bisect_left
約束は「a[:lo] はすべて x 未満、a[hi:] はすべて x 以上」。最初は両側が空で、1周で lo か hi を確かめた側へ動かして保ち、lo == hi で docstring の事後条件になる。
並べ替えと探索。していることは違いますが、正しさの言い方として共通している形は何でしょうか。
それぞれの約束について「i や lo・hi が最初の値のとき」「1周後」「ループを抜けたとき」に何が言えるかを並べてみてください。
この章のまとめ
- テストが示すのは試した入力のことだけ。 range を1つずらした挿入ソートは、乱数列 1,000 本のうち 501 本を通った。
- ループの正しさは不変式で言う。 ①最初に成り立つ ②1周で保たれる ③終了時に答えを意味する。
- 約束は assert で毎周確かめられる。 ずらすと破れる周が見え、その周は入力によって変わる。
- 不変式は事後条件をループの途中まで引き延ばしたもの。 CPython の bisect_left は事後条件を docstring に書いている。
- 誰が最初かは言い切らない。 Hoare 1969 は Floyd 由来と書き、Floyd 1967 は Perlis と Gorn の考えに基づくと書いている。
- 既存クイズでは、「アルゴリズムとデータ構造 III(発展編)」の algorithm-3-20(ループ不変条件)、「アルゴリズムとデータ構造」の algorithm-7(①に当たる再帰の基底条件)がこの章の結論の一行版です。
- CLRS(『アルゴリズムイントロダクション』)の目次や索引で loop invariant(ループ不変式)を探すと、この回の内容に対応する章や節が見つかります。章の題と番号は版で変わるので、手元の目次で確かめてください。
自分の現場で確かめる
読んだだけでは身につきません。手元の環境で実際に見てみてください。
理解度チェック
読んだ直後は「分かった」と感じやすいものです。実際に解いて確かめてください。
ループ不変式でループの正しさを言うとき、確かめる3つの組はどれですか。
用語集(3語)
- ループ不変式(loop invariant)
- ループの各周の始めに成り立つと決めた約束。最初に成り立ち、1周で保たれ、終了時に答えを意味することを示すと、ループの正しさが言える。
- 事後条件
- 処理が終わったときに成り立つと約束する条件。bisect_left の docstring の all e in a[:i] have e < x, and all e in a[i:] have e >= x がその例。
- assert
- 条件が偽なら AssertionError で止める Python の文。不変式を毎周確かめる道具になる。証明ではなく、実行した入力についての検査。
出典(一次ソース)
- C. A. R. Hoare, "An Axiomatic Basis for Computer Programming", Communications of the ACM 12(10), 1969, pp. 576–580, 583 ↗
- Robert W. Floyd, "Assigning Meanings to Programs", Proceedings of Symposia in Applied Mathematics 19, AMS, 1967, pp. 19–32 ↗
- CPython 3.14 — Lib/bisect.py(docstring) ↗
- Python 公式ドキュメント — bisect ↗