AOJ 0324 Downhill Race

解法

ダイクストラで解く.
与えられるグラフはDAGになっていて,この上で2回違う条件で最短経路を求めたい.しかし依存関係にあるので独立にはできない.
こういうときは1回目と2回めを同時に処理するのが定番な気がするので,それで考えてみる.つまり,2つの頂点を同時に持つようにする.
そしてこれもある意味定番なのだが,これらの2つの頂点ないし遷移は,ある規則によって順序付けられるようにすると良い.
今回であればトポロジカル順序を利用する.

dist[fst_v][sec_v] := 1回目の経路の末端が頂点 fst_v であり,2回目の経路の末端が頂点 sec_v であるときの最小コスト

とする.

fst_v == sec_v のときの遷移は,以下の2パターンがある.

  • 同じ辺を使って移動する.
  • 違う辺を使って移動する(今回は多重辺がないので違う頂点へ移動する).

辺のコスト t2 を使うのはこの1つ目の場合である.

次に,fst_v != sec_v のときを考える.これは先に述べたトポロジカル順序に則って遷移させる.
トポロジカル順序で fst_v < sec_v のとき,fst_v を先に遷移させる.このとき,遷移時の辺のコストは t1 としてよい.
逆に fst_v > sec_v のときは,sec_v を先に遷移させる.

これでうまくいく.トポロジカル順序で小さい方を先に移動させることで,1回目と2回目の最短経路で t1 を2度使うことはなくなる.
一応示しておく.もし t1 を2回使ってしまうような状況に陥るとしたら,1回めと2回目の経路で共通の頂点を用い,かつそこからの行き先が同じような経路をもつ場合である.
トポロジカル順序で小さい方を先に処理した場合,共通の頂点を使うならば,かならず fst_v == sec_v で処理されるようになる.このときは場合分けしているので t1 を2回使うことはない.
逆に言うとこうしないと t1 を2回使いうる.例えば,共通頂点が v3 として (v1, v2) -> (v3, v2) -> (v4, v2) -> (v4, v3) のように遷移すると,最後の v2 -> v3 のコストでどちらを使えばいいかわからなくなってしまう.

計算量は O( (N^2 + P^2) log N ) となる.

ソースコード

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

using ll = long long;

constexpr ll inf = 1e18;

struct edge {
    int to;
    ll c1, c2;
};

using edges = vector<edge>;
using graph = vector<edges>;

int main() {
    int n, p;
    cin >> n >> p;
    graph g(n);
    for(int i = 0; i < p; ++i) {
        int s, e;
        ll t1, t2;
        cin >> s >> e >> t1 >> t2;
        g[s - 1].push_back(edge{e - 1, t1, t2});
    }

    vector<int> topo(n, -1);
    int idx = 0;
    function<void(int)> calc_topo = [&] (int v) {
        if(topo[v] != -1) return;
        for(auto& e : g[v]) {
            calc_topo(e.to);
        }
        topo[v] = idx++;
    };
    calc_topo(0);

    vector<vector<ll>> dist(n, vector<ll>(n, inf));
    dist[0][0] = 0;
    using state = tuple<ll, int, int>;
    priority_queue<state, vector<state>, greater<state>> que;
    que.emplace(0, 0, 0);
    while(!que.empty()) {
        ll cur_c;
        int fst_v, sec_v;
        tie(cur_c, fst_v, sec_v) = que.top();
        que.pop();
        if(fst_v == sec_v) {
            for(auto& e : g[fst_v]) {
                ll nxt_c = cur_c + e.c1 + e.c2;
                if(dist[e.to][e.to] > nxt_c) {
                    dist[e.to][e.to] = nxt_c;
                    que.emplace(nxt_c, e.to, e.to);
                }
            }

            for(auto& e1 : g[fst_v]) {
                for(auto& e2 : g[sec_v]) {
                    if(e1.to == e2.to) continue;
                    ll nxt_c = cur_c + e1.c1 + e2.c1;
                    if(dist[e1.to][e2.to] > nxt_c) {
                        dist[e1.to][e2.to] = nxt_c;
                        que.emplace(nxt_c, e1.to, e2.to);
                    }
                }
            }
        } else {
            if(topo[fst_v] > topo[sec_v]) {
                for(auto& e : g[fst_v]) {
                    ll nxt_c = cur_c + e.c1;
                    if(dist[e.to][sec_v] > nxt_c) {
                        dist[e.to][sec_v] = nxt_c;
                        que.emplace(nxt_c, e.to, sec_v);
                    }
                }
            } else {
                for(auto& e : g[sec_v]) {
                    ll nxt_c = cur_c + e.c1;
                    if(dist[fst_v][e.to] > nxt_c) {
                        dist[fst_v][e.to] = nxt_c;
                        que.emplace(nxt_c, fst_v, e.to);
                    }
                }
            }
        }
    }

    cout << dist[n - 1][n - 1] << endl;
}

感想

トポロジカル順序で小さい方から遷移するときに,遷移先がもう一方の頂点より大きくないとだめ,みたいなよくわからない処理を書いて(なぜ?)WAを食らってしまった.
他にも最小費用流とか生えそうな見た目をしているけど,経験的に1回め流れるときと2回め流れるときでコストを分けるみたいなフローは書くのが難しいと思う.解けるのかな?
最後に類題を貼り付けておきます.
http://judge.u-aizu.ac.jp/onlinejudge/description.jsp?id=1324

ちなみにこの問題ダイクストラで解きましたがDAGなので普通に DP で良いと思う.log が取れるので定数倍しんどいときは DP で解くべき.