ABC149E: Handshake
いい問題でした。The 500点って感じ。
【問題概要】
N個の要素を持つ数列が与えられる。
とし、全ての組み合わせにおいてを降順に並べた時、番目までの総和を求めよという問題。
かなり汎用性が高そうな題材なので、無限回既出なんだろうなという気持ちになる。
が最大で になるので、全列挙では残念ながら間に合わない。どうにかかけずに組み合わせを考えたい気持ちになる。
解法を先に提示しておく。
- を降順に並べた際、番目に来る数を求める
- 操作1で求めた番目の数をと置いた時、となる数の総和を求める
(最後に調整が必要な場合もあるが)実はこの2つの操作さえ処理できればこの問題を解くことが出来る。そしてこの処理は一見かかってしまいそうに見えるが、前者は、後者はで処理することが可能である。
以下、各操作について述べる。
1. を降順に並べた際、番目に来る数を求める
本質を言う。
真偽判定として「番目に来る数を仮にKとする。となるような組み合わせの数が本当に個以上あるか?」を考え、二分探索をする。
ここがわからない人に、この問題は早いかも知れない。これなんかをやってまず二分探索を理解するといいと思う。
さて、ではどういったアルゴリズムを用いるかだが、となる組み合わせの数が求められれば上記の真偽判定が出来ることになる。ここではとなる組み合わせの数、という関数を考えてみる(はまやんさんリスペクト)。
さて、ここでこの不等式を移項するととなる。これは各について、よりも大きいの数の合計が、の組み合わせの数と同値であることを意味している。
要はを1つ固定すると、にするために必要なの個数がそれぞれについてわかるということである。
で、これはAを昇順ソートしておくとlower_boundで求められる。詳細な雰囲気は以下のコードで察してほしい。
ll get_count(ll K, const vector<ll> &a) { // aはソート済み int n = (int)a.size(); ll ret = 0ll; rep(i,n) { // K-Axをbと置く ll b = K-a[i]; auto itr = lower_bound(a.begin(), a.end(), b); // lower_boundで得られたイテレータを終端のイテレータから引くと // K-Ax以上であるAyの個数が分かるので、それを各Axについて足していく ret += (a.end()-itr); } return ret; }
Kを二分探索する準備ができたので、やる。
// binary search ll ok = 0; ll ng = LINF; while (abs(ok-ng)>1) { ll mid = (ok+ng)/2; ll cnt = get_count(mid,a); if (cnt>=m) ok = mid; else ng = mid; }
ok という変数に番目に来る数が入っている。
これでを降順に並べた際、番目に来る数を求めることができた。
2. となる数の総和を求める
となる数の総和、という関数を考えてみる(はまやんさんリスペクトその2)。
やっぱりここでもと置く。を1つ固定すると、にするためには以上の全てをと足し合わせていけばいい。
で、やっぱりAを昇順ソートしておくとlower_boundと累積和(を全て足していると間に合わないので)で求められる。詳細な雰囲気は以下のコードで察してほしい。
// s := Aの総和 // sum := Aの累積和 ll get_sum(ll K, ll s, const vector<ll> &a, const vector<ll> &sum) { ll ret = 0; int n = a.size(); rep(i,n) { // K-Axをbと置く ll b = K-a[i]; auto itr = lower_bound(a.begin(), a.end(), b); // lower_boundで得られたイテレータで始点のイテレータを引くと // AyがK-Ax以下となるインデックスがわかるので ll idx = itr-a.begin(); // Aの総和からそこまでの累積和を引くと // K-Ax以上のAyの総和が分かる ret += (s-sum[idx]); // cnt := 足したAyの個数 ll cnt = n-idx; // Axをその分増やす ret += (a[i] * cnt); } return ret; }
これでできた。
3. 足しすぎた分の調整
最初のサンプルはここまでやってきたコードで通すことが出来る。
しかし最初のサンプル
5 3
10 14 19 34 33
を
5 2
10 14 19 34 33
にしても同じ答えが出てしまう。
ここで とし、全ての組み合わせにおいてを降順に並べたものを見てみると
{68, 67, 67, 66, 53, 53, 52, 52, .... } となっているのがわかる。
上から3つ目も2つ目も67になってしまっているため、の答えがの時にも出てしまうのである。
だから、この分を引いてやればいい。
get_count(K)で同じ数になっている中では1番右の数(1番インデックスが大きい数)が得られるので、そこからを引いた個数分Kを引いてやる。
ll K = ok; ll ans = get_sum(K,s,a,sum); ans -= (get_count(K,a)-m)*K;
これで全部通る。
ACコードはこれ。