二分探索で学ぶ、問題の解法が定着しない理由と言語化
この記事では、二分探索のアルゴリズムを学習する過程を例として、なぜ特定の問題の解法を理解して定着できる人とそうでない人がいるのか、ということを具体的な過程として解き明かします。できる限り二分探索のアルゴリズムを知らなくても・それの具体的な手順が理解できなくても、構造がわかるように記述するつもりです。
ここで述べたいことは本質的に認知科学や教育学の領域の話です。もっというと、学力喪失という本で述べられている記号接地の難しさの話です。したがって本質的にはプログラミングや二分探索の話ではないのですが、私個人的にはプログラミングを通して初めて問題の構造を理解できたつもりになったので、プログラミング・二分探索を具体例として記述しています。
例:二分探索を習得したい
問題の解法が定着したりしなかったりする構造を示すために、まず二分探索を2つの方法で説明してメンタルモデルを構築する場合において、メンタルモデルが異なっていても同じ出力を生じる という現象について説明します。
二分探索とは、次のような問題を解く時に使えるアルゴリズムです。
例題)
ある整数の配列aがあり、aの要素は昇順で並んでいることがわかっています。数値xがaの何番目にあるかを求めるプログラムを書いてください。
例えば、a=[1,3,5,7,9]でx=7のとき、xはaの4番目にあります。
プログラミングを知らない方への注釈
ここでは、配列とはa[0], a[1], ...のように 添字 0, 1, ...を使ってデータにアクセスできるような構造のこととし、上記の例[1,3,5,7,9]のように書くと、a[0]=1, a[1]=3, a[2]=5, a[3]=7, a[4]=9を満たすものとします。数学でいう数列に近い概念です。プログラミング言語では添字は0始まりで扱われる事が多いため、何番目という普通の日本語の表現と添字は1ずれています。また、配列の各要素へのアクセスに必要なコストは同等であるとします。
[1,3,5,7,9]と書くと、人間はその配列を目で見てすぐに4番目だとわかりますが、コンピュータで配列を処理する場合、コンピュータは配列の要素をすべて同時に処理するといったことが普通はできず、高速に処理させるためにコンピュータの処理方法に即した効率的な方法を考える必要があります。
このとき、すべての要素を順番に調べる方法で答を求めることができますが、もっと効率的な方法があります。それが二分探索です。
【重要な注意】以下、この記事の説明は嘘ではありませんが、学習中の誤解を再現するために、てきとうな条件を満たさない場合はプログラムが正しく動作しないように意図的に記述している場合があります。
説明その1 - 中央の要素に着目する
二分探索では、その配列の中央の要素に着目して考えます。
- 中央の要素が
xであれば、中央の要素の添字(+1)が求める値である。 - 中央の要素が
xより小さければ、この配列は昇順に並んでいるので、中央の要素より前の要素はすべてxより小さい。したがって、中央の要素より後の要素だけを探せばよい。 - 中央の要素が
xより大きければ、この配列は昇順に並んでいるので、中央の要素より後の要素はすべてxより大きい。したがって、中央の要素より前の要素だけを探せばよい。
そうすると、この方法を繰り返し適用することで、配列の中央より左に探している要素があるか、または配列の中央より右に探している要素があるか、要素がある場所を段々と特定していくことができます。
たとえば、a=[1,3,5,7,9]で考えてみます。上記のアイデアに従うと、a[0],a[1],a[2],a[3],a[4]のうち、まず中央にあるa[2]を調べます。するとa[2]=5なので、xはa[3]かa[4]かのいずれかということになります。2要素の厳密な中央は存在しないため、中央として左をとるか右を取るかの自由度はありますが、たかだか3回調べれば答がわかります。先頭から順番に調べる場合は4回調べる必要があるので、調べる回数が少なくなっています。[1]
では、このアイデアを実際にpythonのプログラムで実現してみましょう。答になる可能性のある添字の範囲について、最小値=左端をleft、最大値=右端をrightとして、a[中央の値]の大きさによってleftまたはrightを変更していきます。ここで、中央の値は、最小値と最大値の平均の切り捨て、つまり(left+right)//2で計算ができます。[2]
最初と最後の要素についてのチェックを付け加えると、以下のようなプログラムができました。
def binary_search(a, x):
# 添字を返すので、n番目とは1ずれていることに注意
left = 0
right = len(a) - 1
if a[left] == x:
return left
if a[right] == x:
return right
while True:
middle = (left + right) // 2
if a[middle] == x:
return middle
if a[middle] < x:
left = middle + 1
if a[middle] > x:
right = middle - 1
print(binary_search([1, 3, 5, 7, 9], 7) + 1) # 4
プログラミングを知らない方への注釈
def binary_search(a, x): というのは、aとxからなにかの値を計算して返却する関数(≒サブルーチン)の定義を示しています(defはdefineの略)。
このプログラムは原則として上から順に一行ずつ処理され、left = 0 などというのは、その時点でleftという変数に0を代入するという意味を表しています。プログラムが実行されるとleftの値は動的に変化していくため、恒等式としてのleft=0を意味しているわけではありません。
if a[left] == x:などのifで始まる行は、もしa[left] == xという条件を満たすならば次の行(行頭が下がっている部分)を実行する、もし満たさなければ行頭が下がっている部分をスキップする、というような意味です。
while True:はループ処理を表し、そこから行頭が下がっている部分の処理を繰り返し行うという意味を持ちます。
return leftなどと書いてあるreturnで始まる行は、returnの後に書いてある値をその関数の値として確定して、関数の処理を終了します。
基本的には、日本語で書いていたことをシンプルに実現するようなプログラムになっています。
(このプログラムは一定の条件下では正しく動作しますが、一般的には適切な動作をしません。それはこの段階では意図的なもので、後ほど詳しく説明します。)
ちなみに、その1の説明は2025/10/13時点で日本語Wikipediaの冒頭に記載されている内容とほぼ同じです。
説明その2 - 配列の分割に着目する
二分探索では、探す対象の配列を2つになるべく等分割して、答えが左半分と右半分のどちらにあるか、ということを繰り返して調べていきます。
分割の途中でたまたま答が見つかることもありますが、配列をどんどん二分割することを繰り返して最終的に答を求めるという考え方が重要です。
二分割したときに、どちらの方に答があるか?というのは端の要素を調べるとわかります。つまり、配列aの中身が昇順で並んでいる場合に以下の事が成り立ちます。
- 右側の配列の左端の要素を調べて、左端の要素が
xより小さければ、その配列の中に答えがある。 - 左側の配列の右端の要素を調べて、右端の要素が
xより大きければ、その配列の中に答えがある。 - (もし調べた要素が
xと一致するなら、それはたまたま答である)
そこで、まず配列の両端leftとrightを調べて、それからその配列の中央middleの値を調べ、配列を分割していく(左端と右端の値を更新していく)という方針でプログラムを書いてみましょう。このとき、配列の分割方法を厳密に考えるとa[middle]がどちらに含まれるかという問題がありますが、xがa[middle]でなければmiddleは配列から除外してしまってよいので、以下の方針で実装ができます。[3]
-
a[middle]の値がxより小さければ、配列のmiddleより大きい方に答えがある(つまりleftをmiddle+1に置き換える) -
a[middle]の値がxより大きければ、配列のmiddleより小さい方に答えがある(つまりrightをmiddle-1に置き換える)
これを踏まえて、pythonでプログラムを書いてみましょう。
def binary_search(a, x):
# 添字を返すので、n番目とは1ずれていることに注意
left = 0
right = len(a) - 1
if a[left] == x:
return left
if a[right] == x:
return right
while True:
middle = (left + right) // 2
if a[middle] == x:
return middle
if a[middle] < x:
left = middle + 1
if a[middle] > x:
right = middle - 1
print(binary_search([1, 3, 5, 7, 9], 7) + 1) # 4
奇しくも全く同じプログラムができてしまいました。
背後のメンタルモデルが異なっていても、全く同じプログラムが出力された ということです。
これらのメンタルモデルには優劣はありません。いずれも同じコードを出力しています。ところが、この後の展開において差が生じます。
この考察で作成された二分探索の不備
さて、上記の二分探索は実用上の問題があります。
- xがaの中になかったら無限ループする
- xが複数ある場合に返却される添字の位置が、複数ある中の左端だったり右端だったり色々である
前者は、while True:というループの中でa[middle]とxが一致するまでreturnされることがないので、一致するものが一つもなければwhile True:を永遠に処理することになります。
後者は、例えば
a=[1,5,5,7,9], x=5
と
a=[1,3,5,5,9], x=5
で比較してみるとわかります。上の例でも下の例でも全体の中央=3番目がxであり、上記の処理の結果は3番目となりますが、上の例では左に5が、下の例では右に5があり、同じ数字が並ぶ中での相対的な位置が一定していません。(1つ目の5か、2つ目の5か、ということです)
例題の問題文の中では、このような状況が起こるか起こらないか、ということを意図的に明示していません。xがaの中に存在しない場合にどういう値を返却すべき、ということが問題文で定められていないので、この問題を解くというだけで言えば無限ループするから間違いだとは言えないでしょうが、実用には問題があります。
複数ある場合については、どうでしょうか。重複のない配列に対してのみこのアルゴリズムを適用するという可能性もありますが、例えば「家賃が5万円以下の物件を列挙したい」という場合だとどうでしょう。家賃でソートされた物件の配列から、列挙すべき5万円以下の物件の最後の添字を調べて、配列をそこまでで切るという操作を考えると、ちょうど5万円の物件は残すことなく取りたいので、探す添字は常に最後(右端)であってほしいです。逆に常に最初を求める場合もあるでしょうから(例えば5万円以上の物件を探している場合)、どうあって欲しいかはケースバイケースということになります。
ここで重要なのは、人間は物事を考えるときに、初手から概念を完全に場合分けして考えられるとは限らない ということです。二分探索の例題を見たときに、訓練された人や問題の記述の精度に敏感な人であれば、xと一致しないものがある場合や複数ある場合について考えることができますが、そうでなければ 無意識的にxと一致するものがただ一つある前提で問題を解くためのメンタルモデルを構成してしまう 、ということです。今回の例で言えば、上記のいずれのメンタルモデルにおいても、一致するものが存在しない可能性や複数ある可能性を考慮していません。
このような場合、 自分が暗黙のうちに仮定していたことに気付いてそれを修正する という作業が必要になります。(なにかの物事を考えていたときに、当初考えていた範囲が狭くて、もっと広い範囲で考える必要があったという考慮不足の経験はきっと皆さんもありますよね。)
では、それぞれのメンタルモデルに沿って、修正する場合の例を考えてみます。以下のルールに合わせて修正します。
-
xが存在しない場合は、便宜上0番目を答とする(添字としては-1) -
xが複数存在する場合は、右端(最後)に出てくるxの場所を答とする
中央の要素に着目するメンタルモデルの修正の一例
探す数値がない場合に無限ループしてしまう場合について、挙動を正確に追いかけてみます。
left, rightはループの中でだんだんと近づいていきますが、やがて差が1になると、次にleftとrightが全く同じ値になります。
つまり、left == rightになったら処理を打ち切るのがよさそうです。いくつか表現がありますが、
while left < right:
とするのが分かりやすくてよいでしょう。こうすることでループを抜けます。ループを抜けたあとに
return -1
とすれば、xが存在しない場合について追加されたルールを満たします。
では、複数同じ値がある場合はどうでしょうか。もともと、以下のような考え方をしていました。
- 中央の要素が
xであれば、中央の要素の添字(+1)が求める値である。 - 中央の要素が
xより小さければ、この配列は昇順に並んでいるので、中央の要素より前の要素はすべてxより小さい。したがって、中央の要素より後の要素だけを探せばよい。 - 中央の要素が
xより大きければ、この配列は昇順に並んでいるので、中央の要素より後の要素はすべてxより大きい。したがって、中央の要素より前の要素だけを探せばよい。
しかし、xが複数ある場合、
- 中央の要素が
xであれば、中央の要素の添字(+1)が求める値である。
とは言えず、xになる右端を探す必要があります。
そこで、a[middle] == x の分岐を削除して、かわりにleftを更新する条件をa[middle] <= xに変えるとどうか?などと考えてみますが、中々うまくいきません。以下のような試行錯誤の末、いくつかのパターンでテストして動作したプログラムを回答としました。
-
a[middle] == xの分岐を削除 -
leftを更新する条件をa[middle] <= xにする -
left = middle + 1だとa[middle] == xの場合に正しく動作しないので、端を含むようにleft = middleにする - 今度は無限ループする場合があったので、ループから抜けるための処理
breakを追加 - 正しい値を
returnするために、left, rightの処理をとりあえずループの下に持ってきてみる -
a[left]とa[right]の判定について、rightの判定を先にする - いくつかのパターンで動作したので回答とする
これは、理屈があまりよくわかっていないものの、とりあえず動作しているように見えるコードができた、という状態です。
def binary_search(a, x):
# 添字を返すので、n番目とは1ずれていることに注意
left = 0
right = len(a) - 1
while left < right:
middle = (left + right) // 2
if left == middle:
break
if a[middle] <= x:
left = middle
if a[middle] > x:
right = middle - 1
if a[right] == x:
return right
if a[left] == x:
return left
return -1
簡易テスト
print(binary_search([1, 3, 5, 7, 9], 5) + 1) # 3
print(binary_search([1, 5, 5, 7, 9], 5) + 1) # 3
print(binary_search([1, 3, 5, 5, 9], 5) + 1) # 4
print(binary_search([1, 3, 5, 5, 5], 5) + 1) # 5
print(binary_search([1, 3, 5, 7, 9], 6) + 1) # 0
この答えを導いた過程は、文章題をうまく解けない小学生が、問題に出てくる数値をデタラメに四則演算に当てはめて解いているのと通じる部分があります。いわゆる記号接地できていない状態になります。
配列の分割に着目するメンタルモデルの修正の一例
探す数値がない場合については、先ほどと同様なので省略します。複数同じ値がある場合について考えましょう。xが一つだけある場合の方針は次のとおりでした。
- 右側の配列の左端の要素を調べて、左端の要素が
xより小さければ、その配列の中に答えがある。 - 左側の配列の右端の要素を調べて、右端の要素が
xより大きければ、その配列の中に答えがある。 - (もし調べた要素が
xと一致するなら、それはたまたま答である)
xが複数存在し得る場合は、次のようにできます。
- 右側の配列の左端の要素を調べて、左端の要素が
x以下ならば、右側の配列の中に答えがある。(または、答が存在しない)- したがって、
leftを更新する
- したがって、
- 左側の配列の右端の要素を調べて、右端の要素が
xより大きければ、左側の配列の中に答えがある。(または、答が存在しない)- したがって、
rightを更新する
- したがって、
ループをいつまで繰り返すか?ということですが、もしleftとrightが隣接した状態(または同じ状態)になった場合は、ループを終了してよいでしょう。この状態は 配列の要素がleftとrightのたかだか2つまで減っている状態 と言えます。この条件はいろいろな記述方法がありますが、leftとrightの間の要素がないという意味でleft == middleでよいでしょう。
ループの更新条件から、
-
leftより左にxが存在する場合、leftはxである -
rightより右にはxは存在しない
ということが言えるので、ループが終了したら以下の条件で返却する値を決められます。
-
a[right]がxの場合は、rightが答(rightより右にはxはないため) -
a[right]がxでなく、a[left]がxの場合は、leftが答 - いずれも
xでない場合は、答なし(-1)
このメンタルモデルに従ってプログラムを書くと、以下のようになります。
def binary_search(a, x):
# 添字を返すので、n番目とは1ずれていることに注意
left = 0
right = len(a) - 1
while left < right:
middle = (left + right) // 2
if left == middle:
break
if a[middle] <= x:
left = middle
if a[middle] > x:
right = middle - 1
if a[right] == x:
return right
if a[left] == x:
return left
return -1
奇しくも全く同じプログラムができてしまいました。
しかし、一例として挙げたプログラムが出来上がるまでのプロセスは全く違っていました。
前者の例ではうまくいかない部分を試行錯誤で訂正してなんとか正しいコードができました。しかし、それを作ったメンタルモデルが出来ているわけではなく、とりあえず正しい動作をしてそうである、というような理解です。それに対して、後者の例ではある程度自然にメンタルモデルを修正して正しい結論に至りました。コードの意味についても、とりあえずうまく行ったということではなくて、その背後でやりたいことがはっきりしています。
なぜ問題を解いても問題の解法が定着しないのか
ここが、この記事で最も強調したいことです。なぜ、同じ問題を"同じように"解いても問題の解法がうまく定着しない場合があるのか。私が見出した要素は3つあります。
-
人間は物事を考えるときに、初手から概念を完全に場合分けして考えられるとは限らない
- 初手で暗黙のうちに仮定されていたことに気づいて修正する必要がある
-
問題を解くにあたって各人が構成しているメンタルモデルは、アウトプットのレベルで同一であったとしても異なる場合がある
- メンタルモデルが異なる場合は、異なる説明が必要になる
-
メンタルモデルを修正できなくても、強引に問題を解くことはできる
- 理屈はわからないが正しく動作する、というものを作れる
ここで、強引に問題を解いた状態で、その解法だけを暗記しようとしても、類似の問題を解くためのメンタルモデルができている訳ではないため、試行錯誤を繰り返したり、正しい回答ができなかったりします。したがって、試行錯誤で問題を解いた場合には、改めてそれを自分の中で筋が通るように理解し直して、自分が自由自在に使える知識として定着させるための努力が必要です。(後述するように、それは一人でやる必要はないと思います。既にできる人がいる場合は、その人に教えてもらうこともできます。)
どのように学習するのがよいか
では、どのように学習をすれば、解法が定着しない状態を避けることができるか。ということについて述べます。
いわゆる応用力はメンタルモデルによって生じる、という認識を持つ
まず、問題を解くにあたって、解いた結果や解けたという事実(だけ)ではなくて、その問題を継続的に解けるようになるためのメンタルモデルを自分の中に作る、ということを意識します。プログラムの場合、試行錯誤でコードを書いてなんとかテストをパスすればOK、では自分自身の活用可能な知識として定着しません。自分自身の活用可能な知識として定着させるために、適切なメンタルモデルを作ることを意識します。
ただ、そうは言っても、メンタルモデルを作る経験を意識的にしていなかった人がいきなりこの文章を見て実感するのは難しいと思います。そのような場合は、まず手や体を何度も動かして、メンタルモデルができるとどういう状態になるかを知ることから始めるべきと思います。
プログラミングの場合、具体的な方法としてはコーディングの練習方法などが良いかと思います。(この方法は後述の事柄も網羅しています)
理解している人のメンタルモデルを詳しく聞いて、メンタルモデルを模倣構築する
理解している人から、答そのものだけではなくて、どう考えてそのような答を得るのか・今回の例であれば何を考えてそのプログラムを書いたのか、ということを聞き出し、また模倣するようにします。答を直接模倣するのではなくて、答をアウトプットするためのメンタルモデルの部分を模倣する、ということです。
とりあえず問題を解ける状態をゴールにしない、問題を理解して解ける・他の人に実際に説明できる状態を最低限のゴールにする
同じようなことを書いていますが、とりあえず問題を解ける状態をゴールにしない、ということがあります。問題を理解して解ける、他の人に自分のメンタルモデルを説明できるという状態を最低限のゴールと考えて、そうなるまで問題を解くことを繰り返します。
おまけ
メンタルモデルの絶対的な優劣を論じているわけではない
ここで念の為に、この記事では上記の2つのメンタルモデルの絶対的な優劣を論じているわけではない、ということを記しておきます。今回提示した例は、ある程度自然な考えで導き出されるものだとは思っていますが、一方で中央の要素への着目から記述している日本語Wikipediaには、(2025/10/13時点で)無限ループせず、かつ常に右端を返す自然なコードへの言及があります。
def binary_search(a, x):
# 添字を返すので、n番目とは1ずれていることに注意
left = 0
right = len(a) # rightには常に配列外が入るようにする
while left < right:
middle = (left + right) // 2
if a[middle] <= x:
left = middle + 1
if a[middle] > x:
right = middle
if a[right - 1] == x:
return right - 1
return -1
これは、rightに敢えて「確実に条件を満たさない添字」を保持することで、最終的にright - 1を答にするという考え方です。本題から外れてしまうので詳しく解説しませんが、このあたりの難しさは、配列を半分にしていくというよりは、配列の端をどう処理するかということにあり、rightが条件を満たす可能性があるのか、それともrightは条件を満たさないことを保証されているのか(最終的にright - 1が答えになるのか)、ということで簡略化できます。これは、一部では閉区間(条件を満たす可能性がある)・閉区間(条件を確実に満たさない)と呼ばれていて、この場合は[left, right)というright側が開区間というような考え方になっています。
個人的な原体験とその影響
私自身は、二分探索に対して分割に着目するようなメンタルモデルをかなり前から持っていました。なぜそうなったのかを紐解いてみると、社会人になって日が浅い頃、上司がバグを探すときの方法論の一つとして二分探索を語っていたからです。彼はコードを全く書かない"システムエンジニア"でしたが、コーヒーを飲みながら同僚に語っていた様子をよく覚えています。
「眼の前にバグっているコードがあるとして、そのバグを探すにも色々効率があるじゃん。上から一行ずつ探すのか、ログを差し込むとかコードを消すとかして、半分ずつに分けて調べるのか。二分探索ってあるでしょ?」[4]
その頃すでに私の中で二分探索の概念はあったと思いますが、なるほど、言われてみれば、ある程度大きいコードからバグを探すときにはそういう考え方もあるのか、と深く印象に残りました。それで、私の中では、二分探索の本質は範囲を分けて調べるということにあって、真ん中の要素はむしろ人為的に引かれたprint()のログ出力の線だったり、あるいは消されたコードだったりして、中央の要素をどう捌くかというのは原理的に着目不可能なケースもあったりしました。
...というほっこりエピソードを披露することが目的ではなく、そのような予測のつかないことによってメンタルモデルは影響を受ける、というのが言いたいことでした。たまたま学校の先生にこう言われた、たまたま友達にこう言われた、たまたま上司にこう言われた、...が時として大きくメンタルモデルに影響し得る、ということです。
なお、一方で、配列にしてもコードの行にしても、それは一つずつ増えていく離散集合であって、二分探索で閉区間とか開区間とかいうのは位相の概念的な意味で全く理解できませんでした。離散集合では閉=開だからです。right)と書くときにrightは含まれない、というニュアンスを言いたいということは理解しましたが、だいぶ時間がかかったと思います。これも、第三者からすぐに分かることかというと、どうでしょうか。
問題を解けばよいわけではない
例題では、敢えて必ずしも厳密に定義されているわけではない問題文を取り扱いました。大学入試では悪問とされる可能性のある問題ですが、しかし入社試験においてはしばしばこういった出題がなされます。それはなぜでしょうか。
例えばソフトウェアエンジニアの仕事をする場合、解決すべき問題は事前に明確に定義されているとは限りません。なんとなく定義されている場合や、誤った定義をされている場合もあります。そういった場合に、会話を通して問題の定義をやり直すあるいは補足するということが日常的に行われます。ソフトウェアエンジニアの仕事の目的は特定の問題を解くことではなくて、人間の役に立つことをすることだからです。問題を解けること自体は大事ですが、解ければ何でも良いわけではありません。
さらに、この記事で述べたいこととしては、日常に存在している問題は常にきれいに定義されている訳ではないので、日常生活を通して学習することは自然と偏っている ということです。それまでに構築している様々なメンタルモデルや思考様式によって思考の進む方向が違うので、
仮定を闇雲に疑えばよいわけではない
では、問題として考えていることについて、あらゆることを闇雲に疑えばよいかというと、それも違うと思っています。
例えば二分探索が前提としていることに、配列が事前に整列していることがあります。この整列していることを疑うと、どうなるでしょうか。例えば、二分探索のロジックの中に整列していることの確認を組み込むとします。整列していることを厳密に確認するには、要素数をxを探せばよいということになり、二分探索そのものにバリデーションを組み込むのは意味がありません。事前にソートしておくなどして、配列が整列していることは保証するしかないでしょう。
こうしたことを考えること自体には意味があると思いますが、それを最終的に実装すればよいかというと、そうでもありません。考えたうえで実装しないということもあります。
効率的な疑い方としては、「問題で言及されていないケースはないか・問題がすべてのパターンを尽くしているか」という疑い方があると思います。今回の場合では配列の中身が昇順に整列していることは明示されているので、まずはそれを疑うよりも、xがaに含まれる個数は何個なのかとか、そういった観点で考えるとよいでしょう。[5]
全く完璧ではないこの記事を世に出そうと思った理由
この記事は少し前に書いていましたが、思うところがあって世に出せませんでした。というのは、「ここに書いてあることは、ソフトウェアエンジニアリング協会のやり方で自分で体感したら、もっと自分の感覚で理解できる(のではないか)」ということがあり、また「逆にここで中途半端に考えを知ってしまうことで、ちょうどよい題材として二分探索を自分で考える機会を奪うことになる(可能性がある)」ということがありました。これを危惧して、学習の足を引っ張りたくなかったために眠らせた状態にしていました。
しかし、今日、次の記事を読んで少し考えたことがありました。
上記の記事では「解像度」の概念についての話をしています。キャッチーな主張としては
言語化の質を高めることは、語彙を増やすことではない。世界を見る解像度を上げることだ。
などがあります。一方、私はこの記事で二分探索を通して「解像度と無関係にメンタルモデル・解釈によって過程も理解度も応用力も変わる」ということを具体的に示しています。メンタルモデルの話に通じる話題としては、上記の記事においても「言葉を通して世界を認識している」という話題があるのですが、それにしても世界を見る解像度を上げるということだけが良い方法であるかというと、そうではないと思いました。それで、一部の言葉やアウトプットは同じでありながらも、その思考過程、および応用力的な部分で差が出るような事例について言及したくなり、不完全でもとりあえず世に出しておこう、と思ったのがこの記事を世に出した最終的な理由でした。
より具体的に言うと、私が二分探索で見出したことのうちの
- 人間は物事を考えるときに、初手から概念を完全に場合分けして考えられるとは限らない
- 初手で暗黙のうちに仮定されていたことに気づいて修正する必要がある
- 問題を解くにあたって各人が構成しているメンタルモデルは、アウトプットのレベルで同一であったとしても異なる場合がある
- メンタルモデルが異なる場合は、異なる説明が必要になる
ということについて、根本的に解像度で解決しない場合があります。つまり、自分の理解のどこが具体的に誤っているかを指摘できない状態においても 解像度を細かくするのではなくて別の見方をする・見えていない場所を見る方がはるかに早い 場合がある、というのが言いたいことです。それはある意味では 語彙を増やす ということでもあります。また、この別の見方が必要なとき、言語化は「どの考え方の部分から異なっているのか」ということを示すにあたって極めて有効な手段になります。つまり、考えの過程を言語化することによって、どこから考え方が異なっているのか、別の考え方ではどうなるのか、ということを具体的に指摘検討できるようになります。
ソフトウェアエンジニアリング協会の学習方法では、例えばコーディングについての問題を解くとき、その思考過程を詳しくPullRequestに記述することが推奨されています。それは、プロとしての行動や常識を身につけるにあたって、最終的には無意識的・身体的に処理できるようになるにしても、まずそれを他の人が指摘しやすくなるように意識的に明文化することがとても有効だからです。
究極的にはヴィトゲンシュタインのような意味で言語化には限界があります。しかしそのずっと手前で、メンタルモデルや身体感覚を得たい対象事物を言語化することには意義があり、またそれによって具体的な修正ができるようにもなります。プログラムを書くということも一種の言語化なので、たしかに言語化の一部を切り取れば差がないような場合もありますが、その過程を言語化すればその差は一目瞭然であり、修正を行う上で協力な武器となります。修正のためには思考過程をこの記事の例ぐらいの粒度では書いたほうがよい時もあるわけです。そういったことを言っておきたかったのでした。
謝辞
この記事は、ソフトウェアエンジニアリング協会のdiscordの会話から着想を得たものです。上で少し述べた通り、ソフトウェアエンジニアリング協会では、この記事で述べていることをもっと具体的に、自分の身体感覚として感じられるような学び方で学習できます。
具体的に実施されていることについては、コーディング練習会の内容や参加者の声についてのドキュメントに詳しく、直近の参加者の声としては ソフトウェアエンジニアリング協会のコーディング練習会で学んだこと などがあります。上記リンクの協会ページの問い合わせフォームから誰でも無料で参加できますので、興味をお持ちの方はぜひどうぞ。(私は運営会員として、余裕のあるときにほんの少し活動をしています。)
discordを通じた学習は随時行われているほか、12月には大阪で会が予定されています。
-
一般に、
aの要素数を とするとき、先頭から順番に調べるときに必要な調べる回数は平均でn 程度ですが、二分探索で必要な調べる回数はn/2 程度になります。 ↩︎\log(n) -
pythonで//2は2で割ったときの端数を除外した整数、つまり普通の割り算/の結果の小数点以下を切り捨てしたものになります。例えば7//2は3です。 ↩︎
-
トリックですが、この考え方によって実は厳密には配列を厳密に2分割しないよう"効率化"が為されています。同じコードが出力されるようになっているのもこのトリックによります。 ↩︎
-
ただ、その発言が出たのは数百行のSQLだったような気もして、素朴にそれを適用するのは難しかったかもしれない...ちなみに、どうしようもないときは、何度も実行して確実に動作している部分を広げていき、問題箇所を特定していくような二分探索的な探し方は有効ですが、普通に上から読み通す方が効率的なケースもよくあります。あくまでも方法論の一つとして。 ↩︎
-
ただ、これも結局は単純に場合分けすべきポイントとしての知識のような気もしています。何の材料もなしに、前提をやみくもに疑ったり細かくしたりするのは難しいと思います。実際にプログラムを動作させられる場面であれば、ランダムに作ったデータで検証しようとすると、xが0個や2個以上の場合に"機械的に"気付けるとは思うのですが。 ↩︎
Discussion