AtCoder Regular Contest 021 C - 増築王高橋君

問題

 N個の建物をK回増築することを考える。建物i

  • 最初の価格はA_iである
  • 増築するときには現在の価格に等しい費用がかかる
  • 1回増築すると価格がD_i上昇する

 K回増築するのに必要な価格の合計値の最小値を求めよ。

解法

 解法の流れは、まず最後に増築するときの費用がCであるような場合について、増築回数がK回を超えられるかどうかを判定することでCについての2分探索をする。そして求まったCに基づいてシミュレーションして答えを得る。

 最後に増築するときの費用がCであるとき、各建物iについては価格がC未満であるときはどこかの段階で必ず増築し、Cちょうどであるときは最後に増築するかしないか選択の余地がある。結局、価格がCを超えるような増築回数が可能な増築回数であり、iについてそれらの和を考えればCに対しての可能な増築回数が求まる。それがKを超えていればそのCを最後の増築費用として行うことができ、これは単調性があるので2分探索が可能である。

 シミュレーションはほぼ上と同様であり、Cについて各建物について価格がCを超えるように増築し、費用の総和を求めながら増築回数を数えていく。最後に増築回数の総和がKを超えていれば超過分の操作におけるコストはCなのでそれをかけた値を引いて答えとなる。超過分の操作コストがC未満ということもC超過であるということもないのでそのまま引いてよい。

反省

 1時間34分(8WA)でAC。部分点解法のpriority_queueを使ってO(K\log{N})というのはすぐ見えたし、満点解法としても答えを直接ではなく違う部分を2分探索するというアイデアには至っていたがまとめきれず、1時間過ぎたあたりで解説を読んだ。

 解説のように操作のコストの方に着目する方が良く、最終的な建物の価格で考えていたので混乱してしまった。K回を超えたりすることがあるのでそこを上手く処理しなければならなかったのだが……。

 解説を理解してからも初期化のミスで4WA出してしまった。INT_MAXでは小さすぎるのと、LLONG_MAXでは大きすぎるというのを両方やってしまったが、内部の判定でN回足されるため限界値よりは多少小さめにして[tex:1015]やLLONG_MAX / Nとしなければならなかったのだった。あまりにこういうミスが多いので、constexpr ll INF = (ll)1e15とでもしておいた方がいいかもしれない。using ll = int64_tも抵抗があってなかなか入れなかったが、こういう本質的でないミスを回避できるとすれば多少の気持ち悪さには目をつぶるべきか。

コード

#include"bits/stdc++.h"
using namespace std;
using ll = int64_t;

ll K, N;
vector<ll> A, D;

bool isOK(ll price) {
    //最後の増築がprice円である場合に増築回数がK回を超えられるか
    ll num = 0;
    for (ll i = 0; i < N; i++) {
        //各建物についてprice円超過となるように増築する回数
        if (A[i] > price) {
            continue;
        }
        num += (price - A[i]) / D[i] + 1;
    }

    return (num >= K);
}

int main() {
    cin >> K >> N;
    A.resize(N);
    D.resize(N);
    for (ll i = 0; i < N; i++) {
        cin >> A[i] >> D[i];
    }

    ll ok = LLONG_MAX / N;
    ll ng = 0;
    while (ng + 1 != ok) {
        ll mid = (ok + ng) / 2;
        assert(ng <= mid && mid <= ok);
        (isOK(mid) ? ok = mid : ng = mid);
    }

    ll ans = 0;
    ll num = 0;
    for (ll i = 0; i < N; i++) {
        //各建物についてprice円以上となるように増築する
        if (A[i] > ok) {
            continue;
        }
        ll time = (ok - A[i]) / D[i] + 1;

        num += time;
        //ans += (A[i] + A[i] + D[i] * (time - 1)) * time / 2;
        ans += A[i] * time + time * (time - 1) / 2 * D[i];
    }

    //足し過ぎた分を引く
    ans -= ok * (num - K);

    cout << ans << endl;
}