0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

K番目の数を決め打ち二分探索で求めたいときのチートシート

Last updated at Posted at 2025-07-04

非本質な部分に関する説明を省くため、本記事に出てくる数字はすべて正の整数であるものとします

 ある配列(ナイーブに扱うと超巨大)において$K$ 番目に小さい数を求めよ、みたいなときに 決め打ち二分探索 をおこなうことはよくあります。

結論

 長さ $N$ の配列において、

  • $K$ 番目に小さい数は、「$X$ 以下となるような数が $K$ 個以上となるような最小の $X$ 」
  • $K$ 番目に大きい数は、「$X$ 超過となるような数が $K$ 個未満となるような最小の $X$」
    • または、「$X$ 以下となるような数が $N-K+1$ 個以上となるような最小の $X$」

 ほかにも色々言い換えはありますが、これが一番バグを踏むおそれが少ないです(と思います)。
 理由は後述します。

理論的背景

「小さい方から」の場合

 アルゴロジック 様が、競技プログラミングで解法を思いつくための典型的な考え方 において、「小さい方から」の場合について端的にまとめてくださっているので、それを引用します。

 「小さい方から $K$ 番目の数が $X$」
⇔「$X-1$ 以下の数は $K$ 個未満で、$X$ 以下の数は $K$ 個以上」
⇔「$X$ 以下の数が $K$ 個以上となる最小の $X$」

 考え方としては、

  • $X-1$ 以下の数(=$X$ 未満の数)が $K$ 個以上ある場合は確実にアウト
  • 一方、$X$ 以下の数が $K$ 個以上あってもセーフ

 という感じかと思います。個人的には、こういう一見して自明でない公式は 暗記した方がいい(なぜなら、手で覚えるぐらいのレベルでないと、本番では安定して使えないため)と考えているので、細かいことは気にせず、

  • 小さい方から $K$ 番目の数は、$X$ 以下の数が $K$ 個以上となる最小の $X$

 と覚えることをおすすめします。

 検証用に、決め打ち二分探索を使う系の代表的問題であると思われる ARC037-C のコードを載せます。

// 筆者の提出(https://atcoder.jp/contests/arc037/submissions/67265219)をAIで改造
#include <iostream>
#include <vector>
#include <algorithm>

int main() {
    std::ios::sync_with_stdio(false);
    std::cin.tie(nullptr);

    int n, k;
    std::cin >> n >> k;

    std::vector<int> a(n), b(n);
    for (int i = 0; i < n; ++i) {
        std::cin >> a[i];
    }
    for (int i = 0; i < n; ++i) {
        std::cin >> b[i];
    }

    // b をソートしておく
    std::sort(b.begin(), b.end());

    // 二分探索の初期値
    long long ng = 0;            // この値では k 個以上 <= とはならない
    long long ok = 1000000000000000000LL; // この値では k 個以上 <= となる

    while (ok - ng > 1) {
        long long mid = (ng + ok) / 2;
        long long cnt = 0;

        // 各 a[i] について mid / a[i] 以下の b[j] の数を数える
        for (int i = 0; i < n; ++i) {
            long long limit = mid / a[i];
            cnt += std::upper_bound(b.begin(), b.end(), limit) - b.begin();
        }

        if (cnt >= k) {
            ok = mid;
        } else {
            ng = mid;
        }
    }

    std::cout << ok << "\n";
    return 0;
}

 ちなみに、ある配列(ソート済)の中で $X$ 以下の個数を求める場合は、upper_bound で求めたインデックスがそのまま答えとなります。これも過去記事で理論的背景を解説していますが、細かい理屈を考えずに覚えた方がいい です。

 更に言うと、「配列 $A$ の要素をすべて $R$ 倍したものにおいて、$X$ 以下となる要素の個数」と、「配列 $A$ において、$\lfloor \frac{X}{R} \rfloor$ 以下となる要素の個数」は厳密に一致しますが、これも覚えた方が(以下略

「超過または以下」を求める場合は切り下げ除算で大丈夫ですが、「以上または未満」を求めるとき(lower_bound はこれにあたります)は切り上げにしないといけないので注意してください。理論的背景は、直前に挙げた過去記事で説明しています。

ほかの言い換え

 条件を整理すると以下のようになります。

image.png

 左の「未満」から攻める方法でも書いてみます。

#include <iostream>
#include <vector>
#include <algorithm>

int main() {
    std::ios::sync_with_stdio(false);
    std::cin.tie(nullptr);

    int n, k;
    std::cin >> n >> k;

    std::vector<int> a(n), b(n);
    for (int i = 0; i < n; ++i) {
        std::cin >> a[i];
    }
    for (int i = 0; i < n; ++i) {
        std::cin >> b[i];
    }

    // b をソートしておく
    std::sort(b.begin(), b.end());

    // ── 初期化を反転 ──
    long long ok = 0;                              // ← 以前の ng 相当の初期値
    long long ng = 1000000000000000000LL;          // ← 以前の ok 相当の初期値

    while (ng - ok > 1) {
        long long mid = (ng + ok) / 2;
        long long cnt = 0;

        // 各 a[i] について mid / a[i] 以下の b[j] の数を数える
        for (int i = 0; i < n; ++i) {
            long long limit = mid / a[i];
            // ── upper_bound → lower_bound に変更 ──
            cnt += std::lower_bound(b.begin(), b.end(), limit) - b.begin();
        }

        // ── 比較演算子を反転 ──
        if (cnt < k) {
            ok = mid;
        } else {
            ng = mid;
        }
    }

    std::cout << ok << "\n";
    return 0;
}

 実はこれ、バグが出ます。

            long long limit = mid / a[i];

 ここが問題で、lower_bound で使うためには繰り上げにする必要があります。なので、正しくは、

            long long limit = (mid+a[i]-1) / a[i];

 となります。本番でこんなバグを踏んだら泣きたくなります。

 このようなことから、筆者としては図の右の方法(=「以下」から攻める方法)を推奨します。理由は以下の通りです。

  • 「以下」を求める限りにおいては、整数除算で余計な対策をしなくてよい
  • 「最大値を最小化する」というスタンダードな二分探索の考え方と一致する

「大きい方から」の場合

 「大きい方から」の場合も、同様に考えていきます。

image.png

  • $X$ 超過の数が $K$ 個未満となるような最小の $X$
  • $X$ 以上の数が $K$ 個以上となるような最大の $X$

 どちらがバグを生みづらいかわかるでしょうか。「超過」を使い、かつ「最小化」を行う前者です。

 または、

  • $X$ 以下の数が $N-K+1$ 個以上となるような最小の $X$

 という言い換えもおすすめです。本質的にはあまり変わりませんが、「小さい方から」を求めるやり方をほとんど流用できるからです。

結論

 という訳で、冒頭の結論に至ります。

 長さ $N$ の配列において、

  • $K$ 番目に小さい数は、「$X$ 以下となるような数が $K$ 個以上となるような最小の $X$ 」
  • $K$ 番目に大きい数は、「$X$ 超過となるような数が $K$ 個未満となるような最小の $X$」
    • または、「$X$ 以下となるような数が $N-K+1$ 個以上となるような最小の $X$」
0
0
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
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?