NoiminのNoise

競技プログラミング (多め) とWeb (たまに) ,自然言語処理 (ブログではまだ)。数式の書き方を一気に KaTeX に変えようとして記事を全削除してインポートし直すなどしたので,過去にブックマークされた記事は URL が変わってしまっている可能性があります…….

AtCoder Beginner Contest 155 D - Pairs

問題: D - Pairs

公式解説: 解説 pdf

問題概要

$N (2 \leq N \leq 2 \times 10^5)$ 個の整数 $A_1, A_2, \cdots, A_N (-10^9 \leq A_i \leq 10^9)$ が与えられる.

ここから2つの整数を選んでペアにして積をとったとき,$K$ 番目に小さい値を求めよ。

解法概要

積が正,負,0になるペアの数はすぐに計算できる。$A_1, A_2, \cdots, A_N $ に含まれる正の数,負の数,0の数をそれぞれ$n_{pos}, n_{neg}, n_0$ とすると,

  • 積が正になるペアは,同じ符号同士のペアだから $\frac{n_{pos} (n_{pos} - 1)}{2} + \frac{n_{neg} (n_{neg} - 1)}{2}$ 個
  • 積が負になるペアは,異なる符号同士のペアだから $n_{pos} n_{neg}$ 個
  • 積が0になるペアは,0同士のペアか0と非0のペアだから $\frac{n_{0} (n_{0} - 1)}{2} + n_{0}(n_{pos}+n_{neg})$ 個

となる。つまり, $K$ の値が $\left( n_{pos} n_{neg}, n_{pos} n_{neg}+\frac{n_{0} (n_{0} - 1)}{2} + n_{0}(n_{pos}+n_{neg}) \right] $ の範囲に入っているなら答えは 0 である。以下,0以外の値についてのみ考える。

$K$ 番目に小さい値 $ m $ というのは,$ m $ 以下の積を持つペアが $K$組存在する値 $ m $ であると言い換えることができる (等しい積を持つペアが複数あっても適当に順序づけするとする) 。$ m $ を増やしたときに $ m $ 以下の積を持つペアが減ることはないので,「$ m $ 以下の積を持つペアが $K$ 組以上存在するか? ($ m $ は小さい順に並べて $K$ 番目以降か?) 」の境目を二分探索で求めることができる。

あとは二分探索の判定パートを $O(n)$ や $O(n \log n)$ あたりで書ければ解ける。愚直にやると判定パートだけで $O(n^2)$ になってしまうので,あらかじめ値をソートしておいて,判定パートでも二分探索を使う (ソースコード中の,for の中で lower_bound や upper_bound を使っている部分にあたる)。

ペアの片方の値を固定して積が mid 以下になるようなもう片方の値の数を数えて足しこんでいく。私の実装では関数 is_ok の中で答えが正か負かによって場合分けをしている。この場合分けは,$K$ の値が $n_{pos} n_{neg}$ 以下かどうかで決まる。

答えが負の場合はペアのうち正の値側を固定し負の側は積が mid 以上となるものを数え (なので,切り上げ除算を使っている) ,全体の数から引く。答えが正の場合はペアが両方同じ符号なので,絶対値の小さい側を固定し,積が mid 以上となるような大きい側の値の数を数えている。2つの整数を選ぶときの順序は不問なので,同じペアを2回数えないように注意。

ソースコード

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

using namespace std;

using ll =  long long;

constexpr ll LINF = 1LL << 60;

bool is_zero(ll k, ll n_pos, ll n_neg, ll n_zero) {
    return n_pos * n_neg < k && k <= n_pos * n_neg + n_zero * (n_pos+n_neg) + n_zero * (n_zero-1) / 2;
}

bool is_ok(ll mid, ll k, ll n_pos, ll n_neg, ll n_zero, vector<ll> &pos, vector<ll> &neg) {
    ll kk = 0;  // mid 未満の積をもつ組数
    if(k <= n_pos * n_neg) {
        for(int i=0;i<n_pos;++i) {
            ll d = n_neg - ll(lower_bound(neg.begin(), neg.end(), (-mid+pos[i]-1)/pos[i]) - neg.begin());
            kk += d;
        }
    } else {
        kk = n_pos * n_neg + n_zero * (n_pos+n_neg) + n_zero * (n_zero-1) / 2;
        for(int i=0;i<n_pos;++i) {
            ll d = ll(upper_bound(pos.begin()+i+1, pos.end(), mid/pos[i]) - pos.begin()) - (i+1);
            kk += d;
        }
        for(int i=0;i<n_neg;++i) {
            ll d = ll(upper_bound(neg.begin()+i+1, neg.end(), mid/neg[i]) - neg.begin()) - (i+1);
            kk += d;
        }
    }

    return kk >= k;
}

int main() {
    ll n, k, a;
    cin >> n >> k;
    vector<ll> pos, neg;
    for(int i=0;i<n;++i) {
        cin >> a;
        if(a > 0) pos.push_back(a);
        else if(a < 0) neg.push_back(-a);
    }
    sort(pos.begin(), pos.end());
    sort(neg.begin(), neg.end());
    ll n_pos = pos.size(), n_neg = neg.size(), n_zero = n - n_pos - n_neg;

    if(is_zero(k, n_pos, n_neg, n_zero)) {
        cout << 0 << endl;
        return 0;
    }

    // 「小さい順から k 番目以下」を取るためには,積の値は mid で足りるのか?
    ll ng = -LINF, ok = LINF;
    while(ok-ng > 1LL) {
        ll mid = (ok + ng) / 2;
        if(is_ok(mid, k, n_pos, n_neg, n_zero, pos, neg)) {
            ok = mid;
        } else {
            ng = mid;
        }
    }

    cout << ok << endl;

    return 0;
}

所感

なんてことはない (と思いたい) 少し実装が重いだけの二分探索典型問題だと思うんですが,本番で WA 連発して破滅した反省の意を込めて……。