AGC027 B - Garbage Collector


このエントリーをはてなブックマークに追加

2018-09-15のAGCに出場。Aと、Bの部分点が解けて325位。Bは考え方は正しかったのですが、計算途中のオーバーフローに泣きました。上位陣でもBはスルーしている人が多かった中で、DPをやりたくなる衝動を抑えつつ部分点が取れたので、少し自信が付きました。もっとも証明できてない箇所があるのですが。

以下、Bの解法とコードです。g++の拡張(__int128)を使っているため、Visual Studioではビルドが通りません。

// 本番時、解説PDFと同様に考えられたが、オーバーフローでlargeがWA。
// 証明できていない箇所がある。
//
// [解法]
// 以下の場合を例にとって考える
//    |------*-----*-----*------*-----*------*-----*
//    0      a     b     c      d     e      f     g
// 原点から距離iの場所にあるゴミを、ゴミiと呼ぶことにする。
//
// ロボットがゴミ箱にゴミを捨てる回数をK回と固定して考える。例えばK=3とする。
// 
// このとき、
// 1回目のゴミ捨ては、「ゴミgまで行ってゴミgを拾い、ゴミa ~ dのどれかを拾いながら原点に戻る」
// 2回目のゴミ捨ては、「ゴミfまで行ってゴミfを拾い、ゴミa ~ dのどれかを拾いながら原点に戻る」
// 3回目のゴミ捨ては、「ゴミeまで行ってゴミeを拾い、ゴミa ~ dのどれかを拾いながら原点に戻る」
// のが最適となる。本番中も直感的にそう思ったが、証明できていない。解説pdfにも「明らか」としか書かれていない。
//
// 上記を信じると、ゴミを拾いに行く行きのコストは常に定数となる。
// したがって残りの問題は、「3回のゴミ捨ての帰りのうち、ゴミa, b, c, dをどのタイミングで拾うか」
// を、帰りのコストが最小になるように決める問題となる。
//
// まずゴミa~cはないものとして、ゴミdについてついて考える。するとゴミdは3回のうち
// どのタイミングで拾っても、かかるコストは同じになることがわかる。よって1回目で拾うことにする。
//
// 次、ゴミcについて考える。選択肢として、1回目で拾うか、2回目で拾うかの2通りがある。
// (3回目に拾うのは、2回目に拾うのとコストが同じであるので考える必要がない)
// これは2回目に拾うのがよい。
// なぜなら、もし1回目で拾うと、ゴミbからゴミcまでの区間のコストが9になるのに対し、
// 2回目で拾うと、同区間のコストが4になるから。
// (ここも証明が怪しい)
//
// 同様に、遠くにあるゴミから順に、1回目のゴミ捨て、2回目のゴミ捨て、3回目のゴミ捨て、
// また最初に戻って1回目のゴミ捨て、...と拾うのが最適。
//
// この場合だと、
// 1回目のゴミ捨ては、「ゴミgまで行ってゴミgを拾い、ゴミd、ゴミaを拾いながら原点に戻る」
// 2回目のゴミ捨ては、「ゴミfまで行ってゴミgを拾い、ゴミcを拾いながら原点に戻る」
// 3回目のゴミ捨ては、「ゴミeまで行ってゴミgを拾い、ゴミbを拾いながら原点に戻る」
// が最適となる。
//
// 累積和を使うとこれを効率的に計算でき、largeが解ける。

#include <algorithm>
#include <cassert>
#include <climits>
#include <cmath>
#include <cstdio>
#include <cstring>
#include <deque>
#include <functional>
#include <iomanip>
#include <iostream>
#include <map>
#include <numeric>
#include <queue>
#include <set>
#include <sstream>
#include <stack>
#include <string>
#include <vector>

using namespace std;
typedef unsigned int uint;
typedef long long ll;
typedef unsigned long long ull;

#define REP(i,n) for(int i = 0; i < (int)(n); ++i)
#define FOR(i,a,b) for(int i = (a); i < (int)(b); ++i)
#define ALL(c) (c).begin(), (c).end()
#define SIZE(v) ((int)v.size())

#define pb push_back
#define mp make_pair
#define mt make_tuple

int main(void)
{
    cin.sync_with_stdio(false);
    ll N, X;
    cin >> N >> X;
    vector<ll> Xs(N);
    REP(n, N) cin >> Xs[n];

    // 原点から遠い順に距離をソート
    auto Ys = Xs;
    reverse(ALL(Ys));

    // 累積和を計算
    vector<ll> Y_sum(N);
    Y_sum[0] = Ys[0];
    FOR(i, 1, N) Y_sum[i] = Y_sum[i - 1] + Ys[i];

    __int128 ans = 1e18;  // ★ 最終的な解は1e18以下に収まるのだが、他の解候補がlong long intの範囲を超えることが
                          // あるため、__int128を使う必要がある!!!
    FOR(k, 1, N + 1) { // ゴミ箱に行く回数をkとする

        // ★この変数がlong long intを超える!!
        // ゴミを捨てる回数に応じたコストをまず計算
        __int128 local_ans = X * k;

        // 距離dにあるゴミを取りに行くコストはd, 取ったあと帰るコストは2^2 * d = 4d。
        // よって足して5d。
        // k回のゴミ捨てで、原点から遠いゴミk個をそれぞれ取って帰るコストは、
        // 累積和を使って以下のように計算できる。
        local_ans += 5 * Y_sum[k-1];

        ll i = 2 * k - 1;
        ll n = 1;

        // まだ取っていないゴミのうち、遠くにあるゴミから順番にk個のゴミを、
        // k回のゴミ捨てでの帰路にそれぞれ1個ずつ取っていくところをシミュレート。
        // もともとn個のゴミを持って帰るところをn+1個のゴミを持って帰ることにすることで
        // 増えるコストは、((n+2)^2 - (n+1)^2 = 2n + 3)。
        while (i <= N - 1) {
            local_ans += (2LL * n + 3) * (Y_sum[i] - Y_sum[i - k]);
            n += 1;
            i += k;
        }
        // 端数処理
        local_ans += (2LL * n + 3) * (Y_sum[N - 1] - Y_sum[i - k]);

        ans = min(ans, local_ans);
    }

    // __int128は出力に失敗するのでllにキャストする
    ll ans2 = (ll)ans;
    // ゴミを拾うコストは常に定数なので最後に足す
    cout << (ans2 + N * X) << endl;

    return 0;
}