模擬国内2010D Mr. リトー郵便局 (AOJ2200)

解けてうれしかったので書きました。新規性は無いです。

問題概要

onlinejudge.u-aizu.ac.jp

 N頂点 M辺の無向グラフがある。無向グラフの各辺は「陸路」か「海路」であり、海路である辺は船が無いと利用することができない。陸路はそのような制限は無いが、船をもっている状態で陸路を移動する場合、船は移動前の頂点に置いていくことになる。それぞれの辺には非負整数の重みがある。

 R要素の数列 Zを与える。あなたは一隻船をもった状態で頂点 Z_1にいる。船はこれ以外に存在しない。この状態から Z_2, Z_3, \dots, Z_Rをこの順に含むような移動の方法で(連続で含んでいる必要は無い)、通った辺の重みの最小値を求めよ。

制約

  •  2\ \le\ N\ \le\ 200

  •  1\ \le\ M\ \le\ 10^{4}

  •  1\ \le\ R\ \le\ 10^{3}

  • それぞれの辺のコストは 1以上 10^{3}以下

  • 条件を満たす( Zを部分列に含む)移動の方法が存在する

  • 多重辺が存在するかもしれない。

  • 入力は複数のテストケースで構成される

  • メモリ制限:  131072KB

  • TL:  8sec

TLE解

辺が非負重みの最短経路問題なので、ダイクストラ法で解くことを考えます。海路が使えるかどうかと、 Zの前から何要素を移動経路に含んだかの状態が欲しいので、以下のように状態を持った頂点を考えます。

 (i, j, k)・・・元のグラフで頂点が iで、船が頂点 jにあって、現在までの移動経路に Zを前から k要素を含んでいる。

このような状態を持った状態で新しくグラフを構築します。辺は以下のように張ります。

  • 元のグラフでコスト tの陸路 (u, v)がある  \rightarrow  1\ \le\ j\ \le\ N, 1\ \le\ k\ \le\ Rについて (u, j, k) (v, j, k)を結ぶコスト tの無向辺を張る

  • 元のグラフでコスト tの海路 (u, v)がある \rightarrow  1\ \le\ k\ \le\ Rについて (u, u, k) (v, v, k)を結ぶコスト tの無向辺を張る

  •  2\ \le\ k\ \le\ R 1\ \le\ j\ \le\ Nについて (Z_k, j, k - 1)から (Z_k, j, k)へのコスト 0の有向辺を張る。

始点は (Z_1, Z_1, 1)としてダイクストラ法を適用し、 k = Rである頂点への最短経路コストの \minが解になります。

  •  (i, j, k) kの方の頂点倍加はAtCoder社長が汎用テクみたいな感じでTwitterでつぶやいていた気がします。

このグラフの大きさは、雑に見積もっても頂点数 O(N^{2}R)、辺数 O(MNR)です。辺数がだいぶ厳しいことになっています。(複数テストケースなので、8secでも厳しいと思います。試してないけど...)

高速化

最短経路について、頂点 Z_iから頂点 Z_{i + 1}へどのような移動をするかを考えます。

陸路のみを使う時・・・頂点 Z_iから頂点 Z_{i + 1}へ陸路のみで最短で移動する

海路を利用する時・・・頂点 Z_iから船がある頂点まで陸路のみで最短で移動する-> 海路のみを利用して船がある頂点から頂点 vまで最短で移動する-> 頂点 vから頂点 Z_{i + 1}で陸路のみで最短で移動する

  •  v 1\ \le\ v\ \le\ Nを満たすどれか

それぞれの移動をすべて「陸路のみの最短経路」と「海路のみの最短経路」の組み合わせで表現できることがわかります。

  • これらは陸路のみのグラフ、海路のみのグラフで全対間最短経路が求まっていれば良いので、ワーシャル・フロイド法でできます。

コストが前計算で求まることがわかったので、解は以下のようなDPで求まります。

DP[i][j] := Zの前からi要素までをすでに移動した(現在頂点がZ_i)、船の位置が頂点jという状態を達成するのにかかる最小コスト

上の陸路のみを使う時、海路を利用する時のコストの計算がそのままDPの遷移になります。

時間計算量はコストの前計算が  O(N^{3} + M)、DPの計算に O(N^{2}R)です。TLE解より改善できていて、TLにも間に合います。

提出コード

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

template <class T, T INF>
vector<vector<T>> WF(int N, vector<tuple<int, int, T>> E, bool directed) {
    vector G(N, vector(N, INF));
    for (int i = 0 ; i < N ; i++) G[i][i] = 0;
    for (auto [u, v, c] : E) {
        G[u][v] = min(G[u][v], c);
        if (!directed) G[v][u] = min(G[v][u], c);
    }
    for (int k = 0 ; k < N ; k++) {
        for (int i = 0 ; i < N ; i++) {
            for (int j = 0 ; j < N ; j++) {
                G[i][j] = min(G[i][j], G[i][k] + G[k][j]);
            }
        }
    }
    return G;
}

bool solve() {
    int N, M; cin >> N >> M;

    if (N == 0) return false;

    vector<tuple<int, int, ll>> L, S;
    for (int _ = 0 ; _ < M ; _++) {
        int u, v, t; cin >> u >> v >> t;
        u--; v--;
        char c; cin >> c;
        if (c == 'L') {
            L.emplace_back(u, v, (ll)t);
        }
        else if (c == 'S') {
            S.emplace_back(u, v, (ll)t);
        }
        else {
            assert(!"input false");
        }
    }

    auto LT = WF<ll, (ll)1e18>(N, L, false), ST = WF<ll, (ll)1e16>(N, S, false);

    for (int i = 0 ; i < N ; i++) assert(ST[i][i] == 0LL);
    for (int i = 0 ; i < N ; i++) assert(LT[i][i] == 0LL);

    int R; cin >> R;
    vector<int> Z(R);
    for (auto& z : Z) cin >> z;
    for (auto& z : Z) z--;

    const ll INF = (ll)1e18;

    vector<ll> dp(N, INF); // pos of ship
    dp[Z[0]] = 0;

    for (int i = 0 ; i + 1 < R ; i++) {
        assert(count(dp.begin(), dp.end(), INF) < N);
        int u = Z[i], v = Z[i + 1];
        vector<ll> ndp(N, INF);
        for (int j = 0 ; j < N ; j++) {
            for (int k = 0 ; k < N ; k++) {
                ll cost = LT[u][j] + ST[j][k] + LT[k][v];
                ndp[k] = min(ndp[k], dp[j] + cost);
            }
        }
        for (int j = 0 ; j < N ; j++) {
            ll cost = LT[u][v];
            ndp[j] = min(ndp[j], dp[j] + cost);
        }
        dp = move(ndp);
    }
    cout << *min_element(dp.begin(), dp.end()) << endl;


    return true;
}

int main() {
    while (solve()) ;
}
  • dp配列を使いまわしています
  • 陸路のみを利用する遷移を忘れていてWAを出して、疑心難儀になってしまいassertを大量に仕込んでしまいました
  • ICPC、JAGでありがちなwhile (solve()) ;

感想

  • TLE解から更にグラフ改造パズルをしようとしていて、沼ってしまいました。
  • 問題の状況について考えるのが大事だなと思わされました。
  • 頂点数に対して辺数が多すぎるところからもワーシャルフロイドを使うことが察せそうです。