アルゴリズム · 28
二分探索
整列された範囲を半分ずつ捨てて、値や、条件を最初に満たす位置を見つけます。
二分探索は、整列されたリストの真ん中を見て、答えがありえない半分を捨てることを繰り返します。
下の例は、低い順に並んだ点数から、質問された点数 x ごとに x 以上の最初の位置を探します。
その位置の番号は x より低い点数の個数でもあるので、その数と、x 点があれば yes、なければ no を表示します。
def lower_bound(a, x): lo, hi = 0, len(a) # 답은 lo부터 hi까지 가운데 어딘가 while lo < hi: mid = (lo + hi) // 2 if a[mid] < x: lo = mid + 1 # mid와 그 왼쪽은 모두 x보다 작아요 else: hi = mid # mid와 그 오른쪽은 모두 x 이상이에요 return lo # x 이상인 첫 자리(없으면 len(a)) n, q = map(int, input().split())scores = list(map(int, input().split()))for x in map(int, input().split()): pos = lower_bound(scores, x) # x보다 낮은 점수의 개수이기도 해요 found = pos < n and scores[pos] == x print(pos, "yes" if found else "no")function lowerBound(a, x) { let lo = 0, hi = a.length; // 답은 lo부터 hi까지 가운데 어딘가 while (lo < hi) { const mid = Math.floor((lo + hi) / 2); if (a[mid] < x) lo = mid + 1; // mid와 그 왼쪽은 모두 x보다 작아요 else hi = mid; // mid와 그 오른쪽은 모두 x 이상이에요 } return lo; // x 이상인 첫 자리(없으면 a.length)} const lines = require("fs").readFileSync(0, "utf8").split("\n");const [n] = lines[0].split(" ").map(Number);const scores = lines[1].trim().split(" ").map(Number);for (const x of lines[2].trim().split(" ").map(Number)) { const pos = lowerBound(scores, x); // x보다 낮은 점수의 개수이기도 해요 const found = pos < n && scores[pos] === x; console.log(pos, found ? "yes" : "no");}import { readFileSync } from "node:fs"; function lowerBound(a: number[], x: number): number { let lo = 0, hi = a.length; // 답은 lo부터 hi까지 가운데 어딘가 while (lo < hi) { const mid: number = Math.floor((lo + hi) / 2); if (a[mid] < x) lo = mid + 1; // mid와 그 왼쪽은 모두 x보다 작아요 else hi = mid; // mid와 그 오른쪽은 모두 x 이상이에요 } return lo; // x 이상인 첫 자리(없으면 a.length)} const lines: string[] = readFileSync(0, "utf8").split("\n");const [n] = lines[0].split(" ").map(Number);const scores: number[] = lines[1].trim().split(" ").map(Number);for (const x of lines[2].trim().split(" ").map(Number)) { const pos: number = lowerBound(scores, x); // x보다 낮은 점수의 개수이기도 해요 const found: boolean = pos < n && scores[pos] === x; console.log(pos, found ? "yes" : "no");}import java.util.Scanner; public class Main { static int lowerBound(int[] a, int x) { int lo = 0, hi = a.length; // 답은 lo부터 hi까지 가운데 어딘가 while (lo < hi) { int mid = lo + (hi - lo) / 2; // (lo + hi) / 2는 큰 수에서 넘쳐요 if (a[mid] < x) lo = mid + 1; // mid와 그 왼쪽은 모두 x보다 작아요 else hi = mid; // mid와 그 오른쪽은 모두 x 이상이에요 } return lo; // x 이상인 첫 자리(없으면 a.length) } public static void main(String[] args) { Scanner sc = new Scanner(System.in); int n = sc.nextInt(), q = sc.nextInt(); int[] scores = new int[n]; for (int i = 0; i < n; i++) scores[i] = sc.nextInt(); for (int i = 0; i < q; i++) { int x = sc.nextInt(); int pos = lowerBound(scores, x); // x보다 낮은 점수의 개수이기도 해요 boolean found = pos < n && scores[pos] == x; System.out.println(pos + " " + (found ? "yes" : "no")); } }}using System; class Program{ static int LowerBound(int[] a, int x) { int lo = 0, hi = a.Length; // 답은 lo부터 hi까지 가운데 어딘가 while (lo < hi) { int mid = lo + (hi - lo) / 2; // (lo + hi) / 2는 큰 수에서 넘쳐요 if (a[mid] < x) lo = mid + 1; // mid와 그 왼쪽은 모두 x보다 작아요 else hi = mid; // mid와 그 오른쪽은 모두 x 이상이에요 } return lo; // x 이상인 첫 자리(없으면 a.Length) } static void Main() { int n = int.Parse(Console.ReadLine().Split(' ')[0]); int[] scores = Array.ConvertAll(Console.ReadLine().Trim().Split(' '), int.Parse); foreach (string s in Console.ReadLine().Trim().Split(' ')) { int x = int.Parse(s); int pos = LowerBound(scores, x); // x보다 낮은 점수의 개수이기도 해요 bool found = pos < n && scores[pos] == x; Console.WriteLine(pos + " " + (found ? "yes" : "no")); } }}#include <iostream>#include <vector> int lowerBound(const std::vector<int> &a, int x) { int lo = 0, hi = a.size(); // 답은 lo부터 hi까지 가운데 어딘가 while (lo < hi) { int mid = lo + (hi - lo) / 2; // (lo + hi) / 2는 큰 수에서 넘쳐요 if (a[mid] < x) lo = mid + 1; // mid와 그 왼쪽은 모두 x보다 작아요 else hi = mid; // mid와 그 오른쪽은 모두 x 이상이에요 } return lo; // x 이상인 첫 자리(없으면 a.size())} int main() { int n, q; std::cin >> n >> q; std::vector<int> scores(n); for (int &s : scores) std::cin >> s; for (int i = 0; i < q; i++) { int x; std::cin >> x; int pos = lowerBound(scores, x); // x보다 낮은 점수의 개수이기도 해요 bool found = pos < n && scores[pos] == x; std::cout << pos << ' ' << (found ? "yes" : "no") << '\n'; }}#include <stdio.h> int lower_bound(const int *a, int n, int x) { int lo = 0, hi = n; // 답은 lo부터 hi까지 가운데 어딘가 while (lo < hi) { int mid = lo + (hi - lo) / 2; // (lo + hi) / 2는 큰 수에서 넘쳐요 if (a[mid] < x) lo = mid + 1; // mid와 그 왼쪽은 모두 x보다 작아요 else hi = mid; // mid와 그 오른쪽은 모두 x 이상이에요 } return lo; // x 이상인 첫 자리(없으면 n)} int main(void) { int n, q; static int scores[10000]; scanf("%d %d", &n, &q); for (int i = 0; i < n; i++) scanf("%d", &scores[i]); for (int i = 0; i < q; i++) { int x; scanf("%d", &x); int pos = lower_bound(scores, n, x); // x보다 낮은 점수의 개수이기도 해요 int found = pos < n && scores[pos] == x; printf("%d %s\n", pos, found ? "yes" : "no"); } return 0;}lowerBound(a, x){ lo = 0; // 답은 lo부터 hi까지 가운데 어딘가 hi = a.個数; 間 (lo < hi) { mid = 数学.商(lo + hi, 2); もし (a[mid] < x) { lo = mid + 1; // mid와 그 왼쪽은 모두 x보다 작아요 } でなければ { hi = mid; // mid와 그 오른쪽은 모두 x 이상이에요 } } 返す lo; // x 이상인 첫 자리(없으면 a.개수)} first = コンソール.数読み();n = first[0];scores = コンソール.数読み();それぞれ (x の中 コンソール.数読み()){ pos = lowerBound(scores, x); // x보다 낮은 점수의 개수이기도 해요 found = pos < n && scores[pos] == x; コンソール.書く(pos + " " + (found ? "yes" : "no"));}7 5 40 55 55 70 82 82 95 55 60 82 100 10
1 yes 3 no 4 yes 7 no 0 no
ポイント
- リストが整列されていれば、真ん中の値一つで半分を捨てられます。
真ん中が x より小さければその左もすべて x より小さく、真ん中が x 以上ならその右もすべて x 以上だからです。 - 例の
lower_boundは x 以上の最初の位置を返します。
その位置の値が x と同じなら x があるということで、同じ値がいくつもあればその先頭です。
そういう値がなければ、リストの長さ n を返します。 - 繰り返しのあいだずっと守られる約束があります:
loの左はすべて x より小さく、hiから右はすべて x 以上です。
最初(lo = 0、hi = n)は両側に何もないので成り立ち、lo = mid + 1とhi = midはこの約束を壊しません。
だからloとhiが出会った位置が答えです。 lo < hiのあいだ、midはいつもhiより小さくなります。
そのためlo = mid + 1でもhi = midでも範囲が 1 以上縮み、必ず終わります。- 1 回で半分になるので、100 万個でも多くて 20 回しか見ません。
点数 1 万個に質問 1 万個を私たちのジャッジで計ると、先頭から一つずつ数える Python の解法は 3 秒以上かかり、二分探索は約 0.02 秒でした。 - 値を探すためだけのものではありません。
前のほうはすべて偽で、ある位置から先はすべて真になる条件なら、条件が最初に真になる位置を同じ方法で探せます。
例の条件は「値が x 以上である」です。
言語ごとの書き方
| Python | mid = (lo + hi) // 2。組み込みは import bisect — bisect.bisect_left(a, x) が x 以上の最初の位置(例の関数と同じ)、bisect.bisect_right(a, x) が x より大きい最初の位置 |
|---|---|
| JavaScript | const mid = Math.floor((lo + hi) / 2); — / は小数になるので、必ず切り捨てます。組み込みの二分探索はないので自分で書きます |
| TypeScript | JavaScript と同じです |
| Java | int mid = lo + (hi - lo) / 2;。組み込みの Arrays.binarySearch(a, x) は、あればその位置、なければ -(入る位置) - 1 を返します(例の点数で 60 は -4)。同じ値がいくつもあると、そのうちどの位置が返るかは決まっていません(ここでは 82 は先頭の 4 ではなく 5)。リストは Collections.binarySearch |
| C# | int mid = lo + (hi - lo) / 2;。組み込みの Array.BinarySearch(a, x) は、なければ負の数を返し、~ を付けると入る位置になります(60 は -4、~(-4) は 3)。同じ値がいくつもあると先頭とは限りません(82 は 5)。リストは list.BinarySearch(x) |
| C++ | #include <algorithm> の std::lower_bound(v.begin(), v.end(), x) - v.begin() が例の関数と同じです。x より大きい最初の位置は std::upper_bound、あるかどうかだけなら std::binary_search |
| C | int mid = lo + (hi - lo) / 2;。<stdlib.h> の bsearch(&x, a, n, sizeof a[0], cmp) は同じ値のアドレスか NULL しか返さないので、「x 以上の最初の位置」は探せません。例のように自分で書きます |
| Craykon | mid = 数学.商(lo + hi, 2); — / は小数になり、a[1.5] は「[ ] の中には整数の番号を書いてください」と言って止まります。組み込みの二分探索はないので自分で書きます |
よくある間違い
lo = mid で狭めたら終わらない
a = [10, 20, 30]
lo, hi = 0, 2 # 20 以下の最後の位置を探す
while lo < hi:
mid = (lo + hi) // 2
if a[mid] <= 20:
lo = mid
else:
hi = mid - 1
print(lo)→ 終わりません。lo = 1、hi = 2 で mid がまた 1 になり、範囲が縮みません
lo = mid を使う形では、真ん中を切り上げにします(mid = (lo + hi + 1) // 2)。
すると 1 を表示して終わります。
いくつもの形を混ぜず、例の形(lo = mid + 1、hi = mid)一つを身につけておくのが安全です。
(lo + hi) / 2 があふれたJava · C# · C · C++
int lo = 1500000000, hi = 2000000000;
int mid = (lo + hi) / 2;
System.out.println(mid);→ -397483648 — lo + hi が int の上限(約 21 億)を超えて負の数になりました。C# も同じ値になります。C・C++ では int があふれると何が起こるか決まっていません(未定義動作)。私たちのジャッジでは同じ値になりましたが、いつもそうだと思ってはいけません
int mid = lo + (hi - lo) / 2; と書けば 1750000000 になります。
配列の番号ではまれですが、1 から 20 億のあいだで答えになる数を半分ずつ狭めて探すときには、すぐに出会います。
真ん中を / 2 だけで求めたJavaScript · TypeScript
const a = [10, 20, 30];
let lo = 0, hi = a.length;
while (lo < hi) {
const mid = (lo + hi) / 2;
if (a[mid] < 25) lo = mid + 1;
else hi = mid;
}
console.log(lo);→ 1 — 答えは 2 です。最初の mid が 1.5 で、a[1.5] はエラーにならず undefined なので、1,077 周回ったあげく間違った位置を返します
Math.floor((lo + hi) / 2) で切り捨てます。
すると 2 になります。
二分探索が正しいのは、整列されたリストだけです。
整列されていない
[30, 10, 20] から 30 を bisect_left で探すと、エラーなしで 3(ないという意味)が返ります。始まりの値も一つの差が大事です。
例で
hi を n ではなく n - 1 から始めると、[10, 20, 30] からどの値よりも大きい 99 を探したとき、3 ではなく 2 になります。