【AOJ】AOJ 2826 - ゲームバランス

模擬国内2017D

問題のリンク

解説

単調性があるため、 X で二分探索をする。

 X を決め打ったときに、最小で何体モンスターを倒す必要があるか求めることができれば、それが  M 以上かを判定することで解くことができる。

倒すための最小のモンスター数について求める方法を解説する。

レベルが高くなると、倒すことができるモンスターの集合は単調に大きくなっていくため、同じモンスターをレベル  L とレベル  L + D (D \geq 0) のときに倒して、後者の方が戦闘後のレベルが高い(か等しい)ことを示せれば、貪欲にレベルを上げられるだけ上げて良いと示すことができる。

(後述するが、今倒すことができるモンスターの中で最もレベルが上がるモンスターを求めることは比較的容易にできる。)

どちらのレベルでも倒せるモンスターについて、その強さを  s_i とおく。

(1)  s_{i} \leq L \leq L + D のとき

 L  \max(L + 1, X + s_i ) に、 L + D  \max(L + D + 1, X + s_i ) になる。

それぞれの最大値がどの組合せになるかで場合分けし、いずれの場合も  L + D のほうが戦闘後のレベルが小さくならないことを示す。

 ( \mathrm{i} )   L + 1 L + D + 1 のとき

自明。

 ( \mathrm{ii} )   L + 1 X + s_i のとき

このようなことはない。(  \max(L + 1, X + s_i ) = L + 1 であるため  L + 1 \geq X + s_i であるが、そのときは  \max(L + D + 1, X + s_i ) = L + D + 1 になるため。)

 ( \mathrm{iii} )   X + s_i  L + D + 1 のとき

 \max(L + D + 1, X + s_i ) = L + D + 1 となっているのでOK。

 ( \mathrm{iv} )   X + s_i  X + s_i のとき

等しい。

(2)  L \leq s_i \leq L + D のとき

 L  \max(L + 1, X + 2L - s_i ) に、 L + D  \max(L + D + 1, X + s_i ) になる。

 ( \mathrm{i} )   L + 1 L + D + 1 のとき

自明。

 ( \mathrm{ii} )   L + 1 X + s_i のとき

 L \leq s_i より、 L + 1 \leq s_i + 1

 X \geq 1 より、 s_i + 1 \leq s_i + X

よりOK。

 ( \mathrm{iii} )   X + 2L - s_i  L + D + 1 のとき

 (X + 2L - s_i ) - (L + D + 1) = X + L - s_i - D - 1

 X + s_i \leq L + D + 1 だから  X - L + s_i - D - 1 \leq 0

ここで  L - s_i \leq 0 だから、 X + L - s_i - D - 1 \leq X - L + s_i - D - 1 \leq 0

よりOK。

 ( \mathrm{iv} )   X + 2L - s_i  X + s_i のとき

 (X + 2L - s_i ) - (X + s_i ) = 2 (L - s_i ) \leq 0 よりOK。

(3)  L \leq L + D \leq s_i のとき

 L  \max(L + 1, X + 2L - s_i ) に、 L + D  \max(L + D + 1, X + 2L + 2D - s_i ) になる。

 ( \mathrm{i} )   L + 1 L + D + 1 のとき

自明。

 ( \mathrm{ii} )   L + 1 X + 2L + 2D - s_i のとき

 L + D + 1 \leq X + 2L + 2D - s_i を最後に使うと、

 (L + 1 ) - (X + 2L + 2D - s_i ) = 1 - X - L  - 2D + s_i \leq 1 - X - L  - D + s_i \leq 0

よりOK。

 ( \mathrm{iii} )   X + 2L - s_i  L + D + 1 のとき

 L + D + 1 \geq X + 2L + 2D - s_i を最後に使うと、

 (X + 2L - s_i ) - (L + D + 1) = X + L - s_i - D - 1 \leq X + L - s_i + D - 1 \leq 0

よりOK。

 ( \mathrm{iv} )   X + 2L - s_i  X + 2L + 2D - s_i のとき

 (X + 2L - s_i ) - (X + 2L + 2D - s_i ) = - 2D \leq 0 よりOK。

以上より、貪欲にレベルを上げられるだけ上げて良いことが分かった。

次に、現在のレベルが  L のときに、どのモンスターを倒せば最もレベルが上がるかについて考える。(なお、最後のモンスターを倒せるときには倒す)

倒すモンスターの強さを  s_i とする。

(1)  L \leq s_i のとき

 \max(L + 1, X + 2L - s_i ) になるため、 s_i はなるべく小さくなるように選べば良い。

(2)  L \geq s_i のとき

 \max(L + 1, X + s_i ) になるため、 s_i はなるべく大きくなるように選べば良い。

よって  L 以上で最小のモンスターと、 L 以下で最大のモンスターが候補になるので、これらを実際に試せば良い。

実装について

二分探索では、倒す回数が  M 回以上となる最小の  X を求めるため、  X 区間と対応する結果としては、 絶対にクリアできない | M回以上かかってクリア | M回未満でクリア となる。

二分探索の中では、(1)ボスを倒せるか判定→(2)倒す候補について調べる→(3)どれも倒せなかったらクリアできない=無限回かかるとする→(4)レベル更新

を繰り返している。

最後に求めた ok が条件を満たすか確認する。クリアできない場合と、 M 回未満でクリアしてしまう場合がある。

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

#define REP(i, n) for (int i = 0; i < (n); i++)

void solve(int N, int M) {
    vector<long long> s(N);
    REP(i, N) cin >> s[i];
    auto check = [&](long long X) -> long long {
        long long curlev = 1, count = 0;
        while (true) {
            count++;
            if (curlev + X > s.back()) {
                break;
            }
            long long nextlev = curlev;
            int i = lower_bound(s.begin(), s.end(), curlev) - s.begin();
            for (int j = i - 1; j <= i + 1; j++) {
                if (0 <= j and j < N and curlev + X > s[j]) {
                    nextlev = max(nextlev, curlev + max(1LL, X - abs(s[j] - curlev)));
                }
            }
            if (curlev == nextlev) {
                count = 2000000;
                break;
            }
            curlev = nextlev;
        }
        return count;
    };
    long long ng = 2000000, ok = 1;
    while (ng - ok > 1) {
        long long md = (ok + ng) >> 1;
        if (check(md) >= M) {
            ok = md;
        } else {
            ng = md;
        }
    }
    long long res = check(ok);
    if (res != 2000000 and res >= M) {
        cout << ok << '\n';
    } else {
        cout << -1 << '\n';
    }
    return;
}

int main() {
    int N, M;
    while (cin >> N >> M, !(N == 0 and M == 0)) solve(N, M);
    return 0;
}