[AtCoder] ABC155 D – Pairs (400点)
[AtCoder] ARC037 億マス計算 の上位問題です。
問題概要
N個の整数 \(A_1, A_2, A_3, \cdots, A_n\) がある。
2つを選んでその積を \(\frac{N(N-1)}{2}\) 個小さい順に並べた時に、K 番目にある数は何か?
制約
- \(2 \leq N \leq 2 \times 10^5\)
- \(1 \leq K \leq \frac{N(N-1)}{2}\)
- \(-10^9 \leq A_i \leq 10^9\ (1 \leq i \leq N)\)
考え方
単純に考えると、\(\frac{N(N-1)}{2}\) 個の積を計算してソートしなければならないので、O(N^2)以上になって間に合いません。
二分探索が基本方針
実は、K番目の数 X を求める時の典型テクニックとして、
「小さい方からK番目の数が X」
⇔「X-1 以下の数は K個未満で、X以下の数は K 個以上」
⇔「X以下の数が K 個以上 となる最小のX」
という言い換えによって、二分探索で X を高速に求めることができます。
つまり、X を決めた時に、
- X以下の数が K 個以上かどうか
を高速に判定することができれば二分探索が使えます。
X以下の数が K 個以上かどうかの判定
全てのペアの積を計算する時間はありません。2つのものを同時に考えないようにすると、ペアの片方を固定することを思いつきます。
ここで知りたいのは、「X以下の数が何個あるか?」です。ペアの片方を固定して \(A_i=a\) とします。単純化するために、全ての数が正の時のみを考えると、
「X以下の数が何個あるか?」
\(\Leftrightarrow \frac{1}{2} \sum_{i=1}^{N}\) (\( a\cdot b \leq X\) となる b の数(a自身は除く))
\(\Leftrightarrow \frac{1}{2} \sum_{i=1}^{N}\) (\( b \leq \lfloor \frac{X}{a} \rfloor\) となる b の数(a自身は除く))
となります。(同じペアを2回数えているので2で割っています)。
これにより、二分探索を N 回行うことで「X以下の数が何個あるか?」を求めることができます。
正負によって場合分け
先程の判定は、単純化するために、全ての数が正の時のみを考えました。しかし、制約によると入力の値は、負や0の場合も含まれています。
よって以下の3つの場合を考える必要があります。
- X が負の時(正と負の積)
- X が0の時(0と何かの積)
- X が正の時 (正と正の積 or 負と負の積)
ペアの積が、負となるものの個数・0となるものの個数・正となるものの個数はそれぞれ簡単に求まります。X がどれに含まれるかは、Kの値で決まるのです。
解き方
- minus := ペアの積が 負 となる個数
- zero := ペアの積が 0 となる個数
- plus := ペアの積が 正 となる個数
とすると、K番目に小さい数 X について
- X が負の時(Kが minus 以下の時)
- X が0の時(Kが minus+zero 以下の時)
- X が正の時 (Kが minus+zero+plus 以下の時)
のどれになるのか判定し、X について二分探索をする。
X が負の時
正と負の積を小さい順に並べて、K番目に小さい数をXとします。
正の数 a を固定した時、負の数 b が何通りあるかを考えます。
「X以下の数が何個あるか?」
\(\Leftrightarrow \sum\) (\( a\cdot b \leq X\) となる b の数)
\(\Leftrightarrow \sum\) (\( b \leq \lfloor \frac{X}{a} \rfloor\) となる b の数)
X が 0 の時
これは0を出力して終わりです。
X が正の時
正と正の積と、負と負の積を小さい順に並べて、(K-minus-zero)番目に小さい数をXとします。
正と正の積
「X以下の数が何個あるか?」
\(\Leftrightarrow \frac{1}{2} \sum_{i=1}^{N}\) (\( a\cdot b \leq X\) となる b の数(a自身は除く))
\(\Leftrightarrow \frac{1}{2} \sum_{i=1}^{N}\) (\( b \leq \lfloor \frac{X}{a} \rfloor\) となる b の数(a自身は除く))
負と負の積
全て正に直してから、正と正の積と同様に計算すれば良いです。
C++での実装例
ABCのD問題にしては実装が重いです。
#include <bits/stdc++.h> #define LOOP(n) for (int _i = 0; _i < (n); _i++) #define REP(i, n) for (int i = 0; i < (n); ++i) #define FOR(i, r, n) for (int i = (r); i < (n); ++i) #define All(obj) begin(obj), end(obj) using namespace std; using ll = long long; using ull = unsigned long long; int N; ll K; vector<ll> P, M, Z; bool judge_m(ll mid) { ll sum = 0; for (int i = 0; i < (int)P.size(); i++) { ll d = (mid - P[i] + 1) / P[i]; sum += upper_bound(M.begin(), M.end(), d) - M.begin(); } return sum >= K; } bool judge_p(ll mid) { ll sum = 0; for (int i = 0; i < (int)P.size(); i++) { // 正と正の積 ll d = mid / P[i]; ll tmp = upper_bound(P.begin(), P.end(), d) - P.begin(); if (i < tmp) tmp--; // 自身を除く sum += tmp; } for (int i = 0; i < (int)M.size(); i++) { // 負と負の積 ll d = mid / M[i]; ll tmp = upper_bound(M.begin(), M.end(), d) - M.begin(); if (i < tmp) tmp--; // 自身を除く sum += tmp; } sum /= 2; return sum >= K; } int main() { cin >> N >> K; vector<ll> A(N); REP(i, N) cin >> A.at(i); REP(i, N) { // Aを正か負か0かで分類 if (A[i] > 0) { P.push_back(A[i]); } else if (A[i] < 0) { M.push_back(A[i]); } else { Z.push_back(A[i]); } } ll p = P.size(); ll m = M.size(); ll z = Z.size(); ll minus = p * m; ll zero = z * (z - 1) / 2 + z * p + z * m; ll plus = p*(p-1)/2 + m*(m-1)/2; if (K <= minus) { // X が負の時 sort(All(M)); ll l = -1e18 - 1, u = -1; // 二分探索: (l,u]に答えがある while (u - l > 1) { ll mid = (u + l) / 2; if (judge_m(mid)) { u = mid; } else { l = mid; } } cout << u << endl; } else if (K <= minus + zero) { // X が 0 の時 cout << 0 << endl; } else if (K <= minus + zero + plus) { // X が正の時 (else if でなくて else でも良い) K -= (minus + zero); REP(i, m) M[i] = abs(M[i]); // 中身を正に変更 sort(All(P)); sort(All(M)); ll l = -1, u = 1e18; //二分探索: (l,u]に答えがある while (u - l > 1) { ll mid = (u + l) / 2; if (judge_p(mid)) { u = mid; } else { l = mid; } } cout << u << endl; } return 0; }
ディスカッション
コメント一覧
Xを正・0・負で場合分けをして解説している場所で、「Xが負の時、Xが0の時、Xが負の時」と並んでいますが、「Xが負の時、Xが0の時、Xが正の時」ではないでしょうか……?
そのとおりです。ご指摘ありがとうございます!
修正させていただきました。