<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[The Reasoning Log]]></title><description><![CDATA[Exploring algorithms, mathematics, software engineering, and distributed systems through reasoning, experimentation, and first principles. Understanding why things work.]]></description><link>https://reasoninglog.hashnode.dev</link><generator>RSS for Node</generator><lastBuildDate>Sun, 25 Oct 2026 14:21:24 GMT</lastBuildDate><atom:link href="https://reasoninglog.hashnode.dev/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[Binary Search: From Finding a Number to Finding a Boundary]]></title><description><![CDATA[Deriving binary search through reasoning, mathematical proofs, loop invariants, and Boolean boundaries, from first principles.]]></description><link>https://reasoninglog.hashnode.dev/binary-search-from-finding-a-number-to-finding-a-boundary</link><guid isPermaLink="true">https://reasoninglog.hashnode.dev/binary-search-from-finding-a-number-to-finding-a-boundary</guid><category><![CDATA[binary search]]></category><category><![CDATA[algorithms]]></category><category><![CDATA[Binary Search Algorithm]]></category><category><![CDATA[data structures]]></category><category><![CDATA[Problem Solving]]></category><category><![CDATA[golang]]></category><dc:creator><![CDATA[0xmu0]]></dc:creator><pubDate>Sun, 18 Oct 2026 16:48:00 GMT</pubDate><content:encoded><![CDATA[<p><em>The Reasoning Log — a journey through assumptions, failed intuitions, proofs, and a more general way of searching</em></p>
<p>I do not particularly enjoy learning an algorithm by memorizing its final implementation. When I come across a mathematical formula or a programming technique, my instinct is to ask why it is valid in the first place. How would I arrive at the same idea if nobody told me its name?</p>
<p>That is how this exploration began. I did not want to be handed a binary-search problem, shown a loop, and asked to remember which pointer moves. I wanted to start with a situation, make decisions based on the information available, question those decisions, and see which algorithm emerged.</p>
<p>What started as finding one number in an ordered sequence eventually led to questions about worst-case reasoning, unknown distributions, integer overflow, duplicates, Boolean predicates, vacuous truth, loop invariants, and the limits of binary search itself. This is not a transcript of the conversation. It is my reasoning from that discussion, reconstructed as one continuous exploration—including places where I was uncertain or where an apparently obvious conclusion needed a proof.</p>
<h2>A hundred numbers, and very little information</h2>
<p>Imagine a piece of paper containing 100 numbers in strictly increasing order. I need to find the number 73. I am allowed to reveal the number at any position, but only one position at a time.</p>
<p>Where would I look first?</p>
<p>Knowing the numbers are strictly increasing is important, but I would not immediately assume that they form a familiar sequence. They might increase linearly, quadratically, exponentially, follow a polynomial of some unknown degree, or follow no simple formula at all. In particular, the <em>value</em> 73 gives me no reliable clue about its <em>position</em>. There is no reason it must sit near position 73, or anywhere else in particular.</p>
<p>My practical instinct is to inspect the middle, around the 50th position.</p>
<p>But the justification matters. I am not choosing the middle because the target is likely to be there. I am choosing it because I do not know how values are distributed, and the midpoint gives me a useful worst-case decision. If the revealed number is smaller than 73, ordering excludes the entire left portion; if it is larger, ordering excludes the entire right portion. Neither outcome leaves me with an overwhelmingly larger region to search.</p>
<p><strong>The midpoint is not a prediction of where the answer lies. It is a way to limit how many possibilities could remain after a single observation.</strong></p>
<p>That distinction turned out to matter throughout the discussion.</p>
<h2>One observation eliminates positions I have never inspected</h2>
<p>Suppose I reveal position 50 and find <strong>120</strong>.</p>
<p>I want 73. Since the sequence is strictly increasing, every number after position 50 is larger than 120. Position 50 itself is also not 73. I can therefore discard positions 50 through 100, leaving positions <strong>1 through 49</strong>.</p>
<p>There was a small but useful question here: should the 50th position remain in the range? No. We already inspected it, and we know it is not the target. The new interval is 1–49, not 1–50.</p>
<p>With 49 candidates left, I inspect position 25 and find <strong>40</strong>. Now I know the target, if it exists, lies somewhere in positions <strong>26 through 49</strong>.</p>
<p>At this point, could I make a smarter guess using the two observations?</p>
<pre><code class="language-text">position 25 → value 40
position 50 → value 120
target      → value 73
</code></pre>
<p>It is tempting to draw a straight line between the observed values and interpolate the position of 73. But where did the assumption of a straight line come from? The sequence could be linear, quadratic, exponential, a higher-degree polynomial, or irregular. Two observations are consistent with infinitely many possible ways of filling in the unobserved positions, subject to increasing order.</p>
<p>Even after inspecting three or four positions, I still would not know the generating rule. A few observed points could suggest a model, but they do not establish it. A different polynomial could fit those points; a monotonic sequence might not follow any useful polynomial at all. There is a difference between finding a model that <em>fits observations</em> and proving that the model <em>describes the unseen data</em>.</p>
<p>So my current best decision remains the same: take the middle of <strong>the remaining candidate positions</strong>, not the middle of the numerical values.</p>
<p>This is not a claim that interpolation can never be useful. If I have trustworthy information about the distribution, an estimate-based search may do better on some inputs. But with ordering as my only guarantee, splitting the possible positions in half gives a reliable bound on the worst case.</p>
<h2>What does “half” actually mean?</h2>
<p>I initially described the search space as shrinking by half, or perhaps half minus one. That phrasing seemed plausible until I examined even and odd ranges.</p>
<p>Start with positions 1 through 100. Using position 50 as the middle:</p>
<pre><code class="language-text">1 .............. 49 | 50 | 51 ................ 100
       49             1             50
</code></pre>
<p>If the target is smaller than the value at position 50, we keep 49 candidates. If it is larger, we keep 50. In the <strong>worst case</strong>, the remaining number is <strong>50</strong>, not 49.</p>
<p>For 25 candidates, however, the middle has 12 candidates on either side. So the number remaining is 12 in the worst case.</p>
<p>More generally, if an inclusive interval contains <code>N</code> candidate positions and I inspect one of its middle positions, the largest side remaining is:</p>
<p>[ \left\lfloor\frac{N}{2}\right\rfloor ]</p>
<p>The worst-case sizes from 100 candidates go like this:</p>
<pre><code class="language-text">100 → 50 → 25 → 12 → 6 → 3 → 1 → 0
</code></pre>
<p>Notice what happened: the reduction is not exactly <code>N/2</code> at every step, because the number of elements is an integer and the inspected element is excluded. But it is tightly bounded by halving.</p>
<p>Now imagine <strong>1,000,000</strong> candidate positions. How many inspections might be needed?</p>
<p>If after <code>k</code> halvings I have roughly <code>N / 2^k</code> candidates, the number of halvings needed to reach around one candidate satisfies:</p>
<p>[ \frac{N}{2^k}\approx 1 ]</p>
<p>Rearranging:</p>
<p>[ 2^k\approx N \qquad\Longrightarrow\qquad k\approx\log_2 N ]</p>
<p>That is where the logarithm comes from. It is not a complexity label attached to the algorithm by convention; it emerges from how quickly the logical candidate space shrinks. With one million positions, this search requires at most <strong>20</strong> midpoint inspections in the worst case.</p>
<p>For a nonempty interval of size <code>N</code>, an exact upper bound for this implementation's number of inspections is <code>floor(log₂ N) + 1</code>. The approximation helped discover the logarithm; the integer bounds make the statement precise.</p>
<h2>Before coding: where exactly is the middle?</h2>
<p>A new question emerged while trying ranges whose lower position was not 1. If my current candidates run from <code>L</code> to <code>H</code>, how do I calculate the midpoint without repeatedly thinking about whether the number of candidates is even or odd?</p>
<p>For an inclusive integer range, the left-middle position is:</p>
<p>[ M_{\text{left}}=\left\lfloor\frac{L+H}{2}\right\rfloor ]</p>
<p>The floor notation means to round down. For example:</p>
<pre><code class="language-text">L = 1,  H = 49   → midpoint 25
L = 1,  H = 50   → midpoint 25
L = 26, H = 49   → midpoint 37
L = 2,  H = 13   → midpoint 7
</code></pre>
<p>An even-sized interval has two middle positions; choosing the left one is a convention, not a logical necessity.</p>
<p>But there is an implementation issue: the expression <code>L + H</code> can overflow a fixed-width integer even if both indices are individually valid.</p>
<p>Consider an 8-bit signed integer, whose range is <code>-128</code> through <code>127</code>. Both 125 and 126 fit. Their sum, 251, does not. Computing <code>(L + H) / 2</code> through that intermediate sum may therefore be wrong, despite the mathematical midpoint being representable.</p>
<p>My first instinct was to use the <em>distance</em> between the boundaries instead of adding the boundaries. In fact, I was thinking of this expression:</p>
<p>[ M_{\text{right}}=H-\left\lfloor\frac{H-L}{2}\right\rfloor ]</p>
<p>It starts from the upper boundary and moves left by half the interval's distance. Another common expression starts from the lower boundary:</p>
<p>[ M_{\text{left}}=L+\left\lfloor\frac{H-L}{2}\right\rfloor ]</p>
<p>The conversation took a useful detour here because I was initially misunderstood: I had suggested <code>H - (H - L) / 2</code>, not simply <code>(H - L) / 2</code>. Those are very different expressions. The latter is only an offset; the former is an actual midpoint position.</p>
<p>Both complete formulas avoid the large <code>L + H</code> intermediate under the usual assumptions <code>0 ≤ L ≤ H</code>, with valid representable indices. But do they return the same middle? Rather than taking that on trust, I wanted to prove the difference.</p>
<p>Let:</p>
<p>[ d=H-L ]</p>
<p>If <code>d</code> is even, write <code>d = 2q</code>. Then:</p>
<p>[ M_{\text{left}}=L+q ]</p>
<p>[ M_{\text{right}}=(L+2q)-q=L+q ]</p>
<p>They coincide.</p>
<p>If <code>d</code> is odd, write <code>d = 2q + 1</code>. Then:</p>
<p>[ M_{\text{left}}=L+q ]</p>
<p>[ M_{\text{right}}=(L+2q+1)-q=L+q+1 ]</p>
<p>So the right-middle formula returns exactly one position after the left-middle formula when the number of candidates is even. When the number of candidates is odd, both choose the same position.</p>
<p>That gave me a more satisfying conclusion than simply memorizing one midpoint expression: <strong>either convention is valid, as long as the interval updates are consistent and always make progress.</strong></p>
<p>For an exact-value search on a sorted ascending sequence, if the midpoint value is smaller than the target, we set <code>L = M + 1</code>; if it is larger, <code>H = M - 1</code>. We exclude the midpoint because we have already ruled it out.</p>
<p>Here is the basic implementation, now derived rather than presented at the beginning:</p>
<pre><code class="language-go">func Find(a []int, target int) int {
    low, high := 0, len(a)-1

    for low &lt;= high {
        mid := low + (high-low)/2

        if a[mid] == target {
            return mid
        }
        if a[mid] &lt; target {
            low = mid + 1
        } else {
            high = mid - 1
        }
    }

    return -1
}
</code></pre>
<p>This finds <em>an</em> occurrence or returns <code>-1</code> if none exists. That qualification became important next.</p>
<h2>Duplicates: finding a value is not the same as finding where it begins</h2>
<p>Until this point I had assumed strictly increasing values. What if the numbers are merely non-decreasing, so duplicates are allowed?</p>
<pre><code class="language-text">index:  0  1  2  3  4  5  6  7  8
value:  2  4  4  4  4  4  7  9 11
</code></pre>
<p>Suppose the goal is not to find any <code>4</code> but to find its <strong>first occurrence</strong>.</p>
<p>If I inspect index 4 and see <code>4</code>, stopping immediately is no longer correct. It could be the first <code>4</code>, but there could also be more copies to its left.</p>
<p>My first approach was straightforward: save the index as a candidate, then continue searching the left portion. If I find another <code>4</code>, update the candidate and keep going left. If the inspected value is smaller, I search right, because the first <code>4</code> must be to the right of that smaller value.</p>
<p>For example:</p>
<pre><code class="language-text">index 4 → 4  : remember 4, search left
index 1 → 4  : remember 1, search left
index 0 → 2  : search right
stop         : first 4 is at index 1
</code></pre>
<p>That works. But a different question made the idea much cleaner.</p>
<p>What if, instead of looking for equality with 4, I ask at each position:</p>
<blockquote>
<p>Is this value greater than or equal to 4?</p>
</blockquote>
<p>The answers form a Boolean sequence:</p>
<pre><code class="language-text">index:       0  1  2  3  4  5  6  7  8
value:       2  4  4  4  4  4  7  9 11
value &gt;= 4:  F  T  T  T  T  T  T  T  T
</code></pre>
<p>Now I can forget about equality during the search itself. I only need to find the <strong>first position where the condition becomes True</strong>.</p>
<p>This was the conceptual shift. The search was no longer about encountering the number 4. It was about locating a transition boundary.</p>
<h2>What is monotonicity doing for us?</h2>
<p>A monotonic sequence moves in one direction without reversing. A non-decreasing numeric sequence can contain equal values, but its values never decrease.</p>
<p>A Boolean sequence can have a similar structure:</p>
<pre><code class="language-text">F F F F T T T T T
</code></pre>
<p>Here, once the condition becomes True, it stays True. There is at most one False-to-True transition. All-False and all-True sequences also satisfy this property, even though the transition lies outside the represented positions.</p>
<p>With an ascending sorted array, conditions such as <code>a[i] &gt;= x</code> or <code>a[i] &gt; x</code> produce exactly that structure. The crucial object is the <strong>predicate</strong>—the condition evaluated at a position—not the original numeric array by itself.</p>
<p>For the first True boundary, the reasoning is simple:</p>
<ul>
<li><p>If the midpoint is False, every position at or before it must also be False. The answer, if it exists, lies to the right.</p>
</li>
<li><p>If the midpoint is True, that position may be the first True, or the first True may lie to its left. The answer cannot be strictly to its right.</p>
</li>
</ul>
<p>Using inclusive search boundaries, we can move <code>L = M + 1</code> for False and <code>H = M - 1</code> for True. This second update initially seemed slightly suspicious: we have discarded a True midpoint that might itself be the answer. How can that still work?</p>
<p>That question led to the most interesting proof in the discussion.</p>
<h2>An unsettling result: why can I return L after the search becomes empty?</h2>
<p>Imagine this Boolean sequence:</p>
<pre><code class="language-text">index:  0  1  2  3  4  5  6  7
value:  F  F  F  T  T  T  T  T
</code></pre>
<p>Using inclusive bounds, a left midpoint, and the updates above:</p>
<table>
<thead>
<tr>
<th>Step</th>
<th>L</th>
<th>H</th>
<th>M</th>
<th>Result</th>
<th>Update</th>
</tr>
</thead>
<tbody><tr>
<td>1</td>
<td>0</td>
<td>7</td>
<td>3</td>
<td>True</td>
<td>H = 2</td>
</tr>
<tr>
<td>2</td>
<td>0</td>
<td>2</td>
<td>1</td>
<td>False</td>
<td>L = 2</td>
</tr>
<tr>
<td>3</td>
<td>2</td>
<td>2</td>
<td>2</td>
<td>False</td>
<td>L = 3</td>
</tr>
<tr>
<td>End</td>
<td>3</td>
<td>2</td>
<td>—</td>
<td>—</td>
<td>No candidates remain</td>
</tr>
</tbody></table>
<p>The search ends with <code>L = 3</code>, which is exactly the first True position.</p>
<p>At first, that looked like an observation, not an explanation. What if I did not inspect <code>L</code> directly? How can an empty interval tell me something so definite about a boundary? I wanted a justification that did not depend on a few convenient examples.</p>
<p>The explanation comes from two guarantees that hold <strong>before the search, after each update, and at the end</strong>:</p>
<ol>
<li><p>Every <strong>valid</strong> position strictly before <code>L</code> is False.</p>
</li>
<li><p>Every <strong>valid</strong> position strictly after <code>H</code> is True.</p>
</li>
</ol>
<p>Why do those guarantees hold?</p>
<p>Initially, <code>L = 0</code> and <code>H = n - 1</code>. There are no valid array positions before zero or after the last index, so neither statement can be violated. The guarantees are true before we inspect anything.</p>
<p>If the midpoint is False, monotonicity tells us all positions up to that midpoint are False. Moving <code>L</code> to <code>M + 1</code> extends the portion already known to be False.</p>
<p>If the midpoint is True, monotonicity tells us all positions from that midpoint onward are True. Moving <code>H</code> to <code>M - 1</code> extends the portion already known to be True.</p>
<p>This also answers the worry about throwing away a possible answer. <strong>We are not merely discarding positions; we are classifying them.</strong> A True midpoint excluded from the active interval remains in the region known to be True. The eventual boundary can sit exactly at the edge of those classified regions.</p>
<p>When the search finishes, there are no unresolved candidate positions, and because we always move across an inspected position, the boundaries meet in this relationship:</p>
<p>[ L=H+1 ]</p>
<p>The first guarantee says:</p>
<p>[ i&lt;L\implies P(i)=\text{False} ]</p>
<p>The second says:</p>
<p>[ i&gt;H\implies P(i)=\text{True} ]</p>
<p>But <code>H = L - 1</code>. Therefore:</p>
<p>[ i&gt; L-1\quad\Longleftrightarrow\quad i\ge L ]</p>
<p>So at termination:</p>
<p>[ \boxed{\ i&lt;L\Rightarrow P(i)=\text{False},\qquad i\ge L\Rightarrow P(i)=\text{True}\ } ]</p>
<p>These statements refer to valid positions within the array. They describe exactly what it means for <code>L</code> to be the first True position.</p>
<p>If <code>L = n</code>, there is no valid index at or after <code>L</code>: that means no True exists, and <code>n</code> acts as a sentinel immediately beyond the array.</p>
<p>I found one distinction especially clarifying: when <code>L &gt; H</code>, it does not mean we have <strong>inspected every element</strong>. It means we have exhausted the <strong>logical search space</strong>. Every possibility has either been inspected or eliminated by a proof based on monotonicity.</p>
<p>The two guarantees are examples of <strong>loop invariants</strong>—statements preserved throughout execution. Rather than treating the final value of <code>L</code> as a trick, the invariants make it a consequence.</p>
<h2>A detour into logic: vacuous truth</h2>
<p>There was a moment when the first invariant seemed intuitive but the second did not. If <code>H</code> is already at the final position, how can I claim that <em>everything strictly after H is True</em>? There is nothing after H!</p>
<p>That led to the idea of <strong>vacuous truth</strong>.</p>
<p>Take an empty classroom. Consider these statements:</p>
<blockquote>
<p>Every student in the classroom passed.</p>
</blockquote>
<blockquote>
<p>Every student in the classroom failed.</p>
</blockquote>
<p>Surprisingly, in classical logic, <strong>both</strong> are true when there are no students in the classroom.</p>
<p>That does not mean a student simultaneously passed and failed. Neither statement claims that a student exists. Each is a universal claim about <em>all members</em> of a set; when the set is empty, there is no member that violates either claim.</p>
<p>Formally, the statement</p>
<p>[ \forall x\in S,\ P(x) ]</p>
<p>is true for an empty set <code>S</code>. But the claim that <strong>there exists</strong> an element satisfying <code>P</code> is false for an empty set.</p>
<p>This is easy to misinterpret as “if I have not found a counterexample, a statement is true.” That is <strong>not</strong> generally valid. For a nonempty, unexamined set, we simply do not know. The special point here is that the set is <em>known to be empty</em>, so a counterexample is impossible.</p>
<p>Returning to the algorithm: when <code>H = n - 1</code>, the set of valid positions strictly after H is empty. Therefore, “every valid position after H is True” holds at initialization without requiring any True elements to exist.</p>
<p>And if the entire Boolean sequence is False, the search ends with <code>L = n</code> and <code>H = n - 1</code>. Everything before L is False. Everything after H is True only vacuously, because there is no such valid position. The same invariant handles the missing-boundary case cleanly.</p>
<p>This detour did not feel unrelated to algorithms. It explained precisely why an invariant can be true at the beginning of a loop even when it appears to assert knowledge about positions we have never examined.</p>
<h2>The reusable operation we discovered</h2>
<p>Once the invariant is clear, the implementation follows naturally:</p>
<pre><code class="language-go">// FirstTrue returns the first index where pred(i) is true.
// It returns n if all positions are false.
// pred must have the shape: F ... F T ... T.
func FirstTrue(n int, pred func(int) bool) int {
    low, high := 0, n-1

    for low &lt;= high {
        mid := low + (high-low)/2
        if pred(mid) {
            high = mid - 1
        } else {
            low = mid + 1
        }
    }

    return low
}
</code></pre>
<p>There is something appealing about how little this function knows. It does not know whether we are searching for a number, an insertion position, or something else. It only asks a Boolean question about a position and relies on the promised False-to-True order.</p>
<p>The predicate constructs the Boolean search space. The algorithm finds its boundary.</p>
<p>Go already provides a closely related operation, <code>sort.Search</code>, which returns the first index satisfying a monotonic predicate. Using the standard library does not eliminate the need to justify monotonicity; it merely packages the search loop.</p>
<h2>Finding the last occurrence required another change of perspective</h2>
<p>After understanding the first occurrence, I did not want to move immediately to unrelated applications. What about the <strong>last</strong> occurrence of a value?</p>
<p>Consider:</p>
<pre><code class="language-text">index:  0  1  2  3  4  5  6  7  8
value:  2  3  5  5  5  5  5  8 10
</code></pre>
<p>The first <code>5</code> is at index 2, and the last is at index 6.</p>
<p>For the first occurrence we used <code>a[i] &gt;= 5</code>, which has the shape <code>F ... F T ... T</code>.</p>
<p>For the last occurrence, I considered the condition <code>a[i] &lt;= 5</code>, which has the reverse shape:</p>
<pre><code class="language-text">value &lt;= 5: T  T  T  T  T  T  T  F  F
</code></pre>
<p>Now a True midpoint tells me that the last True must be at that midpoint or somewhere to its right, so I move <code>L = M + 1</code>. A False midpoint moves <code>H = M - 1</code>.</p>
<p>The invariants reverse:</p>
<p>[ i&lt;L\implies P(i)=\text{True} ]</p>
<p>[ i&gt;H\implies P(i)=\text{False} ]</p>
<p>At termination, <code>L = H + 1</code>. Therefore, <code>L</code> is the first False position, and <code>H = L - 1</code> is the last True position.</p>
<p>So I can find the last index satisfying <code>a[i] &lt;= 5</code> by returning <code>L - 1</code>.</p>
<p>But that is <strong>not automatically the last occurrence of 5</strong>. Suppose the array is <code>[1, 2, 3, 4, 7, 9]</code> and the target is 5. The last element <code>&lt;= 5</code> is 4, not 5. The boundary exists even when the target does not.</p>
<p>The missing piece is just a final check: is the candidate index valid, and does the value at that index actually equal the target? If not, the target is absent.</p>
<p>The same caution applies when searching for a first occurrence: the first value <code>&gt;= target</code> might be larger than the target.</p>
<h2>Two boundary searches, one reusable algorithm</h2>
<p>I wondered whether finding the first and last occurrences separately was unnecessary work. Couldn't one search find both?</p>
<p>Suppose we inspect a middle position containing the target. We learn something, but not enough to discard both sides: the first occurrence might still be to the left, and the last occurrence might still be to the right.</p>
<p>We can organize a single procedure that explores both sides after finding a match, but that is different from finding both boundaries by following only one chain of midpoint decisions. Two dedicated searches still take <code>O(log n) + O(log n) = O(log n)</code> time overall.</p>
<p>A useful observation is that neither search needs to find the target as its first midpoint value. We can search for two neighboring transitions:</p>
<ul>
<li><p><code>lowerBound(x)</code>: the first index with <code>a[i] &gt;= x</code>.</p>
</li>
<li><p><code>upperBound(x)</code>: the first index with <code>a[i] &gt; x</code>.</p>
</li>
</ul>
<p>On the array:</p>
<pre><code class="language-text">index:     0  1  2  3  4  5  6  7  8
value:     1  2  3  3  3  3  3  5  7
&gt;= 3:      F  F  T  T  T  T  T  T  T
&gt;  3:      F  F  F  F  F  F  F  T  T
</code></pre>
<p>The lower boundary is 2. The upper boundary is 7. Everything from 2 up to—but not including—7 equals 3.</p>
<p>That immediately gives the number of occurrences:</p>
<p>[ \boxed{\text{count}(x)=\operatorname{upperBound}(x)-\operatorname{lowerBound}(x)} ]</p>
<p>What if the value is absent? Take <code>[1, 2, 4, 4, 4, 7, 9]</code> and target 5. Both boundaries are index 5, the position of 7. Their difference is zero, which correctly says there are no 5s.</p>
<p>And what if every value equals the target? In <code>[2, 2, 2, 2, 2]</code>, the lower boundary is 0 and the upper boundary is 5. Index 5 is not a valid element, but it is a perfectly meaningful boundary after the array. The count is <code>5 - 0 = 5</code>.</p>
<p>The two boundaries also give insertion positions: lower bound inserts before existing equal elements; upper bound inserts after them.</p>
<p>At first these looked like separate tasks—first occurrence, last occurrence, count, insertion point. But they are consequences of identifying where a monotonic condition changes.</p>
<p>Here is the implementation using one generalized search function:</p>
<pre><code class="language-go">func LowerBound(a []int, target int) int {
    return FirstTrue(len(a), func(i int) bool {
        return a[i] &gt;= target
    })
}

func UpperBound(a []int, target int) int {
    return FirstTrue(len(a), func(i int) bool {
        return a[i] &gt; target
    })
}

func FirstOccurrence(a []int, target int) int {
    i := LowerBound(a, target)
    if i == len(a) || a[i] != target {
        return -1
    }
    return i
}

func LastOccurrence(a []int, target int) int {
    i := UpperBound(a, target) - 1
    if i &lt; 0 || a[i] != target {
        return -1
    }
    return i
}

func CountOccurrences(a []int, target int) int {
    return UpperBound(a, target) - LowerBound(a, target)
}
</code></pre>
<p>One small disagreement in the conversation was actually clarifying. I initially said the two tasks could share a search function. My concern was that they generated different Boolean spaces. Both statements are compatible: <strong>the predicate changes; the algorithm does not.</strong> We can pass either predicate to the same first-True search.</p>
<h2>Do the original numbers even need to be sorted?</h2>
<p>This was the next natural question. At the beginning I used sorting to justify discarding half of an array. But by this stage, the correctness proof no longer referred to numerical sorting directly. It referred to a Boolean predicate with a False-to-True pattern.</p>
<p>So the input values themselves do not necessarily need to be sorted. What matters is whether the <em>condition we are searching over</em> is monotonic.</p>
<p>A Boolean predicate might be derived from a numerical sequence, from some calculation, or even from a yes/no feasibility test. As long as its answers across an ordered candidate space have the shape</p>
<pre><code class="language-text">F F F F F T T T T
</code></pre>
<p>we can search for the transition.</p>
<p>But this is a requirement to prove, not something that follows merely because a function returns Boolean values. A predicate can just as easily produce <code>F T F T</code>, in which case a midpoint answer cannot safely eliminate an entire side.</p>
<p>We explored that distinction with prefix sums.</p>
<h2>An unsorted array whose predicate is still monotonic</h2>
<p>Consider this array:</p>
<pre><code class="language-text">[4, 2, 7, 1, 8, 3, 6]
</code></pre>
<p>It is not sorted. Define a question for each prefix length <code>k</code>:</p>
<blockquote>
<p>Is the sum of the first <code>k</code> elements at least 12?</p>
</blockquote>
<p>For the nonempty prefixes:</p>
<pre><code class="language-text">k:          1   2   3   4   5   6   7
prefix sum: 4   6  13  14  22  25  31
&gt;= 12:     F   F   T   T   T   T   T
</code></pre>
<p>The array is unsorted, yet the predicate has a clean False-to-True transition. Why?</p>
<p>Every element in this particular array is positive. Extending a prefix can only increase its sum. Once a prefix reaches 12, every longer prefix also reaches 12.</p>
<p>The mathematical guarantee is:</p>
<p>[ S(k+1)=S(k)+a[k] ]</p>
<p>With positive elements, <code>S(k+1) &gt; S(k)</code>. Even nonnegative elements would be sufficient for a non-decreasing prefix sum. The values' original ordering is irrelevant.</p>
<p>This example made the generalization concrete: <strong>binary search relies on the ordering of the answers to a question, not necessarily on the ordering of the underlying data.</strong></p>
<h2>Negative numbers: a broken guarantee, but not always a broken predicate</h2>
<p>Now allow negative numbers:</p>
<pre><code class="language-text">[4, 9, -8, 2, 10, -5, 3]
</code></pre>
<p>The prefix sums are:</p>
<pre><code class="language-text">k:          1   2   3   4   5   6   7
prefix sum: 4  13   5   7  17  12  15
&gt;= 12:     F   T   F   F   T   T   T
</code></pre>
<p>This is not monotonic. A prefix can reach the target and then fall below it after adding a negative number. Consequently, an observation at one position no longer proves that all later positions behave the same way. Our boundary-search proof is invalid here.</p>
<p>But the presence of a negative number does not, by itself, prove that <strong>every</strong> threshold predicate is nonmonotonic.</p>
<p>Consider:</p>
<pre><code class="language-text">array:      [10, -2, 5, -1, 4]
prefix sum:  10   8  13  12  16
</code></pre>
<p>For target 8, the predicate is:</p>
<pre><code class="language-text">T T T T T
</code></pre>
<p>It is monotonic even though the sums dip. For target <strong>9</strong>, however, it becomes:</p>
<pre><code class="language-text">T F T T T
</code></pre>
<p>and monotonicity fails.</p>
<p>That distinction matters. A property of the underlying sequence may be sufficient to guarantee monotonicity for many predicates, but it is not always necessary for a particular predicate. The same underlying data can generate a monotonic Boolean space for one threshold and a nonmonotonic one for another.</p>
<p>It would be incorrect to argue “there is a negative number, therefore binary search can never work.” The precise question is whether the predicate for the current problem has the required transition structure. It may be monotonic accidentally for one input, but if the algorithm is meant to work for arbitrary inputs, we need a guarantee from the problem's assumptions, not a lucky example.</p>
<h2>When binary search is no longer justified, what should I do?</h2>
<p>We then changed the task. Given arbitrary positive and negative integers, find the <strong>smallest prefix length</strong> whose sum reaches a target.</p>
<p>I first clarified what “prefix” means. A prefix begins at the start of the array (index 0 in a zero-indexed array). A contiguous sequence beginning later is a subarray, not necessarily a prefix. The empty prefix is also permitted by some definitions, but for this particular exercise we considered nonempty prefixes.</p>
<p>With no monotonicity guarantee, binary search is not justified. But the task has an uncomplicated property of its own: if I examine prefix lengths in increasing order, <strong>the first qualifying prefix must be the shortest qualifying prefix</strong>.</p>
<p>So I can scan once, maintain a running sum, and stop as soon as it reaches the threshold:</p>
<pre><code class="language-go">// SmallestPrefixAtLeast returns the length of the shortest
// nonempty prefix whose sum is at least target, or -1.
func SmallestPrefixAtLeast(a []int, target int) int {
    sum := 0
    for i, x := range a {
        sum += x
        if sum &gt;= target {
            return i + 1
        }
    }
    return -1
}
</code></pre>
<p>For <code>[4, 9, -8, 2, 10, -5, 3]</code> and target 12, the prefix sum at length 1 is 4; at length 2 it is 13. We can stop at length 2. Even if the sum drops below 12 later, no later prefix can have a <em>smaller length</em> than 2.</p>
<p>This takes <code>O(n)</code> time in the worst case and <code>O(1)</code> extra space. It does not rely on a monotonic threshold predicate; it relies on examining candidate prefix lengths in increasing order.</p>
<p>The lesson was not that we had failed to use binary search. It was that <strong>a correct simple solution is better than forcing a favored technique onto a problem that does not satisfy its assumptions</strong>.</p>
<h2>A final variation, and where we paused</h2>
<p>Before stopping, we considered a closely related problem: find the <strong>largest prefix length</strong> whose sum is at least the target.</p>
<p>My first answer was to keep scanning the whole array, update the best prefix length whenever the running sum reaches the target, and return the largest qualifying length. That works in <code>O(n)</code> time and <code>O(1)</code> extra space. Unlike the smallest-prefix scan, it cannot stop at the first match, because a longer qualifying prefix may appear later.</p>
<p>Then another possibility came up: what if I begin with the <strong>entire</strong> array and work backward? The full-array sum is the longest prefix sum. If it qualifies, I am done immediately. If it does not, I can remove the final element from the running sum to obtain the next-shorter prefix sum, without recomputing each prefix from scratch.</p>
<p>That was the next direction to explore, but we stopped there. I want to leave it as the point where the discussion actually paused, rather than manufacture a more elaborate discovery we had not yet worked through together.</p>
<h2>What I am taking away</h2>
<p>The most useful part of this exploration was not the Go code. It was watching the meaning of the problem change as I questioned what the search actually needed.</p>
<p>At first, I had an ordered list and wanted to find a number. I chose the middle because I had no justified model for the numerical distribution—even after observing several values. Then I quantified the worst-case reduction, derived the logarithm, and examined how midpoint arithmetic behaves on fixed-width integers.</p>
<p>Duplicates forced me to distinguish “I found a matching value” from “I found where matching values begin.” That led to monotonic Boolean conditions and transition boundaries. Returning <code>L</code> after the search interval became empty felt strange until loop invariants made the result inevitable. Even the detour into vacuous truth mattered, because it explained why those invariants held before the very first inspection.</p>
<p>Finding the last occurrence revealed the reverse boundary. First and last occurrences, insertion positions, and counts turned out to be different questions answered by the same boundary-search mechanism with different predicates.</p>
<p>Finally, unsorted arrays and negative prefix sums revealed both the power and the limit of the abstraction. Sorting is one way to obtain the necessary structure, but not the essence of the method. The essence is <strong>a predicate whose behavior lets an observation rule out an entire region with certainty</strong>. When that proof is missing, we should not use the search simply because its code is familiar.</p>
<p>I started with a number and a midpoint. I ended up thinking in terms of information, elimination, invariants, and boundaries.</p>
<p>That feels like a more durable understanding of binary search than memorizing any one implementation.</p>
]]></content:encoded></item></channel></rss>