kacho65535の競プロメモ

Atcoderと戯れる予定のブログです

AtCoder Beginner Contest 164 E - Two Currencies

解説が自分には難しかったので備忘録として書きます

問題

https://atcoder.jp/contests/abc164/tasks/abc164_e

配点:500点

解法

頂点間の距離に加えて、金貨銀貨の設定が加わっているため一気に複雑になります。幸い頂点数Nと銀貨の運賃A_iの制約が、1\leqq N \leqq 50, 2\leqq A_i  \leqq 50と抑え目になっているため、ここを生かすことを考えます。

制約からグラフは連結であることがわかるので、任意の2頂点間を移動するとき、最悪の条件でも運賃が銀貨50枚である辺をN-1本移動移動すればたどり着けることがわかります。つまり、任意の頂点にいる時のコインの枚数は0~50(N-1)枚の範囲しか持つ必要がないことがわかります。(もっと言うと運賃の最大値をA_Mとして0~A_M (N-1)枚の範囲)しかし、先ほどの制約からここは思い切って0~2500枚として考えてしまっても問題ないことがわかります。さて、一つの頂点上でコインを持つ状態を複数重ねるという実装をするときは、イメージ的には頂点の真上にエッジを張るようにして遷移させればよいです。要するに、頂点2501i+x (i=0,1,...,n-1,x=0,1,...2500)は頂点iで銀貨をx枚持つ状態を表す、として2501n個の頂点を持つグラフを構築したのちに普通のダイクストラ法で単一始点最短路を求めればよいです。

ここからは、具体的なグラフの構築方法のまとめです。

頂点上で金貨1枚をD_i分かけて銀貨C_iに交換するという操作を表すために、まずは頂点i(i=0,1,...n-1)について、銀貨をx枚持っている状態から銀貨をx+C_i持っている状態へ、コストD_iの有向辺を張るという操作をx=0~2499まで繰り返します。ここで注意したいのが、先ほど決めた添え字のルール上、どの頂点についても、銀貨を2501枚以上持つという状態を考えることができないので、x+C_i2501を超える場合は、辺を問答無用で銀貨2500枚持っている状態に繋ぎます。(ここが公式の解説で"持ちきれない銀貨は捨てる"と表現されている部分)

次に頂点U_i,V_iを結ぶ作業に入ります。まず、頂点の値が1-indexedだと扱いづらいので、どちらもデクリメントしておきます。次に、頂点U_iから頂点V_iに有向辺を結ぶ操作を考えます。移動には必ず正の枚数の銀貨を運賃として払う必要があるので、頂点U_iで銀貨を2500枚持っている状態をはじめとして段々と枚数を下げていけばよいです。運賃は銀貨A_i枚なので、頂点U_iで銀貨をy枚持っているとして、実際の実装では頂点2501U_i+yから頂点2501V_i+(y-A_i)にコストB_i(移動にかかる時間)の有向辺を張ります。頂点U_iで銀貨の枚数がA_i未満ならば辺は張れないことに注意しましょう。これと全く同様の操作を頂点V_iから頂点U_iに対しても行えば、いよいよ目的のグラフの完成です!あとは始点を決めてダイクストラ法を実行すればよいのですが、銀貨の初期値Sも2501以上は切り捨てるよう注意しましょう。あとはそれぞれの頂点への最短経路を求めて出力すれば終了です!お疲れ様でしたー!

コード

#include <bits/stdc++.h>
#define _GLIBCXX_DEBUG
using namespace std;
struct edge
{
    long long to;
    long long cost;
};
typedef pair<long long, long long> P; //P.first:最短距離,P.second:頂点の番号
//頂点V個の負閉路なし重み付きグラフGにおける始点sからの最短距離を求める
long long VV;
vector<vector<edge>> G;
void dijkstra(vector<long long> &dist, long long s)
{
    //greater<P>によってfirstが小さい順に取り出される
    priority_queue<P, vector<P>, greater<P>> que;
    for (long long i = 0; i < VV; i++)
        dist[i] = 1e18;
    dist[s] = 0;
    que.push(P(0, s));
    //queが空になるまで繰り返す
    while (!que.empty())
    {
        P p = que.top();
        que.pop();
        long long v = p.second;
        if (dist[v] < p.first)
            continue; //すでに最短距離ならスルー
        for (long long i = 0; i < G[v].size(); i++)
        {
            edge e = G[v][i];
            //更新作業
            if (dist[e.to] > dist[v] + e.cost)
            {
                dist[e.to] = dist[v] + e.cost;
                que.push(P(dist[e.to], e.to));
            }
        }
    }
}
int main()
{
    cout << fixed << setprecision(30);
    long long N, M, S;
    cin >> N >> M >> S;
    VV = N * 2501; //とりあえず2500枚あれば安泰(0~2500の遷移)
    G.resize(VV);  //グラフを確保
    //入力受け取り
    vector<long long> U(M), V(M), A(M), B(M);
    vector<long long> C(N), D(N);
    for (long long i = 0; i < M; i++)
    {
        cin >> U[i] >> V[i] >> A[i] >> B[i];
        U[i]--;
        V[i]--;
    }
    for (long long i = 0; i < N; i++)
        cin >> C[i] >> D[i];
    //まずは同じ頂点の上部(銀貨が多い方向)へ向かう有向辺を結ぶ
    for (long long i = 0; i < N; i++)
    {
        for (long long j = 0; j < 2500; j++)
        {
            edge e;
            if (j + C[i] >= 2501)
                e.to = i * 2501 + 2500; //行きすぎたら切り捨てる(max2500枚)
            else
                e.to = i * 2501 + (j + C[i]);
            e.cost = D[i];
            G[i * 2501 + j].push_back(e);
        }
    }
    //次に違う頂点間を渡るとき銀貨の必要枚数だけ下るような有向辺を結ぶ(頂点の最下部(銀貨0)より下にいったら結ばない)
    for (long long i = 0; i < M; i++)
    {
        //uからvへの有向辺
        for (long long j = 2500; j >= 0; j--)
        {                                  //j=2500~>0,jはuの現在のコインの枚数
            long long nextcoin = j - A[i]; //nextcoin:vの現在のコインの枚数
            if (nextcoin < 0)
                break;
            edge e;
            e.to = V[i] * 2501 + nextcoin;
            e.cost = B[i];
            G[U[i] * 2501 + j].push_back(e);
        }
        //vからuへの有向辺
        for (long long j = 2500; j >= 0; j--)
        {                                  //j=2500~>0,jはvの現在のコインの枚数
            long long nextcoin = j - A[i]; //nextcoin:uの現在のコインの枚数
            if (nextcoin < 0)
                break;
            edge e;
            e.to = U[i] * 2501 + nextcoin;
            e.cost = B[i];
            G[V[i] * 2501 + j].push_back(e);
        }
    }
    vector<long long> dist(VV);
    dijkstra(dist, min(S, 2500ll)); //頂点0でコインをmin(S,2500)持っている状態からスタート
    //都市t(t=1~>N-1)について、各コインの状態を全探索(一応)
    for (long long i = 1; i < N; i++)
    { //i=1~>N-1
        long long ans = 1e18;
        for (long long j = 0; j <= 2500; j++)
            ans = min(dist[i * 2501 + j], ans);
        cout << ans << endl;
    }
    return 0;
}