AtCoder Beginner Contest 205: D – Kth Excludedの初心者解説(二分探索)

問題

公式問題はコチラ

問題文

長さ $N$ の正整数列 $A = (A_1, A_2, …, A_N)$ と $Q$ 個のクエリが与えられる。$i (1 \leq i \leq Q)$ 番目のクエリでは正整数 $K_i$ が与えられるので、$A_1, A_2, …, A_N$ のいずれとも異なる正整数のうち、小さい方から数えて $K_i$ 番目のものを求めよ。

制約

  • $1 \leq N, Q \leq 10^5$
  • $1 \leq A_1 < A_2 < … < A_N \leq 10^{18}$
  • $1 \leq K_i \leq 10^{18}$
  • 入力は全て整数

解説

公式解説はコチラ

二分探索というのがよくわかっていなくて、TLEしてしまいました。計算量が最大で $O(QN)$ になっていたのが原因でした。

まず、$A_1, A_2, …, A_N$ のいずれでもない正整数を「良い整数」と定義し、$A_i$ 以下の良い整数の個数を $C_i$ とします。($A_0 = 0, C_0 = 0$)

このとき、$K$ の値によって、以下の2つのパターンが考えられます。

  • $C_N < K$ のとき
  • $C_N \geq K$ のとき

$C_N < K$ のとき

前者のときは二分探索は必要なく、答えは $A_N + (K \, – \, C_N)$ となります。 $A_N$ 以上の整数はすべて良い整数なので、$A_N$ に $K \, – \, C_N$ を足した数が答えとなります。

たとえば以下の場合を考えます。

$N = 3$
$A = 1 ,2, 4$
$K = 4$

$N = 3$ なので $C_3$ を考えると、$C_3 = 1$ となります。このときの答えは、4 + (4 – 1) で答えは 7 になります。たしかに、良い整数は 3, 5, 6, 7, …で小さい方から数えて 4 番目の値は 7 となっているので、計算は正しそうです。

$C_N \geq K$ のとき

これがお待ちかねの二分探索を使う場合ですね。二分探索によって、$C_i \geq K$ を満たす最小の $C_i$ が求まれば $A_{i-1} + (K \, – \, C_{i-1})$ で先と同様に答えが求まります。ここでは、$A_{i-1} < B < A_{i}$ を満たす整数 $B$ はすべて良い整数という事実を使っています。

C++ では 以下の3つ二分探索用の関数が用意されています。

  • binary_search():探索したい key があるかどうかを返す。
  • lower_bound():探索したい key 以上のイテレータの内、最小のものを返す。
  • upper_bound():探索したい key より大きいイテレータの内、最小のものを返す。

今回の用途では、lower_bound() を使います。

$N = 4$
$A = 1 ,4, 6, 9$
$K = 3$

たとえば上記の例だと、良い整数は 2, 3, 5, 7, 8… で $C_0 = 0, C_1 = 0, C_2 = 2, C_3 = 3, C_4 = 5$ となります。lower_bound() で 3 番目のイテレータが出力されます。$A_2 + (K \ – \ C_2) = 4 + (3 \ – \ 2) \ = \ 5$ となるため、正しい答えが導き出されていることがわかります。

二分探索について

二分探索とは、要素数 $N$ の配列から指定された数を探索するアルゴリズムで、計算量を $O(\log N) $ に抑えられるものです。

wikipedia とか qiita とかが参考になると思います!

ソースコード

#include <bits/stdc++.h>
#include <algorithm>
#include <math.h>
#include <string>
using namespace std;

int main(void) {
    int N, Q;
    vector<long long> A;
    vector<long long> C;
    vector<long long> K;
    cin >> N >> Q;

    A.push_back(0);
    C.push_back(0);
    for (int i=1; i<=N; i++) {
        long long a;
        cin >> a;
        A.push_back(a);
        C.push_back(C[i-1] + A[i] - A[i-1] - 1);
    }
    for (int i=0; i<Q; i++) {
        long long k;
        cin >> k;
        K.push_back(k);
    }

    for (int i=0; i<Q; i++) {
        auto it = lower_bound(C.begin(), C.end(), K[i]);
        if (C[N] < K[i]) cout << A[N] + K[i] - C[N] << endl;
        else cout << A[it-C.begin()-1] + K[i] - C[it-C.begin()-1] << endl;
    }
    
    return 0;
}

感想

「にぶたん」とよく略される二分探索についてまとめてみました。二分探索のアルゴリズムの詳細にはふれず、どちらかというと二分探索を活用したAtCoderの解法についてまとめました。

コメント