今日勉強したことを
つらつらと
logo

agc044_aの解き方の解説 Pay to Win 変則的な深さ優先探索(dfs)

2020/05/24 10:05
AtCoder Grand Contest 44のA問題 Pay to Winの解説。自力じゃ解けなかったが、解けてみるとdfsで解ける400点問題らしい問題だった。

感謝の正拳突き1万回ならぬ、競プロサイトで 1 日 1AC(正解)するように頑張る。

昨日はじめて AtCoder Grand Contest(AGC)に挑戦しましたが 1 問も解けずに撃沈。今日は振り返りながらAGC の A 問題 Pay to Win を解いていきます。

逆に答えから辿る

「0 から初めて n へするには」という問題は、最初に答えからたどるか、スタートからたどるか考えます。今回の問題はコイン d 枚を支払って 1 マイナスできるので、スタートから辿ると無限にルートができます。また支払ったコインで DP できそうですが、n が 10^18 とメモリ不足になります。

スタートから辿るのは無理筋なので、答えからたどる方向です。

DFS で間に合う

時間内にここまでは思いついたのですが、どうたどればいいか検討がつきませんでした。コイン d 枚を支払って 1 増減できるせいで、dfs だと深くなりすぎるように思ったせいで、無駄に複雑なロジックを考えてしまいました。

他の方の回答を参考にしてみたら、よく考えたらコイン d 枚を支払って 1 増減できる部分は O(1)で計算可能で再帰する必要がありませんでした。

となると 2,3,5 で割っていくので、余裕で間に合います。

dfs のロジックを考えていきましょう

初めは単純に 1 ずつ減らした場合を考える

1 ずつ減らせばいずれ 0 に到着します。

x * d

で簡単に計算できます。

倍率の倍数になるように足すか引くか 2 パターン辿る

元の問題が?倍にする問題なので、割り切れなければなりません。

割れるようにするためには、倍数になるように足すか引くかする必要があります。 どちらのほうが総コインが低くなるのかわからないため、両方とも探索してみる必要があります。

ひとまず 5 倍にするパターンだけ考えてみましょう。

引く場合

5 で割るコインは c なので、コインを c 枚払います。

割り切れるようにするには x mod 5 の分だけ引けばいいです。x mod 5 * d 枚かかります。

割った後は x / 5 の切り捨てが残るので、次の dfs に x / 5 を渡します。

トータルで c + x % 5 * d + dfs(x / 5)枚かかります。

足す場合

割り切れるようにするために必要なコインの枚数と、割った後に残る x が変わります。

割り切れるように足すには 5 - x mod 5 を足します。5 - x mod 5 * d 枚コインがかかります。

割った後は x / 5 の切り上げが残ります。(x + (5 - 1)) / 5 で切り上げ計算ができます。

トータルで c + (5 - x % 5) * d + dfs((x + (5 - 1)) / 5)枚かかります。

2,3 も同様

2,3 も同じです。

引く場合は 割るコイン枚数 + abs(x % 倍率) * d + dfs(x / 倍率)

足す場合は 割るコイン枚数 + abs(倍率 - x % 倍率) * d + dfs((x + 倍率 - 1) / 倍率)

となります。

メモ化する

dfs(x / 倍率)と dfs((x + 倍率 - 1) / 倍率)はちょうど割り切れる数字の場合同じ結果になるので、メモ化が有効です。メモ化しておかないと O(NlogN)くらいかかってしまいます。

メモ化には unorderedmap が有効です。map は内部的にキーが昇順で並んで保持しているので、参照も insert も O(logN)かかります。unorderedmap は hash で持つので、参照も insert も O(1)です。ただし hash が衝突する場合は最悪 O(n)になる可能性があります。が、競プロの場合キーに数値型を使う場合が多いので、まず衝突は考えなくて大丈夫でしょう。ただタイプ数が多く、O(logN)なので大して遅くもなく、map を好んで使う方が多いようです。

できあがり

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

template<class T>inline bool chmin(T &a, const T &b) {if(a>b){a = b; return 1;} return 0; };

ll n, a, b, c, d;
unordered_map<ll, ll> memo;
ll dfs(ll x){
    if(x == 0) return 0;
    if(x == 1) return d;
    if(memo.count(x)) return memo[x];

    ll result = LLONG_MAX / d > x ? x * d : LLONG_MAX;
    ll rate, pay;

    rate = 2;
    pay = a;
    // 引いて割る
    chmin(result,
        pay +
        x % rate * d +
        dfs(x / rate)
    );
    // 足して割る
    chmin(result,
        pay +
        abs(rate - x % rate) * d +
        dfs((x + rate - 1) / rate)
    );

    rate = 3;
    pay = b;
    // 引いて割る
    chmin(result,
        pay +
        x % rate * d +
        dfs(x / rate)
    );
    // 足して割る
    chmin(result,
        pay +
        abs(rate - x % rate) * d +
        dfs((x + rate - 1) / rate)
    );


    rate = 5;
    pay = c;
    // 引いて割る
    chmin(result,
        pay +
        x % rate * d +
        dfs(x / rate)
    );
    // 足して割る
    chmin(result,
        pay +
        abs(rate - x % rate) * d +
        dfs((x + rate - 1) / rate)
    );


    return memo[x] = result;
}

int main(){
    int t;
    cin >> t;
    for(int i = 0; i < t; i++) {
        memo.clear();
        cin >> n >> a >> b >> c >> d;
        cout << dfs(n) << endl;;
    }
}

© 2023 simodake