For each candidate first index in a sorted array, use lower-bound search for only in the suffix starting at . A matching result is automatically at a different position. The outer loop invariant excludes all solutions whose smaller index precedes ; an unsuccessful suffix search extends this exclusion by one. Exhaustion proves absence of any pair. Searching all suffixes gives running time and constant auxiliary space. Compute the complement in a sufficiently wide type to avoid integer overflow before comparison.
Use a lower-bound search separately in each nondecreasing row:
def searchRows(f: Array[Array[Int]], x: Int): Option[(Int, Int)] = {
  val n = f.length
  val m = if (n == 0) 0 else f(0).length
  if (m == 0) return None
  var i = 0
  while (i < n) {
    var lo = 0
    var hi = m
    while (lo < hi) {
      val mid = lo + (hi - lo) / 2
      if (f(i)(mid) < x) lo = mid + 1
      else hi = mid
    }
    if (lo < m && f(i)(lo) == x) return Some((i, lo))
    i += 1
  }
  None
}
The outer loop invariant says there is no match in earlier rows. The inner invariant is , with every value before lo smaller than and every value from hi onward at least . Sortedness justifies each exclusion, and hi - lo strictly decreases. At exit, a match in this row exists exactly when the lower-bound index is inside the row and holds . Failure extends the outer exclusion.
The cost is , conventionally written for ; the extra one matters when a row has a single entry. Storage is constant. The source's column ranges must use indices strictly below , since f(i)(m) is outside the array.
This is lower-bound search: return the first position whose value is at least the target, allowing the result to equal the array length. The notation for an empty suffix is vacuously true, so this also covers a target larger than every entry and the empty array. Assume a stable sorted array, ordered nondecreasingly.
def ternarySearch(a: Array[Int], x: Int): Int = {
  var lo = 0
  var hi = a.length
  while (lo < hi) {
    val third = (hi - lo) / 3
    val m1 = lo + third
    val m2 = hi - 1 - third
    if (a(m1) >= x) hi = m1
    else if (a(m2) < x) lo = m2 + 1
    else {
      lo = m1 + 1
      hi = m2
    }
  }
  lo
}
The loop invariant is
Initially the excluded prefix and suffix are empty, so both quantified assertions hold. During an iteration, . If , sortedness makes every later value at least , so replacing hi by m1 preserves the right assertion. If the first test failed and , every earlier value is below , so replacing lo by m2 + 1 preserves the left assertion. Otherwise ; both new exclusions are valid, and lo = m1 + 1; hi = m2 retains exactly the unresolved middle. In that branch , since a single value cannot satisfy both strict and nonstrict inequalities in opposite directions.
The nonnegative loop variant hi - lo strictly decreases. In fact, with old length and , the three possible new lengths are , with the middle branch feasible only when nonnegative; each is at most . At exit lo == hi, and the loop invariant is exactly the desired partition. The result is the unique lower-bound index, including the first equal entry when duplicates occur. This ternary lower-bound search uses comparisons and auxiliary space, in the Big O notation word-operation model. The index formulas also avoid computing an overflowing 2 * length.
For the given sorted array, the complete lower-bound search behavior is for , for , for , and for . In particular, index is never the correct answer because the two equal entries share the same first insertion position.
These unit tests cover every value boundary, equality case and missing-value interval:
  • Below all entries, including the smallest machine integer: ternarySearch(a, Int.MinValue) == 0 and ternarySearch(a, 0) == 0.
  • Equality with the first entry: ternarySearch(a, 1) == 0.
  • Values in the first gap: ternarySearch(a, 2) == 1 and ternarySearch(a, 3) == 1.
  • Repeated equality must return the first duplicate: ternarySearch(a, 4) == 1.
  • Values in the second gap: ternarySearch(a, 5) == 3 and ternarySearch(a, 9) == 3.
  • Equality with the final entry: ternarySearch(a, 10) == 3.
  • Beyond the final entry: ternarySearch(a, 11) == 4 and ternarySearch(a, Int.MaxValue) == 4.
For every tested target, also assert that the array is unchanged, the result lies in , every entry before it is strictly smaller, and every entry from it onward is at least the target. These postconditions check the intended partition directly rather than merely accepting any equal occurrence.
A lower-bound search that probes two positions to retain one of three subintervals. For unresolved of positive length , put , and . If , retain ; if , retain ; otherwise retain . Sortedness preserves the lower-bound partition. The new interval lengths are , and the feasible middle case has nonnegative length at most . This proves strict progress and comparison cost. Equality must retain the first possible boundary rather than return an arbitrary matching occurrence.