この実習でできるようになること
- 現在の連続回数と、これまでの最大回数を分けて追跡できる。
- ループ条件と添字の範囲から、最後の要素と範囲外アクセスを確認できる。
- 通常のデータで隠れる誤りを、境界値の小さな入力で見つけられる。
前提と課題
架空の学習記録アプリで、1日に取り組んだ問題数を配列に記録しています。1日18問以上なら目標達成とし、最も長く連続達成した日数を求めます。配列の要素は0以上の整数、添字は1から始まり、要素数をnとします。
この演習の擬似言語では、whileは条件を先に評価し、偽なら本体を実行しません。代入記号←は右辺を評価して左辺へ設定します。配列の範囲外参照は許さず、空配列のときnは0です。特定のプログラミング言語の添字規則を持ち込まず、ここで示した条件を使ってください。
- 入力配列a
- {12, 18, 18, 9, 20, 21}
- 要素数としきい値
- n=6、target=18、達成条件はa[i]≧target
- current
- 今読んだ日まで連続して達成している日数
- best
- これまでに見つけた最長の連続達成日数
自分で求めてみよう
次の処理が返すbestを求め、各反復後のi、current、bestを追跡してください。続いて、空配列、1要素、配列末尾まで連続する場合に正しく動く理由を説明しましょう。
途中の式や表も紙に書いてから、次の手順と照合してください。
途中の手順と検算
1. 変数の役割を決めてから、処理全体を読む
currentとbestは似ていますが、更新条件が異なります。currentは未達成の日を読むと0に戻ります。bestは過去の最長記録なので、その後に未達成の日が来ても減らしません。この意味を先に書くと、「どちらも毎日リセットする」といった誤りを見つけやすくなります。
i ← 1 current ← 0 best ← 0 while (i ≦ n) if (a[i] ≧ target) current ← current + 1 else current ← 0 endif if (current > best) best ← current endif i ← i + 1 endwhile return best
最大値の更新は、達成判定でcurrentを変更した後に行います。比較がcurrent > bestでもcurrent ≧ bestでも、bestの数値だけを返すこの処理では結果は同じです。ただし、最長区間の開始日も記録する拡張では、同点時に更新するかが「最初の区間」「最後の区間」の違いになります。
2. 一回の反復で変わる値を、実行順どおり記録する
初期値はi=1、current=0、best=0です。1日目の12は未達成なのでcurrentは0のままです。2日目の18は「以上」に含まれるのでcurrentが1、bestも1になります。3日目も18なのでcurrentが2、bestが2へ進みます。比較対象の18を除外してしまうと、この途中経過が変わります。
4日目の9ではcurrentだけを0へ戻します。bestは2を保ちます。5日目の20から新しい連続区間が始まり、6日目の21で再び長さ2になります。下表のiは、その行で参照した添字です。本体の最後ではiが1増えるため、最終行の終了時点ではi=7になります。
| 参照したi | a[i] | 達成判定 | 更新後current | 更新後best |
|---|---|---|---|---|
| 1 | 12 | 偽 | 0 | 0 |
| 2 | 18 | 真 | 1 | 1 |
| 3 | 18 | 真 | 2 | 2 |
| 4 | 9 | 偽 | 0 | 2 |
| 5 | 20 | 真 | 1 | 2 |
| 6 | 21 | 真 | 2 | 2 |
3. 反復の前後で成り立つ性質を使って、更新順を検算する
i番目を処理した直後、currentは「i番目で終わる連続達成区間の長さ」、bestは「1番目からi番目までの最長連続達成区間の長さ」です。達成なら昨日の連続区間に1を足し、未達成ならその日で区間が切れるため0にする、と考えればcurrentの更新を説明できます。
新たな最長記録が生まれる可能性があるのは、今回のcurrentが以前のbestを超える場合だけです。従ってcurrent更新後に最大値を比較すれば、同じ性質が次の反復でも保たれます。表を埋めるだけでなく、この説明ができると、配列が600要素になっても手順の正しさを考えられます。
4. 最初・最後・要素0件を、小さな入力で試す
空配列ならn=0なので、最初の条件1≦0は偽です。a[1]は参照されず、初期値best=0が返ります。1要素の{18}なら1回だけ実行して1、{17}なら0です。値がしきい値と等しいケースを含めると、≧を誤って>にした実装を検出できます。
末尾の取りこぼしには{4, 18, 19, 20}を使います。正解は3です。条件をi < nにするとi=4を処理せず2を返します。一方、元の6要素では最後を飛ばしても前半の最長2が残るので、戻り値だけでは誤りが見えません。通常例が通ったことと、境界が正しいことを分けて検証する必要があります。
| 入力 | 期待するbest | 主に確かめること |
|---|---|---|
| {} | 0 | 要素へ触れる前の終了判定 |
| {18} | 1 | 1要素としきい値ちょうど |
| {17} | 0 | 全て未達成の初期値 |
| {4, 18, 19, 20} | 3 | 最終要素を含む区間 |
5. 計算量と仕様変更の影響を説明する
各要素を一度だけ読み、反復の中では一定個数の比較・加算・代入を行います。従って時間計算量はO(n)、入力配列を除く追加領域はi、current、bestなどの一定個数なのでO(1)です。二つのifがあることを、二重ループと混同しないでください。
もし「最長の区間が何日目から始まったか」まで返したいなら、bestを更新した時点の開始添字i−current+1を記録できます。例えば3日目なら3−2+1=2です。同点で更新しない現在の>を使えば、最後の区間ではなく最初に見つかった最長区間を保てます。返す値が増えたら、同点時や空配列の仕様も追加してから変更します。
解答と、ここまでの要点
返すbestは2です。2〜3日目と5〜6日目に、どちらも2日間の連続達成区間があります。4日目にcurrentは0へ戻りますが、bestは2を保持します。
i≦nによってn番目まで読み、i=n+1になってから終了するため範囲外を参照しません。n=0では本体を一度も実行せず0を返します。
つまずいたときに確認すること
- 「現在の連続区間」と「過去の最大区間」を同じ変数の意味として扱う。
- 「18以上」を「18より大きい」と読み替えて、境界値を除外する。
- 戻り値が合う通常例一つだけで、最終要素も処理したと判断する。
- 最大値を更新する前後の順序や、添字を増やす位置を読み飛ばす。
条件が変わっても説明できますか?
選択後に結果と理由を確かめられます。解答は折りたたみからいつでも読めます。
選んでから確認できます。確認問題は通常の演習記録には加算しません。
解答と理由を見る
解答:current=0、best=2
未達成で途切れたのでcurrentだけ0になります。過去に2日間の連続達成があった事実は消えないためbestは2を保ちます。
選んでから確認できます。確認問題は通常の演習記録には加算しません。
解答と理由を見る
解答:誤った処理は2、正しい値は3
i=1、2、3だけを処理し、最後の20へ到達する前に終了します。2番目と3番目の2日間しか数えないため2です。正しくは2〜4番目の3日間を数えます。
次の学習
手順を見ずにもう一度解き、以下の関連問題でも同じ考え方を使えるか確かめましょう。
- 問54:次の関数rangeSumで累積和prefixを作る。a[3]からa[5]までの合計を返すため、空欄に入る式はどれか。aとprefixの要素番号はどちらも1から始まり、forの両端を含む。prefixの要素数はaより1多…
- 問56:単方向リストを、値を持つdataと次の要素番号を持つnextの二つの配列で表す。要素番号は1から始まり、nextの値0はリストの終端であって配列の要素番号ではない。先頭は要素1である。次の処理で要素4を挿入した後、出力…
- 問67:次の処理は、昇順配列の異なる値を先頭へ詰め直す。終了後のwriteの値と、a[1]からa[write]までの値はどれか。要素番号は1から始まり、forの両端を含む。writeより後ろの値は結果に含めない。 整数型の配列…
- 問68:有向グラフを幅優先探索する。初めにAを訪問済みにしてFIFOキューへ入れる。以後、キューの先頭から頂点を取り出して出力し、その頂点の未訪問の行き先を表の左から順に訪問済みにしてキュー末尾へ入れる。キューへ入れる時点で訪…
参照資料とケースの範囲
計算条件、表、シナリオ、確認問題は本サイト独自のものです。用語と考え方の確認には以下の一次資料を使っています。実務では組織の手順や実際の製品仕様も確認してください。