Part 03 – Sorting and Searching | Difficulty: ⭐⭐⭐☆☆
Introduction: Applying Binary Search to the 'Answer'
Parametric Search is a technique that applies binary search not to values but to the space of possible answers.
Core Pattern
Determine with an O(n) decision function whether "a certain value X is possible," and
use binary search over the range [lo, hi] of X to find the optimal X in O(n log(range)).
Common problem types:
Pattern
Example
Maximizing the Minimum
Maximizing the minimum distance when installing K routers
Minimizing the Maximum
Minimizing the maximum burden of one person when distributing among K people
Possible Maximum
The maximum H that yields at least M meters when cutting trees
Learning Goals
Define a decision function (feasibility function) and check its monotonicity.
Memorize the pattern templates for "Maximizing the Minimum" / "Minimizing the Maximum."
Set the search direction (updating left vs right) correctly according to the problem type.
Key Concepts
Parametric Search Template
def feasible(mid: int) -> bool:
#Does the mid value satisfy the condition?
...
left, right = minimum value, maximum value
answer = left #Or right (initial value determined depending on the problem)
while left <= right:
mid = (left + right) // 2
if feasible(mid):
answer = mid #Update the possible value
left = mid + 1 #Try a larger value (maximize)
#Or right = mid - 1 (minimize)
else:
right = mid - 1 #Need to decrease
#Or left = mid + 1 (need to increase)
Monotonicity of the Decision Function
Parametric search is possible only if the decision function is monotonic:
Maximization problem (possible maximum value):
X=1: ✅ X=2: ✅ X=3: ✅ X=4: ❌ X=5: ❌
→ After a certain point, all ❌ → Binary search possible
Minimization problem (possible minimum value):
X=1: ❌ X=2: ❌ X=3: ✅ X=4: ✅ X=5: ✅
→ After a certain point, all ✅ → Binary search possible
Pattern for Minimizing the Maximum
"When dividing into K groups, minimize the size of the largest group"
def feasible(max_size):
#Is it possible to divide into K groups or fewer due to max_size limit?
groups = 1
current = 0
for item in items:
if current + item > max_size:
groups += 1
current = item
else:
current += item
return groups <= K
left, right = max(items), sum(items)
answer = right
while left <= right:
mid = (left + right) // 2
if feasible(mid):
answer = mid #Update minimum value
right = mid - 1 #Try a smaller value
else:
left = mid + 1
Maximum of the minimum pattern
"When placing K items, maximize the minimum distance between adjacent items"
def feasible(min_dist):
#Is it possible to place K items with at least min_dist distance?
...
left, right = 1, max_position
answer = 0
while left <= right:
mid = (left + right) // 2
if feasible(mid):
answer = mid #Update the maximum value
left = mid + 1 #Try a larger value
else:
right = mid - 1
Example
File
Contents
src/01_parametric_template.py
General parameter search template
src/02_cable_cutting.py
Cable/Wood cutting variant
src/03_book_pages.py
Distributing book pages to K people (minimize the maximum)
src/04_kth_smallest.py
K-th smallest number in multiplication table
5 Common Mistakes
Setting the decision function in the wrong direction
Maximization: if condition is satisfied left = mid + 1 (search for larger values)
Minimization: if condition is satisfied right = mid - 1 (search for smaller values)
Doing it the opposite way can cause infinite loops or wrong answers
Errors in setting search range (lo, hi)
If lo is too high or hi is too low, you can miss the answer
Safely: lo = possible minimum, hi = possible maximum
Using a non-monotonic decision function
✅ ❌ ✅ pattern makes binary search impossible
Need to reanalyze the problem
Integer/Real-Number boundary handling
Integer search: while left <= right
Floating-point search: while right - left > eps (e.g., 1e-9)
**Not initializing answer value
Handling cases where feasible is always False was omitted
Always explicitly set the initial value of answer
Summary
Problem Type
Decision Function Direction
answer Update
Maximization (possible maximum)
Satisfies → left++
answer = mid when feasible
Minimization (possible minimum)
Satisfies → right--
answer = mid when feasible
Try It Yourself
Solve the tree cutting problem (Baekjoon 2805) using a decision function.
Explain the monotonicity of the decision function in the router installation problem (Baekjoon 2110).
Calculate the search range for K=1 and K=n in the book distribution problem.
Example files
The runnable files used in this lesson. Copy them into your own project and experiment.
src/01_parametric_template.pyPython
"""Topic: General Template for Parameter Search
Time: O(n log(range))
Space: O(1) (excluding the decision function)"""
from collections.abc import Callable
#── General Template: Maximization ───────────────────────────────────────────────
def parametric_max(
lo: int,
hi: int,
feasible: Callable[[int], bool],
) -> int:
"""Search for the maximum possible value.
If feasible(mid) = True, assume all values less than or equal to mid are also possible.
(Monotonically decreasing: ✅✅✅❌❌ pattern)
Returns:
The maximum value that satisfies the condition"""
answer = lo - 1 #Default when nothing is possible
while lo <= hi:
mid = (lo + hi) // 2
if feasible(mid):
answer = mid
lo = mid + 1 #Try a larger value
else:
hi = mid - 1 #Need to decrease
return answer
def parametric_min(
lo: int,
hi: int,
feasible: Callable[[int], bool],
) -> int:
"""Search for the minimum possible value.
If feasible(mid) = True, assume all values greater than or equal to mid are also possible.
(Monotonically increasing: ❌❌✅✅✅ pattern)
Returns:
The minimum value that satisfies the condition"""
answer = hi + 1 #Default when nothing is possible
while lo <= hi:
mid = (lo + hi) // 2
if feasible(mid):
answer = mid
hi = mid - 1 #Try a smaller value
else:
lo = mid + 1 #Must press
return answer
#── Example 1: Maximizing the number of elements in the array with sum at least the target ─────────────
def demo_array_sum() -> None:
"""Find the minimum k such that the sum of the first k elements is at least S."""
arr = [1, 3, 2, 4, 2, 5, 1, 3]
S = 10
prefix = [0]
for x in arr:
prefix.append(prefix[-1] + x)
def feasible_min_k(k: int) -> bool:
#Is the sum of the first k elements at least S?
return prefix[k] >= S
min_k = parametric_min(1, len(arr), feasible_min_k)
print(f"Array: {arr}, Target sum: {S}")
print(f"Minimum k with sum at least {S}: {min_k} (Sum={prefix[min_k]})")
#── Example 2: Monotonicity Visualization ─────────────────────────────────────────────
def visualize_monotone() -> None:
"""Visualize the monotonicity of the decision function."""
trees = [20, 15, 10, 17]
need = 7
def feasible_cut(h: int) -> bool:
return sum(max(0, t - h) for t in trees) >= need
print(f"\n=== Monotonicity Visualization ===")
print(f"Tree heights: {trees}, Needed amount: {need}")
print(f"{'H':>4} {'Collected':>6} {'Possible?':>6}")
for h in range(0, max(trees) + 1):
collected = sum(max(0, t - h) for t in trees)
ok = feasible_cut(h)
print(f"{h:>4} {collected:>6} {'✅' if ok else '❌'}")
answer = parametric_max(0, max(trees), feasible_cut)
print(f"\n→ Maximum possible H = {answer}")
#── Example 3: Real Parameter Search ───────────────────────────────────────────────
def parametric_float(
lo: float,
hi: float,
feasible: Callable[[float], bool],
eps: float = 1e-9,
max_iter: int = 100,
) -> float:
"""Search over real-number parameters.
Terminate when the number of iterations reaches max_iter or convergence is within eps."""
for _ in range(max_iter):
if hi - lo < eps:
break
mid = (lo + hi) / 2
if feasible(mid):
lo = mid
else:
hi = mid
return (lo + hi) / 2
def demo_float_search() -> None:
"""Search over real-number parameters: finding sqrt(2)."""
target = 2.0
def feasible_sqrt(x: float) -> bool:
return x * x <= target
result = parametric_float(0.0, 2.0, feasible_sqrt)
import math
print(f"\n=== Real Parameter Search ===")
print(f"sqrt(2) search result: {result:.10f}")
print(f"math.sqrt(2) : {math.sqrt(2):.10f}")
print(f"Error : {abs(result - math.sqrt(2)):.2e}")
#── Main ─────────────────────────────────────────────────────────────────────────
if __name__ == "__main__":
demo_array_sum()
visualize_monotone()
demo_float_search()
#Simple verification: minimum x in range 0~100 with x*x >= 64
result = parametric_min(0, 100, lambda x: x * x >= 64)
assert result == 8, f"Expected 8, got {result}"
print(f"Verification: Minimum value of x² ≥ 64 = {result} PASS")
src/02_cable_cutting.pyPython
"""Topic : Cable/Tree Cutting — Parameter Search
Time : O(N log(max_length))
Space : O(N)
Problem: From N cables, how many pieces of length L can be made at most?
(Or) What is the maximum length L to make at least K pieces?"""
#── Decision Function ─────────────────────────────────────────────────────────
def count_pieces(cables: list[int], length: int) -> int:
"""Number of pieces obtained when cut to length `length`."""
return sum(c // length for c in cables)
def can_make_k(cables: list[int], length: int, k: int) -> bool:
"""Can we make at least K pieces with length `length`?"""
return count_pieces(cables, length) >= k
#── Parameter Search ────────────────────────────────────────────────────────────
def max_length_for_k(cables: list[int], k: int) -> int:
"""Maximum length to make at least K pieces.
Returns:
Maximum length (0 if impossible)"""
if not cables:
return 0
left, right = 1, max(cables)
answer = 0
while left <= right:
mid = (left + right) // 2
if can_make_k(cables, mid, k):
answer = mid
left = mid + 1 #Try longer length
else:
right = mid - 1 #Need to decrease
return answer
def min_cables_for_k(cables: list[int], k: int) -> int:
"""Maximum of the minimum number of pieces for each piece when making exactly k pieces.
(= Same problem as `max_length_for_k`)"""
return max_length_for_k(cables, k)
#── Step Visualization ─────────────────────────────────────────────────────────
def visualize_cutting(cables: list[int], k: int) -> None:
"""Visualization of the search process."""
print(f"Cable lengths: {cables}")
print(f"Target number K: {k}")
print(f"{'L':>5} {'Number of pieces obtained':>12} {'Possible?':>6}")
max_len = max(cables)
for l in range(1, min(max_len + 1, 30)): #Output only if 30 or less
pieces = count_pieces(cables, l)
ok = pieces >= k
bar = "█" * min(pieces, 30)
print(f"{l:>5} {pieces:>12} {'✅' if ok else '❌'} {bar}")
if max_len >= 30:
print(f"... (up to {max_len})")
#── Variation: Minimum Number of Cables (Maximum length when divided into K pieces) ────────────────────────────
def demo_lanline() -> None:
"""Baekjoon 1654 (Cutting LAN Cables) style."""
cables = [802, 743, 457, 539]
k = 11
print("=== Cable Cutting (Baekjoon 1654 style) ===")
answer = max_length_for_k(cables, k)
pieces = count_pieces(cables, answer)
print(f"Cables: {cables}")
print(f"Maximum length to create at least {k} pieces: {answer}cm")
print(f"Actual number of pieces obtained: {pieces}")
#── Variation: Cutting Trees ────────────────────────────────────────────────────
def demo_tree_cutting() -> None:
"""Baekjoon 2805 (Cutting Trees) style."""
trees = [20, 15, 10, 17]
need = 7
def tree_feasible(h: int) -> bool:
return sum(max(0, t - h) for t in trees) >= need
left, right = 0, max(trees)
answer = 0
steps = []
while left <= right:
mid = (left + right) // 2
collected = sum(max(0, t - mid) for t in trees)
ok = tree_feasible(mid)
steps.append((left, mid, right, collected, ok))
if ok:
answer = mid
left = mid + 1
else:
right = mid - 1
print("\n=== Cutting Trees (Baekjoon 2805 style) ===")
print(f"Trees: {trees}, Needed amount: {need}")
print(f"{'left':>5} {'mid':>5} {'right':>6} {'Collected':>6} {'Possible?':>6}")
for l, m, r, c, ok in steps:
print(f"{l:>5} {m:>5} {r:>6} {c:>6} {'✅' if ok else '❌'}")
print(f"→ Cutting height H = {answer}")
#── Main ─────────────────────────────────────────────────────────────────────────
if __name__ == "__main__":
cables = [10, 15, 20, 25]
k = 8
print("=" * 50)
print("Cable cutting step visualization")
print("=" * 50)
visualize_cutting(cables, k)
answer = max_length_for_k(cables, k)
print(f"→ Maximum length = {answer}, Number of pieces = {count_pieces(cables, answer)}")
demo_lanline()
demo_tree_cutting()
#Validation
assert max_length_for_k([802, 743, 457, 539], 11) == 200
assert max_length_for_k([20, 15, 10, 17], 7) == 15 #Cutting trees H=15 → 7
print("Accuracy check: PASS")
src/03_book_pages.pyPython
"""Topic: Book Page Allocation — Minimize Maximum
Time: O(N log(sum))
Space: O(N)
Problem: When dividing N chapters among K readers,
minimize the maximum number of pages any one person has to read.
(Maintain chapter order, only consecutive sections allowed)"""
#── Decision Function ─────────────────────────────────────────────────────────
def can_distribute(chapters: list[int], k: int, max_pages: int) -> bool:
"""Can the chapters be divided among K or fewer people under a max_pages limit?
Each reader must read consecutive chapters."""
readers = 1
current = 0
for pages in chapters:
if pages > max_pages:
return False #Single chapter exceeds limit
if current + pages > max_pages:
readers += 1
current = pages
if readers > k:
return False
else:
current += pages
return True
#── Parameter Search ────────────────────────────────────────────────────────────
def min_max_pages(chapters: list[int], k: int) -> int:
"""Minimum maximum number of pages when dividing among K people.
Search range:
lo = max(chapters) : each person gets one chapter when K=N
hi = sum(chapters) : one person reads all when K=1"""
lo = max(chapters)
hi = sum(chapters)
answer = hi
while lo <= hi:
mid = (lo + hi) // 2
if can_distribute(chapters, k, mid):
answer = mid
hi = mid - 1 #Try a smaller value
else:
lo = mid + 1 #Needs to be increased
return answer
#── Visualization ───────────────────────────────────────────────────────────────────
def visualize_distribution(
chapters: list[int], k: int, max_pages: int
) -> None:
"""Print a way to divide among K people under a max_pages limit."""
readers: list[list[int]] = [[]]
current = 0
for ch in chapters:
if current + ch > max_pages:
readers.append([])
current = 0
readers[-1].append(ch)
current += ch
print(f"Distribute to {len(readers)} people with max_pages={max_pages}:")
for i, r in enumerate(readers):
print(f"Reader {i+1}: {r} (Sum={sum(r)})")
print(f"→ {'Possible ✅' if len(readers) <= k else 'Not possible ❌'}")
#── Main ─────────────────────────────────────────────────────────────────────────
if __name__ == "__main__":
chapters = [100, 200, 300, 400, 500, 600, 700]
k = 3
print("=" * 55)
print("Book page distribution — Minimize maximum pages for K people")
print("=" * 55)
print(f"Chapter pages: {chapters}")
print(f"Number of readers K: {k}")
print(f"lo : {max(chapters)} (minimum if each handles one chapter)")
print(f"hi : {sum(chapters)} (maximum if one reads all)")
#Check for monotonicity
lo, hi = max(chapters), sum(chapters)
sample_points = range(lo, hi + 1, (hi - lo) // 20 or 1)
print(f"{'max_pages':>10} {'readers':>8} {'Possible?':>6}")
for mp in sample_points:
readers_needed = 1
cur = 0
for ch in chapters:
if cur + ch > mp:
readers_needed += 1
cur = ch
else:
cur += ch
ok = readers_needed <= k
print(f"{mp:>10} {readers_needed:>8} {'✅' if ok else '❌'}")
answer = min_max_pages(chapters, k)
print(f"→ Minimum = {answer} pages")
visualize_distribution(chapters, k, answer)
#Test various k values
print("=== Minimum maximum pages by K ===")
for ki in range(1, len(chapters) + 1):
result = min_max_pages(chapters, ki)
print(f" K={ki}: {result}")
#Verify accuracy
assert min_max_pages([1, 2, 3, 4, 5], 2) == 9 # [1,2,3] [4,5]
assert min_max_pages([1, 2, 3, 4, 5], 5) == 5 #Each person one
assert min_max_pages([1, 2, 3, 4, 5], 1) == 15 #One person all
print("Accuracy check: PASS")
src/04_kth_smallest.pyPython
"""Topic: The k-th smallest number in a multiplication table (parametric search)
Time: O(m log(m*n)) m*n = table size
Space: O(1)
Problem: What is the k-th smallest number in an m×n multiplication table?
Multiplication table[i][j] = i*j (1 ≤ i ≤ m, 1 ≤ j ≤ n)
Decision function: "Are there at least k numbers less than or equal to x?"
Number of numbers less than or equal to x = sum(min(x//i, n) for i in range(1, m+1))"""
import math
#── Decision Function ─────────────────────────────────────────────────────────
def count_leq(m: int, n: int, x: int) -> int:
"""The number of numbers less than or equal to x in an m×n multiplication table.
Number of j in row i satisfying i*j ≤ x = min(x//i, n)"""
count = 0
for i in range(1, m + 1):
count += min(x // i, n)
return count
def find_kth_in_multiplication_table(m: int, n: int, k: int) -> int:
"""The k-th smallest number in an m×n multiplication table.
Search range: [1, m*n]"""
lo, hi = 1, m * n
while lo < hi:
mid = (lo + hi) // 2
if count_leq(m, n, mid) >= k:
hi = mid #If there are k or fewer elements below mid, reduce hi
else:
lo = mid + 1
return lo
#── Brute-force verification ───────────────────────────────────────────────────────────
def brute_force_kth(m: int, n: int, k: int) -> int:
"""O(mn log mn) brute-force solution calculation."""
table = sorted(i * j for i in range(1, m + 1) for j in range(1, n + 1))
return table[k - 1]
#── Visualization ───────────────────────────────────────────────────────────────────
def print_multiplication_table(m: int, n: int) -> None:
"""Print multiplication table."""
print(f"{m}×{n} multiplication table:")
header = " " + " ".join(f"{j:3d}" for j in range(1, n + 1))
print(header)
for i in range(1, m + 1):
row = f"{i:2d} |" + " ".join(f"{i*j:3d}" for j in range(1, n + 1))
print(row)
def visualize_kth_search(m: int, n: int, k: int) -> None:
"""Visualization of the search process."""
lo, hi = 1, m * n
print(f"{'lo':>5} {'mid':>5} {'hi':>5} {'count_leq':>10} {'direction':>6}")
while lo < hi:
mid = (lo + hi) // 2
cnt = count_leq(m, n, mid)
direction = "hi=mid" if cnt >= k else "lo=mid+1"
print(f"{lo:>5} {mid:>5} {hi:>5} {cnt:>10} {direction}")
if cnt >= k:
hi = mid
else:
lo = mid + 1
print(f"→ k-th = {lo}")
#── Router installation (maximum of minimum) ────────────────────────────────────────────
def max_min_distance(houses: list[int], c: int) -> int:
"""Maximize the minimum distance between C installed routers.
Baekjoon 2110 style.
Decision function: Can you install C routers while maintaining a minimum distance d?"""
houses_sorted = sorted(houses)
def feasible(d: int) -> bool:
count = 1
last = houses_sorted[0]
for h in houses_sorted[1:]:
if h - last >= d:
count += 1
last = h
if count >= c:
return True
return count >= c
lo, hi = 1, houses_sorted[-1] - houses_sorted[0]
answer = 0
while lo <= hi:
mid = (lo + hi) // 2
if feasible(mid):
answer = mid
lo = mid + 1
else:
hi = mid - 1
return answer
def demo_router() -> None:
"""Router installation demo."""
houses = [1, 2, 8, 4, 9]
c = 3
result = max_min_distance(houses, c)
print(f"=== Router installation (Baekjoon 2110 style) ===")
print(f"House locations: {sorted(houses)}")
print(f"Number of routers C={c}")
print(f"Maximum of minimum: {result}")
#── Main ─────────────────────────────────────────────────────────────────────────
if __name__ == "__main__":
m, n = 3, 3
print_multiplication_table(m, n)
print(f"=== k-th smallest number in {m}×{n} table ===")
for k in range(1, m * n + 1):
fast = find_kth_in_multiplication_table(m, n, k)
brute = brute_force_kth(m, n, k)
status = "✓" if fast == brute else "✗"
print(f" k={k:2d}: {fast:>4} {status}")
print()
m2, n2 = 5, 5
print_multiplication_table(m2, n2)
k2 = 10
print(f"{k2}-th smallest number in {m2}×{n2} table:")
visualize_kth_search(m2, n2, k2)
#Large-scale verification
large_result = find_kth_in_multiplication_table(100, 100, 5000)
print(f"100×100 table 5000th: {large_result}")
demo_router()
#Verify accuracy
assert find_kth_in_multiplication_table(3, 3, 5) == 4
assert max_min_distance([1, 2, 8, 4, 9], 3) == 3
print("Accuracy check: PASS")
Assignment
Solve optimization problems by combining decision functions and binary search.
Problem 1: Baekjoon 2805 – Cutting Trees (Silver II)
Problem Description
When cutting N trees at height H, to collect at least M meters, what is the maximum H?
Designing the Decision Function
feasible(H): Is it possible to collect at least M meters when cut to H?
= sum(max(0, tree - H) for tree in trees) >= M
Checking Monotonicity
The larger H is, the less is collected
H=0: collect all (always ✅)
H=max: no collection (always ❌ if M > 0)
Search Direction
if feasible(mid):
answer = mid
left = mid + 1 #Try increasing H (searching for the maximum value)
else:
right = mid - 1 #H needs to be lowered
Notes
Include H=0 in the search range (left=0)
No overflow issue when calculating collected amount (Python)
Problem 2: Baekjoon 2110 – Installing Routers (Gold IV)
Problem Description
Among N houses, when installing routers in C houses,
maximize the minimum distance between adjacent routers.
Designing the Decision Function
feasible(d): Is it possible to install C units while maintaining a minimum distance of d?
- After sorting the houses, install them greedily
- Install at the first house that is at least d away from the last installation location
Search Direction
#Maximum of the minimum → Maximize search
if feasible(mid):
answer = mid
left = mid + 1 #Tried to increase the distance
else:
right = mid - 1 #Need to reduce the distance
Search Range
lo = 1 (minimum adjacent distance)
hi = last house - first house (after sorting)
Implementing the Greedy Decision Function
def feasible(d: int) -> bool:
count = 1
last = houses[0] #Install in the first house without fail
for h in houses[1:]:
if h - last >= d:
count += 1
last = h
return count >= c
Common Notes
Did you set the search range sufficiently large?
Did you check the monotonicity of the decision function?
Is the search direction of Maximum/Minimum correct?
Did you set the initial value of answer?
Did you use input/output optimization (sys.stdin)?
Model Answer
Refer to the reference solution below.
Show the reference solution
Try the assignment yourself first — comparing your attempt with a working solution is where most of the learning happens.
boj_2110.pyPython
"""Baekjoon 2110 – Router Installation
https://www.acmicpc.net/problem/2110
Time : O(N log N + N log(max_dist))
Space : O(N)
Strategy: Maximum of the minimum values → Parametric search
Decision function: Is it possible to install C routers while maintaining a minimum distance of d?"""
import sys
input = sys.stdin.readline
def main() -> None:
n, c = map(int, input().split())
houses = sorted(int(input()) for _ in range(n))
def feasible(d: int) -> bool:
"""Is it possible to install C routers while maintaining a minimum distance of d?"""
count = 1
last = houses[0]
for h in houses[1:]:
if h - last >= d:
count += 1
last = h
if count >= c:
return True
return count >= c
left, right = 1, houses[-1] - houses[0]
answer = 0
while left <= right:
mid = (left + right) // 2
if feasible(mid):
answer = mid
left = mid + 1 #Tried to increase the distance
else:
right = mid - 1 #Need to reduce the distance
print(answer)
if __name__ == "__main__":
main()
boj_2805.pyPython
"""Baekjoon 2805 – Cutting Trees
https://www.acmicpc.net/problem/2805
Time : O(N log(max_height))
Space : O(N)"""
import sys
input = sys.stdin.readline
def main() -> None:
n, m = map(int, input().split())
trees = list(map(int, input().split()))
def feasible(h: int) -> bool:
return sum(t - h for t in trees if t > h) >= m
left, right = 0, max(trees)
answer = 0
while left <= right:
mid = (left + right) // 2
if feasible(mid):
answer = mid
left = mid + 1
else:
right = mid - 1
print(answer)
if __name__ == "__main__":
main()