CLRSの「Introduction to Algorithm」を読んでいます。第2章で、著者は「ループ不変式」に言及しています。ループ不変式とは何ですか?
簡単な言葉で言えば、ループ不変条件は、ループのすべての反復に対して保持される述語(条件)です。たとえば、次のような単純なfor
ループを見てみましょう。
int j = 9;
for(int i=0; i<10; i++)
j--;
この例では、i + j == 9
が(繰り返しごとに)当てはまります。弱い不変式は、i >= 0 && i <= 10
です。
私はこの非常に単純な定義が好きです:( source )
ループ不変条件は、[プログラム変数の中で]ループの各反復の直前と直後に必ず真になる条件です。 (これは、反復の途中でその真実または偽造について何も述べていないことに注意してください。)
それ自体では、ループ不変式はあまり機能しません。ただし、適切な不変式が与えられると、アルゴリズムの正確性を証明するために使用できます。 CLRSの単純な例は、おそらくソートに関係しています。たとえば、ループの不変式を、ループの開始時に、この配列の最初のi
エントリがソートされるようにします。これが実際にループ不変であることを証明できる場合(つまり、各ループ反復の前後に保持される)、これを使用してソートアルゴリズムの正確性を証明できます。ループの終了時に、ループ不変式はまだ満たされています、およびカウンタi
は配列の長さです。したがって、最初のi
エントリがソートされるということは、配列全体がソートされることを意味します。
さらに簡単な例: ループ不変量、正確性、およびプログラム派生 。
ループ不変式を理解する方法は、プログラムについて推論する体系的で正式なツールとしてです。真を証明することに焦点を当てた単一のステートメントを作成し、それをループ不変式と呼びます。これでロジックが整理されます。一部のアルゴリズムの正確性について非公式に議論することもできますが、ループ不変式を使用することで、慎重に考え、推論が気密であることを確認できます。
ループと不変式を扱うとき、多くの人がすぐに気付かないことが1つあります。それらは、ループ不変式と条件付きループ(ループの終了を制御する条件)の間で混乱します。
人々が指摘するように、ループ不変式は真でなければなりません
(ただし、ループの本文中に一時的にfalseになる場合があります)。 一方、ループ条件付きmustは、ループの終了後にfalseになります。そうでない場合、ループは終了しません。
したがって、ループ不変式と条件付きループmustは異なる条件になります。
複雑なループ不変式の良い例は、バイナリ検索です。
bsearch(type A[], type a) {
start = 1, end = length(A)
while ( start <= end ) {
mid = floor(start + end / 2)
if ( A[mid] == a ) return mid
if ( A[mid] > a ) end = mid - 1
if ( A[mid] < a ) start = mid + 1
}
return -1
}
したがって、ループ条件付きseemsはかなり単純です-start> endのときにループが終了します。しかし、なぜループは正しいのでしょうか?それが正しいことを証明するループ不変式とは何ですか?
不変式は論理ステートメントです。
if ( A[mid] == a ) then ( start <= mid <= end )
このステートメントは論理的なトートロジーです-証明しようとしている特定のループ/アルゴリズムのコンテキストでは常に真です。また、ループの終了後、ループの正確性に関する有用な情報を提供します。
A[mid] == a
の場合、a
が配列内にあり、mid
がstartとendの間にある必要があるため、配列内の要素が見つかったために戻る場合、ステートメントは明らかにtrueです。また、start > end
が原因でループが終了した場合、start <= mid
andmid <= end
のような数はあり得ないため、ステートメントA[mid] == a
はfalseでなければなりません。ただし、結果として、全体的な論理ステートメントはnullの意味でまだ真です。 (ロジックでは、ステートメントif(false)then(something)は常にtrueです。)
では、ループが終了したときにループ条件が必ずしもfalseであると言ったことについてはどうでしょうか。要素が配列内で見つかると、ループが終了したときにループ条件が真になります!?暗黙のループ条件は実際にはwhile ( A[mid] != a && start <= end )
であるため、実際にはそうではありませんが、最初の部分が暗黙であるため、実際のテストを短縮します。この条件は、ループの終了方法に関係なく、ループ後は明らかにfalseです。
以前の回答では、非常に良い方法でループ不変式を定義しました。
以下は、CLRSの作成者がループ不変式を使用して、挿入ソートの正確さを証明する方法です。
挿入ソートアルゴリズム(ブックに記載):
INSERTION-SORT(A)
for j ← 2 to length[A]
do key ← A[j]
// Insert A[j] into the sorted sequence A[1..j-1].
i ← j - 1
while i > 0 and A[i] > key
do A[i + 1] ← A[i]
i ← i - 1
A[i + 1] ← key
この場合のループ不変式:Sub-array [1 to j-1]は常にソートされます。
これをチェックして、アルゴリズムが正しいことを証明しましょう。
初期化:最初の繰り返しj = 2の前。したがって、サブアレイ[1:1]がテスト対象のアレイです。要素が1つしかないため、ソートされます。したがって、不変式が満たされます。
Maintenance:これは、各反復後に不変式をチェックすることで簡単に検証できます。この場合、満足です。
終了:これは、アルゴリズムの正確性を証明するステップです。
ループが終了すると、j = n + 1の値。ループ不変条件も満たされています。これは、Sub-array [1 to n]がソートされることを意味します。
これが私たちのアルゴリズムでやりたいことです。したがって、私たちのアルゴリズムは正しいです。
すべての良い答えに加えて、私はアルゴリズムについて考える方法、ジェフ・エドモンズからの素晴らしい例を推測することができます非常によく概念:
例1.2.1「Find-Max 2本指アルゴリズム」
1)仕様:入力インスタンスは、要素のリストL(1..n)で構成されます。出力は、L(i)が最大値になるようなインデックスiで構成されます。この同じ値を持つ複数のエントリがある場合、それらのいずれかが返されます。
2)基本手順:2本指の方法を決定します。あなたの右の指がリストを駆け下ります。
3)進捗状況の測定:進捗状況の測定値は、リストに沿って右指がどれだけ離れているかです。
4)ループ不変式:ループ不変式は、左指が右指でこれまでに遭遇した最大のエントリの1つを指していることを示します。
5)主な手順:各反復で、リスト内の1つのエントリを右指で下に移動します。右の指が左の指のエントリよりも大きいエントリを指している場合は、左の指を動かして右の指に合わせます。
6)進行:右の指が1つのエントリを移動するため、進行します。
7)ループ不変式の維持:ループ不変式が次のように維持されていることがわかります。各ステップで、新しい左指要素はMax(古い左指要素、新しい要素)です。ループ不変式により、これはMax(Max(shorter list)、new element)です。数学的には、これはMax(長いリスト)です。
8)ループ不変式の確立:最初に、両方の指を最初の要素に向けることにより、ループ不変式を確立します。
9)終了条件:あなたの右の指がリストを横断し終えたとき、あなたは終わっています。
10)終わり:最終的に、問題は次のように解決されます。終了条件では、右指がすべてのエントリに遭遇しています。ループ不変量により、左指はこれらの最大値を指します。このエントリを返します。
11)終了および実行時間:必要な時間は、リストの長さの一定の倍数です。
12)特殊なケース:同じ値のエントリが複数ある場合、またはn = 0またはn = 1の場合に何が起こるかを確認します。
13)コーディングと実装の詳細:...
14)形式的証明:アルゴリズムの正確性は、上記の手順に従います。
ループ不変式は、各反復の開始時およびループの終了時に真でなければならない変数間の重要な関係を表すアサーションと見なされる場合、反復アルゴリズムの設計に役立つことに注意してください。これが当てはまる場合、計算は有効性に向かっています。 falseの場合、アルゴリズムは失敗しています。
この場合の不変とは、すべてのループ反復の特定のポイントで真でなければならない条件を意味します。
コントラクトプログラミングでは、不変式は、パブリックメソッドが呼び出される前後に(コントラクトによって)trueでなければならない条件です。
不変式の意味は決して変わらない
ここで、ループ不変式とは、「ループ内の変数に発生する変化(増分または減分)がループ条件を変更していない、つまり条件が満たされている」ことを意味するため、ループ不変式の概念は
コメント権限がありません。
@Tomas Petricekおっしゃるように
これも真であるより弱い不変式は、i> = 0 && i <10(これは継続条件だからです!)
どのようにループ不変ですか?
私が理解する限り、私が間違っていないことを願っています[1]、ループの不変式はループの開始時に真になり(初期化)、各反復の前後に真になり(メンテナンス)、ループの終了後にも真になります(終了)しかし、最後の反復の後、iは10になります。したがって、条件i> = 0 && i <10はfalseになり、ループを終了します。ループ不変条件の3番目のプロパティ(終了)に違反します。
[1] http://www.win.tue.nl/~kbuchin/teaching/JBP030/notebooks/loop-invariants.html
ループで何が起こっているかを追跡するのは困難です。目標動作を達成せずに終了または終了しないループは、コンピュータープログラミングの一般的な問題です。ループ不変式が役立ちます。ループ不変条件は、プログラム内の変数間の関係に関する正式なステートメントであり、ループが実行される直前(不変条件を確立する)にtrueを保持し、ループの下部で、ループを通るたびに(不変条件を維持する)再びtrueになります)。コードでのループ不変条件の一般的な使用パターンは次のとおりです。
... //ループ不変式はここで真でなければなりません
while(TEST CONDITION){
//ループの先頭
...
//ループの下部
//ループ不変式はここで真でなければなりません
}
//終了+ループ不変=ゴール
...
ループの上部と下部の間で、おそらくループの目標に到達するための前進が見られます。これは不変式を妨害する(偽にする)可能性があります。ループ不変条件のポイントは、毎回ループ本体を繰り返す前に不変条件が復元されるという約束です。これには2つの利点があります。
作業は、複雑なデータ依存の方法で次のパスに繰り越されません。他のすべてとは無関係にループを通過する各パスは、不変式がパスを作業全体にバインドする役割を果たします。ループが機能するという理由は、ループを通過するたびにループ不変量が復元されるという理由に帰着します。これにより、ループの複雑な全体的な動作が小さな単純なステップに分割され、各ステップは個別に検討できます。ループのテスト条件は不変式の一部ではありません。それがループを終了させるものです。ループを終了する理由と、ループが終了したときにループが目標を達成する理由を2つ個別に検討します。ループを通過するたびに終了条件を満たすようになると、ループは終了します。多くの場合、これを保証するのは簡単です。固定の上限に達するまで、カウンタ変数を1つステップします。終了の背後にある推論がより困難な場合があります。
ループの不変条件は、終了条件が達成され、不変条件が真である場合に目標に到達するように作成する必要があります。
不変式+終了=>ゴール
シンプルで関連性のある不変条件を作成するには、終了を除くすべての目標達成をキャプチャするための練習が必要です。ループの不変式を表現するには数学記号を使用するのが最善ですが、これが過度に複雑な状況につながる場合は、明確な散文と常識に頼っています。
ループ不変プロパティは、ループ実行のすべてのステップを保持する条件です(つまり、forループ、whileループなど)。
これはループ不変証明に不可欠であり、実行のすべてのステップでこのループ不変プロパティが保持される場合にアルゴリズムが正しく実行されることを示すことができます。
アルゴリズムが正しいためには、ループ不変式は次を保持する必要があります。
初期化(最初)
メンテナンス(以降の各ステップ)
終了(終了したら)
これは、多くのことを評価するために使用されますが、最良の例は、重み付きグラフトラバーサルの貪欲なアルゴリズムです。貪欲なアルゴリズムが最適なソリューション(グラフ全体のパス)を生成するためには、可能な限り最小の重みのパスですべてのノードに接続する必要があります。
したがって、ループ不変プロパティは、パスの重みが最も小さいということです。 beginningではエッジを追加していないため、このプロパティはtrueです(この場合はfalseではありません)。 各ステップで、最も低い重みのエッジ(貪欲なステップ)をたどるので、再び最も低い重みのパスを使用します。 終了で、最小の重み付きパスが見つかったため、プロパティもtrueです。
アルゴリズムがこれを行わない場合、最適ではないことを証明できます。
ループ不変式は、(x=y+1)
などの数式です。その例では、x
とy
はループ内の2つの変数を表します。コードの実行中にこれらの変数の動作が変化することを考えると、x
およびy
の値をすべてテストして、バグが発生するかどうかを確認することはほとんど不可能です。 x
が整数だとしましょう。整数は、メモリ内に32ビットのスペースを保持できます。その数を超えると、バッファオーバーフローが発生します。そのため、コードの実行中、そのスペースを超えないようにする必要があります。そのためには、変数間の関係を示す一般式を理解する必要があります。結局のところ、プログラムの動作を理解しようとするだけです。
簡単な言葉で言えば、それはすべてのループ反復で真であるLOOP条件です。
for(int i=0; i<10; i++)
{ }
これで、iの状態はi<10 and i>=0
であると言えます。
ループ不変式は、ループ実行の前後に真であるアサーションです。