0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

《アルゴリズムを学ぶ》14.二分探索

0
Last updated at Posted at 2025-03-12

Overview

二分探索(Binary Search)は、ソート済み配列に対し探索範囲を半分に絞っていく高速な探索手法。
探索時間は O(log N) で、線形探索より桁違いに高速。
1.二分探索の基本
2.bisect モジュール(Python)
3.二分探索の応用
4.典型問題を解いてみる

1. 二分探索の基本

(1) 二分探索とは

ソートされた配列から目的の値を高速に探す方法。

手順:

  1. 範囲の中央(mid)を見る
  2. target と比較して、左 or 右どちらか半分を捨てる
  3. 残った方を同じように調べる
    → この繰り返し

時間計算量:O(log N)
データ数 N=100万でもたった 20 回の比較で済む。

(2) 二分探索の条件

2分探索を使うには以下が必須:

✔ データがソート済みである
✔ 単調性(区間が連続 or 一方向に進む性質)
✔ 探索条件が Yes/No で判定できる

(3) 二分探索の基本実装

def binary_search(arr, target):
    left, right = 0, len(arr) - 1
    while left <= right:
        mid = (left + right) // 2

        if arr[mid] == target:
            return mid
        elif arr[mid] < target:
            left = mid + 1
        else:
            right = mid - 1

    return -1

(4) 二分探索が必要になる典型パターン

✦ パターン1:配列内の検索(基本)

配列に target が存在するか
最初/最後の位置を知りたいとき。

✦ パターン2:答えが数値で 単調 なとき(典型的競プロパターン)

例:「条件を満たす最小の X を求める」

  • X 以下で買える商品数
  • 時間 T 以内に終わる人数
  • 距離 D 以上離せる?
  • Y 本切り出せる最小の長さ L

など。

✦ パターン3:Yes/No 判定 × 単調性(判定関数型)

def is_ok(x):
    return 条件

left, right = NG, OK
while abs(left - right) > 1:
    mid = (left + right) // 2
    if is_ok(mid):
        right = mid
    else:
        left = mid
return right

2.bisect モジュール(Python)

Python では bisect を使うと、二分探索を簡単に実装できる。
bisect は内部でC実装の二分探索が使われており $O(log N)$ の高速処理が可能。

import bisect

bisect.bisect_left(arr, x)   # lower_bound
bisect.bisect_right(arr, x)  # upper_bound

使用例:

arr = [1, 2, 2, 3, 5]

print(bisect.bisect_left(arr, 2))   # 1
print(bisect.bisect_right(arr, 2))  # 3
関数 説明
bisect_left(arr, x) x を挿入できる最小のインデックス
bisect_right(arr, x) x を挿入できる最大のインデックス
insort_left(arr, x) x を arr にソート済みで挿入

3.二分探索の応用

(1) 最初に出現する位置を探す(lower_bound

def lower_bound(arr, x):
    left, right = 0, len(arr)
    while left < right:
        mid = (left + right) // 2
        if arr[mid] < x:
            left = mid + 1
        else:
            right = mid
    return left
  • bisect_left を使うと 指定した値の最初の出現位置を取得

(2) 最後に出現する位置を探す(upper_bound

def upper_bound(arr, x):
    left, right = 0, len(arr)
    while left < right:
        mid = (left + right) // 2
        if arr[mid] <= x:
            left = mid + 1
        else:
            right = mid
    return left
  • bisect_right - 1 を使うと 指定した値の最後の出現位置を取得

4. 典型問題を解いてみる

例題1. 標準的な二分探索

問題: 配列 A 内に target が存在するか?

回答 
def binary_search(arr, x):
    left, right = 0, len(arr) - 1
    while left <= right:
        mid = (left + right) // 2
        if arr[mid] == x:
            return True
        elif arr[mid] < x:
            left = mid + 1
        else:
            right = mid - 1
    return False

例題2. 値 X が配列 A に何個含まれるかを求める(bisect の基本)

問題: 整数 N と、昇順ソート済みの整数配列 A(長さ N)、そして整数 X が与えられる。
A の中に 値 X が何個含まれるか を二分探索(bisect)を使って求めよ。

回答 
import bisect

N = int(input())
A = list(map(int, input().split()))
X = int(input())

# A はソート済みという前提
left = bisect.bisect_left(A, X)
right = bisect.bisect_right(A, X)

print(right - left)

例題3. 「判定関数を使った」二分探索(典型パターン)

問題: 長さ N の棒が複数ある。
これらの棒を長さ L で切り分けると、合計で M 本以上作れるかがわかる。
条件「長さ L で M 本以上作れる」を満たす 最大の L を求めよ。
(いわゆる「答えが数値」の二分探索の典型問題)

回答 
# 入力
N, M = map(int, input().split())
A = list(map(int, input().split()))  # 各棒の長さ

# 判定関数: 長さ x で M 本以上切り出せるか?
def is_ok(x):
    return sum(a // x for a in A) >= M

left, right = 0, max(A) + 1  # L の範囲(right は常に OK)
while right - left > 1:
    mid = (left + right) // 2
    if is_ok(mid):
        left = mid  # mid は可能 → もっと大きくできるか探す
    else:
        right = mid  # mid は不可 → 小さくする

print(left)  # 最大の L
0
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?